# Story: Actualité DTO/Model Layer, ContentSaver, CreationService and Template Registration

## 1. Contexte (Context)

This story implements the backend data layer for the new "Actualité" (news article) page template, following the project's real per-template module pattern (`src/Page/{Template}/{Dto,Model,Service}/`, as implemented for `About`/`List`/`ProjectDetail` — NOT the stale `src/Dto/Page/{Template}/` path described in CLAUDE.md's generic instructions). Each article has a hero image and an arbitrary number of admin-authored content blocks (text-only / text+image-left / text+image-right), each with its own independent `mobileImagePosition` setting.

**Dependency gate — do not start before this lands**: the `blocks` array in `ActualitePageContentDto` is exactly the kind of repeatable, admin-mutable content structure that the project's `section-component-architecture.md` rule currently prohibits admins from adding/removing/reordering. Story "Architectural rule amendment (section-component-architecture.md)" formally documents the exception that makes this template's data shape legitimate. **This story must not begin until that rule-amendment story is reviewed and merged.** This story also depends on Story "Tag entity + admin CRUD", since `CreateActualitePageDto.tagIds` must validate against real, persisted `Tag` entities.

## 2. Le besoin (User Story)

```
En tant qu'administrateur du site (via l'API de création de pages) ou système interne
Je souhaite disposer d'une structure de données validée (DTO/Model) pour le contenu d'un article Actualité, avec ses blocs de contenu répétables
Afin de pouvoir créer et faire persister des articles d'actualité de façon fiable, avec une validation stricte par type de bloc
```

## 3. Liens utiles (Useful Links)

- **Figma**: https://www.figma.com/design/ldjPgBvJ6j2I9RVTa17H33/Galta_2026?node-id=146-1824 (node `146:1824`, "Detail-News_Galta")
- **Spec source**: `spec-driven-development/2026-07-07-actualite-news-page-template.md`, sections "Technical Requirements" and task groups "Backend — DTO & Model layer", "Backend — ContentSaver & CreationService", "Backend — Controller", "Backend — Template registration"
- **Decisions to respect**: Decision #5 (no `ctaLabel`/`ctaUrl` fields — "Découvrir en detail" CTA explicitly out of scope for v1, leave a code comment noting it as a future enhancement); Decision #6 (`mobileImagePosition` field on text-image blocks, `#[Assert\Choice(choices: ['before', 'after'])]`, default `'before'`, independent of the desktop `imagePosition`/`type`)
- **Existing precedent to mirror**:
  - `src/Page/List/Dto/ListPageSectionDto.php`, `src/Page/About/Dto/AboutPageSectionDto.php` (repeatable, type-discriminated block shape)
  - `ListPageContentSaver` (exact pattern for `validateAndMapContent()`, `\DomainException` on violation)
  - CLAUDE.md "When adding a new template type" (steps 1-6, noting paths are stale — use real `src/Page/{Template}/` structure)
- **Architecture rules**: `spec-driven-development/rules/data-transfer-objects.md`, `spec-driven-development/rules/business-services.md`
- **Blocking dependencies**: Story "Architectural rule amendment" (`stories/actualite-architecture-rule-amendment.md`) must merge first; Story "Tag entity + admin CRUD" (`stories/actualite-tag-entity-admin-crud.md`) must merge first (for `tagIds` validation)

## 4. Critères d'acceptation (Acceptance Criteria)

### Scénario 1: Validation du bloc texte seul
```gherkin
Scénario: ActualiteTextBlockFieldsDto valide les champs requis
  Étant donné un `ActualiteTextBlockFieldsDto` avec `title` et `content` vides
  Quand la validation est exécutée
  Alors des violations sont levées sur `title` et `content` (`#[Assert\NotBlank]`)
  Et `subtitle` reste optionnel (nullable) sans provoquer de violation s'il est vide
