Parfait — voici la **source de vérité définitive** (mix validé + raffinements). C’est le plan à consigner et à suivre.

# 1) Principes & invariants (V4)

* **Mutations** uniquement via `/api/commands` (users) et `/api/admin/*` (admin).
* **Lectures** en `GET` uniquement. Réponses **canoniques** `Response::ok(...)` / `Response::error(...)`.
* **MySQL only**, jobs **CLI** idempotents (cron-safe).
* **TZ**: logique calendrier en **Europe/Paris**, stockage **UTC**.
* **Idempotence**: clés uniques/UPSERT, *no double send*.

---

# 2) Modèle de données (DDL)

## 2.1 Catégories

```sql
ALTER TABLE notification_categories
  ADD COLUMN audience_mode ENUM('EVERYONE','SUBSCRIBERS') NOT NULL DEFAULT 'EVERYONE',
  ADD COLUMN dispatch_mode ENUM('BROADCAST','PERSONALIZED') NOT NULL DEFAULT 'BROADCAST',
  ADD COLUMN frequency_kind ENUM('IMMEDIATE','EVERY_N_DAYS','WEEKLY','MONTHLY') NOT NULL DEFAULT 'IMMEDIATE',
  ADD COLUMN frequency_param TINYINT UNSIGNED NULL,   -- EVERY_N_DAYS: n>=1 ; WEEKLY: 1..7 (lun..dim) ; MONTHLY: 1..28
  ADD COLUMN anchor_ts BIGINT UNSIGNED NULL,          -- ancre UTC (optionnelle) pour déphaser le slot
  ADD COLUMN allow_user_override TINYINT(1) NOT NULL DEFAULT 0;
```

> Décisions :
>
> * **IMMEDIATE** est **unifié** avec `available_at = created_at` (voir §2.3).
> * **IMMEDIATE × PERSONALIZED** : **interdit** (validation UI + back).

### Séquence des épisodes

```sql
ALTER TABLE notifications_rich
  ADD COLUMN sequence_index INT UNSIGNED NOT NULL,
  ADD UNIQUE KEY uq_notif_sequence (category_id, sequence_index);
```

> PERSONALIZED: `sequence_index` requis — aucun fallback implicite (pas de repli sur `created_at`).

## 2.2 Abonnements

```sql
CREATE TABLE user_subscriptions (
  user_id INT UNSIGNED NOT NULL,
  category_id INT UNSIGNED NOT NULL,
  subscribed TINYINT(1) NOT NULL DEFAULT 1,
  override_kind ENUM('IMMEDIATE','EVERY_N_DAYS','WEEKLY','MONTHLY') NULL,
  override_param TINYINT UNSIGNED NULL,
  cycle_anchor_ts BIGINT UNSIGNED NULL,                -- ancre personnelle pour PERSONALIZED
  last_delivered_notification_id BIGINT UNSIGNED NULL, -- progression (PERSONALIZED ou backfill étalé)
  created_at BIGINT UNSIGNED NOT NULL,
  PRIMARY KEY (user_id, category_id),
  KEY ix_us_cat (category_id),
  CONSTRAINT fk_us_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
  CONSTRAINT fk_us_cat  FOREIGN KEY (category_id) REFERENCES notification_categories(id) ON DELETE CASCADE
);
```

## 2.3 État user×notif (pivot)

```sql
ALTER TABLE notification_user_state
  ADD COLUMN available_at BIGINT UNSIGNED NULL,   -- visibilité in-app (slot)
  ADD COLUMN delivered_at BIGINT UNSIGNED NULL;   -- expédition transport (email/push), optionnel

-- Indexation
CREATE INDEX ix_nus_user_available ON notification_user_state(user_id, available_at);
-- À activer lors de l’activation email/push
-- CREATE INDEX ix_nus_user_delivered ON notification_user_state(user_id, delivered_at);
-- Assurez la contrainte d'unicité logique (user_id, notification_id) via PK/UNIQUE (si pas déjà en place).
```

## 2.4 Base utilisateur & trophées

```sql
CREATE TABLE user_data (
  user_id INT UNSIGNED NOT NULL,
  namespace VARCHAR(64) NOT NULL,   -- 'stats','prefs','themes','answers','features','consent',...
  `key` VARCHAR(128) NOT NULL,
  value_json JSON NOT NULL,
  updated_at BIGINT UNSIGNED NOT NULL,
  PRIMARY KEY (user_id, namespace, `key`),
  CONSTRAINT fk_ud_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);

CREATE TABLE user_achievements (
  user_id INT UNSIGNED NOT NULL,
  code VARCHAR(64) NOT NULL,
  earned_at BIGINT UNSIGNED NOT NULL,
  metadata JSON NULL,
  PRIMARY KEY (user_id, code),
  CONSTRAINT fk_ua_user FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
);
```

## 2.5 Curseur persistant (BROADCAST)

```sql
CREATE TABLE scheduler_meta (
  category_id INT UNSIGNED NOT NULL,
  last_slot_ts BIGINT UNSIGNED NOT NULL DEFAULT 0,
  PRIMARY KEY (category_id),
  CONSTRAINT fk_sm_cat FOREIGN KEY (category_id) REFERENCES notification_categories(id) ON DELETE CASCADE
);
```

---

# 3) Règles fonctionnelles

## 3.1 Audience

* `EVERYONE` : visible par tous.
* `SUBSCRIBERS` : visible **seulement** si `user_subscriptions.subscribed=1`.

## 3.2 Modes d’acheminement

* `BROADCAST` : **mêmes créneaux** pour tous (slots globaux).
* `PERSONALIZED` : **cycle par utilisateur**, à partir du **début de la série** (épisode 1), slots dérivés de `cycle_anchor_ts`.
* **Interdit** : `PERSONALIZED` + `IMMEDIATE`.

## 3.3 Fréquences & ancre

* `IMMEDIATE` : `available_at = created_at`.
* `EVERY_N_DAYS` : `n≥1` (pratique : 1..7 + 14/21/30 acceptés si besoin).
* `WEEKLY` : `frequency_param ∈ [1..7]` (1=lundi…7=dimanche).
* `MONTHLY` : `frequency_param ∈ [1..28]`.
* `anchor_ts` (UTC) optionnel côté catégorie ; **Europe/Paris** pour les calculs, converti en UTC en DB.

## 3.4 Overrides utilisateur

* Autorisés **uniquement si** `dispatch_mode = PERSONALIZED` **et** `allow_user_override=1`.
* Sinon → **422 INVALID_PAYLOAD** (y compris en BROADCAST).
* En cas d’override valide, **override > catégorie**.

## 3.5 Visibilité en lecture

* Une notif est éligible si :

  * **Audience** OK, **et**
  * `available_at <= now()` (IMMEDIATE incluse car `available_at = created_at`), **et**
  * non supprimée/archivée (états existants inchangés).

---

# 4) API & Handlers

## 4.1 Admin Catégories (existants étendus)

* **CRUD** inclut : `audience_mode`, `dispatch_mode`, `frequency_kind`, `frequency_param`, `anchor_ts`, `allow_user_override`, (`sequence_index` géré côté notifs).
* **Validations strictes** (plages param ; interdiction IMMEDIATE×PERSONALIZED).

## 4.2 Abonnements (NonBoardBus → `/api/commands`)

* `User.SubscribeCategory { categoryId }`

  * upsert `(subscribed=1)` ; si `PERSONALIZED` et `cycle_anchor_ts IS NULL` → `cycle_anchor_ts = midnight_EuropeParis_utc(now)` ; `last_delivered_notification_id` = NULL (ou conservé selon politique).
* `User.UnsubscribeCategory { categoryId }` → `subscribed=0`.
* `User.SetCategoryFrequencyOverride { categoryId, kind, param? }`

  * **422** si `allow_user_override=0` ou payload incompatible.

## 4.3 Lecture

* `GET /api/notifications/rich` (et `/:id`) : applique **audience + available_at** + états. JSON de sortie **inchangé**.

## 4.4 Admin “Fiche utilisateur”

* `GET /api/admin/users/:id/data` : profil, `user_data.*`, `user_subscriptions`, métriques `notification_user_state`, `user_achievements`.
* (Actions) via `/api/admin/users/:id/subscriptions/*` **ou** réutilisation `/api/commands`.

---

# 5) Scheduler CLI (idempotent)

## 5.1 Helper `next_slot(now, kind, param, anchor_ts)`

* Calcule le **prochain slot** en **Europe/Paris** (gestion DST), renvoie un **timestamp UTC**.
* Règles :

  * `IMMEDIATE` → `null` (déjà visible via `available_at=created_at`).
  * `EVERY_N_DAYS` → `midnight_local(now)` + k×n jours (ancré si `anchor_ts`).
  * `WEEKLY` → prochain `jour_semaine=param` à `00:00 local`.
  * `MONTHLY` → prochain `jour_mois=param` à `00:00 local`.

## 5.2 BROADCAST (global)

* Pour chaque catégorie `dispatch=BROADCAST` & `frequency_kind != IMMEDIATE` :

  * `slot = next_slot(now, kind, param, anchor_ts)` ; si `now < slot` → skip.
  * Récupérer `last_slot_ts` (table `scheduler_meta`), sinon 0.
  * **Delta** : ne traiter que les notifs **nouvelles depuis** `last_slot_ts`.
  * Pour chaque notif du delta : **UPSERT** `notification_user_state(user, notif)` avec `available_at = slot` pour les **users éligibles** (EVERYONE || SUBSCRIBERS).
  * Mettre à jour `scheduler_meta.last_slot_ts = slot`.
* **Batch & locks** : pagination (ex. 5k users/itération), verrou advisory par `category_id`.

## 5.3 PERSONALIZED (par abonné)

* Pour chaque abonnement `subscribed=1` avec `dispatch=PERSONALIZED` :

  * Résoudre config (override || catégorie) + `anchor = cycle_anchor_ts || category.anchor_ts`.
  * `slot = next_slot(now, kind, param, anchor)` ; si `now < slot` → skip.
  * **Prochain épisode** =
    * si `last_delivered_notification_id IS NULL` → 1er (ordre strict `sequence_index`)
    * sinon → suivant (ordre strict `sequence_index`).
  * **UPSERT** `notification_user_state(user, notif)` avec `available_at = slot`.
  * MAJ `last_delivered_notification_id = notif.id`.