```

### Scénario 2: Validation du bloc texte+image et mobileImagePosition
```gherkin
Scénario: ActualiteTextImageBlockFieldsDto valide mobileImagePosition
  Étant donné un `ActualiteTextImageBlockFieldsDto` avec `mobileImagePosition` = "sideways" (valeur invalide)
  Quand la validation est exécutée
  Alors une violation est levée car la valeur ne fait pas partie de `['before', 'after']`
  Et si `mobileImagePosition` est omis, sa valeur par défaut est `'before'`
  Et le DTO ne contient aucun champ `ctaLabel`/`ctaUrl` (vérifié par test unitaire, conformément à la Décision #5)
```

### Scénario 3: Discriminateur de type sur ActualiteBlockDto
```gherkin
Scénario: Le type de bloc est validé et détermine la sous-structure fields
  Étant donné un `ActualiteBlockDto` avec `type` = "text"
  Quand `fields` est fourni avec la structure attendue pour un bloc texte
  Alors la validation réussit
  Étant donné un `ActualiteBlockDto` avec un `type` non reconnu (ex: "video")
  Quand la validation est exécutée
  Alors une violation claire est levée sur le champ `type` (`#[Assert\Choice(choices: ['text', 'text-image-left', 'text-image-right'])]`)
```

### Scénario 4: Contenu de page Actualité avec zéro à N blocs
```gherkin
Scénario: ActualitePageContentDto accepte un tableau de blocs de longueur variable
  Étant donné un `ActualitePageContentDto` avec `blocks = []` (aucun bloc)
  Quand la validation est exécutée
  Alors elle réussit (l'admin peut créer la page puis remplir le contenu plus tard via l'éditeur inline)
  Étant donné un `ActualitePageContentDto` avec un mélange de blocs texte et texte+image, dans un ordre quelconque
  Quand la validation est exécutée
  Alors chaque bloc est validé individuellement selon son `type` (`#[Assert\Valid]` sur `blocks`)
  Et le DTO ne contient pas de champ `tags` ni `category` (les tags sont une relation d'entité, pas du JSON)
```

### Scénario 5: Sauvegarde et round-trip via ActualitePageContentSaver
```gherkin
Scénario: Un contenu valide est sauvegardé et se re-sérialise à l'identique
  Étant donné un payload de blocs valide (0 à N blocs, tous types mélangés, chacun avec son propre mobileImagePosition)
  Quand `ActualitePageContentSaver::validateAndMapContent()` est appelé
  Alors la validation réussit, la dénormalisation vers `ActualitePageContentDto` fonctionne et le mapping DTO → Model est correct
  Et une nouvelle normalisation du Model produit exactement la même structure JSON que celle envoyée par l'éditeur

Scénario: Un contenu avec un type de bloc invalide est rejeté
  Étant donné un payload contenant un bloc avec `type` = "unknown"
  Quand `ActualitePageContentSaver::validateAndMapContent()` est appelé
  Alors une `\DomainException` est levée avec les messages de violation concatenés (miroir exact de `ListPageContentSaver`)
```

### Scénario 6: Création d'une page Actualité via CreationService
```gherkin
Scénario: La création avec des blocks vides réussit
  Étant donné un `CreateActualitePageDto` avec `blocks: []` et un slug unique
  Quand `ActualitePageCreationService::create()` est appelé
  Alors la page est créée avec `template = 'actualite'` (forcé, peu importe la valeur du DTO)
  Et la page persiste correctement via le Repository

Scénario: Des tagIds inconnus sont rejetés
  Étant donné un `CreateActualitePageDto` avec `tagIds` contenant un UUID qui ne correspond à aucun `Tag` existant
  Quand `ActualitePageCreationService::create()` est appelé
  Alors la création échoue avec une erreur de validation claire, avant toute persistance

Scénario: Des tagIds valides sont assignés à la page
  Étant donné un `CreateActualitePageDto` avec `tagIds` correspondant à des tags existants
  Quand la page est créée
  Alors la collection de tags de la `Page` créée contient exactement les tags correspondants
```

### Scénario 7: Contrôleur API de création reste orchestration uniquement
```gherkin
Scénario: ActualitePageController ne fait qu'orchestrer
  Étant donné une requête `POST /api/admin/pages/actualite/create` avec un `CreateActualitePageDto` valide (via `#[MapRequestPayload]`)
  Quand le contrôleur traite la requête
  Alors il délègue toute la logique métier à `ActualitePageCreationService`, sans manipuler d'entité directement
  Et il retourne uniquement une `JsonResponse` au format `{success, data|error}` (jamais de `render()` ni de redirection)
  Et une requête avec des `tagIds` invalides produit une erreur 400 avec un message clair
```

### Scénario 8: Enregistrement du template dans TemplateMatcher
```gherkin
Scénario: Le template actualite est reconnu par le système
  Étant donné que `'actualite' => 'pages/actualite.html.twig'` est ajouté à `TemplateMatcher::AVAILABLE_TEMPLATES`
  Quand `TemplateMatcher::getTemplateByName('actualite')` est appelé
  Alors il retourne le chemin `pages/actualite.html.twig`
  Et l'option `<option value="actualite">Actualité</option>` apparaît dans le sélecteur de template de `templates/admin/pages/create.html.twig`
```