* **Resubscribe** : par défaut, **reprend** où il s’était arrêté (option : `restart_on_resubscribe` si souhaité).

## 5.4 Backfill (BROADCAST) — politiques (option produit, hors Phase 1)

* Objectif produit: rattraper ce qu’un nouvel abonné n’a pas vu avant son abonnement.
* Modes:
  * **NONE (défaut)** : pas de rattrapage historique à l’abonnement.
  * **IMMEDIATE** : pousser d’un coup K items historiques (`available_at = now()`), K paramétrable.
  * **STAGED** : rejouer l’historique à une cadence (réutilise `cycle_anchor_ts` + `last_delivered_notification_id` au niveau de l’abonné), pour un flux étalé.
* Implémentation: en Phase ultérieure (non incluse en Phase 1). Peut être portée par un job ad‑hoc ou un champ optionnel (`backfill_mode`) si nécessaire.

---

# 6) UI

## 6.1 Admin Catégories

* Champs : Audience, Mode (Broadcast/Personnalisé), Fréquence (IMMEDIATE / EVERY_N_DAYS(n) / WEEKLY(jour) / MONTHLY(jour)), `anchor_ts`, `allow_user_override`.
* Validation UI : bloquer **IMMEDIATE×PERSONALIZED**.

## 6.2 Compte → “Mes abonnements”

* Liste des catégories → toggle `subscribed`, select override (si `allow_user_override=1`).

## 6.3 Admin → “Fiche utilisateur”

* Sections : **Stats**, **Abonnements** (toggle + override + progression), **Notifications** (historique), **Q/R**, **Thèmes**, **Trophées**.

> **Fix CSS** modal rich :

```css
.notifrich-modal { overflow: auto; }
.notifrich-preview { max-height: calc(100vh - 240px); }
.notifrich-iframe { height: 100%; }
```

---

# 7) Validations & règles métier

* `EVERY_N_DAYS`: `param >= 1`. Recommander 1..7 (+14/21/30 si nécessaire).
* `WEEKLY`: `1..7`.
* `MONTHLY`: `1..28`.
* Overrides refusés (`422`) si `allow_user_override=0` ou payload incohérent.
* **Lecture** : une seule condition `available_at <= now()` (IMMEDIATE compris via écriture `available_at=created_at` à la création).

---

# 8) Indexation, perf, rétention

* Unicité `(user_id, notification_id)` (PK/UNIQUE).
* Index `(user_id, available_at)`, `(user_id, delivered_at)`.
* (Si volumétrie forte) index `(category_id, available_at)`.
* **Purge** : job qui archive/supprime vieux enregistrements (ex. `available_at < now()-N jours`), selon rétention produit.
* Partitionnement par date si besoin (ultérieur).

---

# 9) Migration (ordre d’exécution)

1. **Migrations DDL** (§2).
2. Initialiser `scheduler_meta` (une ligne par catégorie concernée, `last_slot_ts=0`).
3. **Admin UI** : champs étendus + validations.
4. **Lecture**: activer le filtre `available_at <= now()` (audience + états).
5. **Scheduler** : déployer CLI + crontab (ex. 1×/h).
6. **Fiche utilisateur** (`GET /api/admin/users/:id/data`) + UI.

---

# 10) Tests (obligatoires)

* **Unit** : `next_slot()` (WEEKLY/MONTHLY + DST), résolution override > catégorie, progression PERSONALIZED.
* **Intégration** : `GET /api/notifications/rich` (audience/available_at/archived/deleted).
* **E2E** :

  * Non abonné ne voit pas `SUBSCRIBERS`.
  * BROADCAST : delta-only, idempotent si cron rejoué.
  * PERSONALIZED : nouvel abonné commence à l’épisode 1, avance à sa cadence ; unsubscribe/resubscribe reprend.
  * IMMEDIATE visible via `available_at=created_at`.

---

# 11) Observabilité & ops

* Logs scheduler (catégorie, slot, nb upserts, durée, erreurs).
* Métriques : taux de livraison, temps moyen `created_at → available_at`, backlog.
* Alerting sur erreurs récurrentes / durée anormale.

---

# 12) Sécurité & conformité

* Middleware **auth + CSRF + rate-limit** inchangés, ordre strict.
* **Consentements** dans `user_data(namespace='consent', key='notifications', value_json=...)`.
* Export JSON par utilisateur (admin) pour RGPD.

---

## ✅ Conclusion

* Plan **validé** : simple, robuste, extensible ; respecte **strictement** la V4.
* **Dates réelles** (slots) = *quand* ; **pointeurs** (sequence/cursors) = *quoi* ; `available_at` unifié.
* Modes **BROADCAST** et **PERSONALIZED** cohabitent proprement ; backfill paramétrable.
* Prêt pour implémentation en **4 phases** (migrations → surfaces → scheduler → fiche utilisateur) avec validations, tests et observabilité.
