PR 04b — Authentication : statut User en value object tagué #15

Closed
opened 2026-07-04 12:10:53 +00:00 by momsse · 0 comments
Owner

Migré depuis viziertronic/octant#35 — ouvert le 2026-06-11 par @momsse.

Tranche intercalaire du découpage de feat/authentication (plan : docs/pr-split/feat-authentication.md), issue du retour de review de la PR 04 (#34) sur les gardes de statut.

Branche : split/04b-user-status-value-objectBloquée par : #4
À merger avant : #5 (PR 05) — les seuls call-sites aval de UserState.status (authenticate.use-case.ts, 2 lectures) s'extrairont directement contre le nouveau modèle.
Taille : ~4 fichiers (1 value object nouveau, user.state.ts, user.aggregate.ts + tests)

Motivation

UserState porte aujourd'hui un littéral status + 4 champs nullables (suspensionReason, suspendedAt, anonymizedAt, deletedAt) dont la validité dépend du statut — rien n'empêche un état illégal (status: 'active' avec suspendedAt renseigné). Les méthodes decide* du cycle de vie dupliquent 4 échelles if status === ... aux ordres divergents (relevé en review de la PR 04).

Design

application/value-objects/user-lifecycle.value-object.ts :

class ActiveUser extends Schema.TaggedClass<ActiveUser>()('ActiveUser', {}) {}
class AbsentUser extends Schema.TaggedClass<AbsentUser>()('AbsentUser', {}) {}
class SuspendedUser extends Schema.TaggedClass<SuspendedUser>()('SuspendedUser', {
  reason: Schema.NonEmptyString,
  suspendedAt: Schema.DateTimeUtc,
}) {}
class AnonymizedUser extends Schema.TaggedClass<AnonymizedUser>()('AnonymizedUser', {
  anonymizedAt: Schema.DateTimeUtc,
}) {}
class DeletedUser extends Schema.TaggedClass<DeletedUser>()('DeletedUser', {
  deletedAt: Schema.DateTimeUtc,
}) {}
const UserLifecycle = Schema.Union([...])
  • Les 4 champs nullables migrent dans leur variante et disparaissent de UserState — les états illégaux deviennent irreprésentables.
  • Les échelles d'erreurs de decideSuspendUser/decideReactivateUser/decideAnonymizeUser/decideDeleteUser s'effondrent en un Match.exhaustive partagé — un futur statut (locked, …) casse la compilation partout où il faut décider au lieu de passer silencieusement.
  • Le contrat de non-divulgation pinné en PR 04 (statuts non-actifs → UserNotFoundError dans les 5 commandes active-only, describe status disclosure) est conservé tel quel — ce refactor change la modélisation, pas le comportement.
  • UserNotActiveError (qui transporte le littéral du statut) s'adapte ici ; encode du tag vers le littéral à la frontière si besoin.

Impact aval (mesuré sur la branche de référence)

  • authenticate.use-case.ts : 2 lectures de cursor.state.status → adaptation à l'extraction PR 05.
  • Backoffice : lit user.status sur le read model UserSummary (UserDirectory), pas sur UserStatenon impacté.
  • Pas de persistance du state (aucun snapshot User branché) — pas d'enjeu de sérialisation.

Focus review

Équivalence comportementale stricte avec la PR 04 (les 130 tests existants doivent passer sans changement d'assertions, seuls les fixtures/constructions d'état changent), exhaustivité des Match, disparition effective des champs nullables de UserState.

> _Migré depuis [viziertronic/octant#35](https://github.com/viziertronic/octant/issues/35) — ouvert le 2026-06-11 par @momsse._ Tranche intercalaire du découpage de `feat/authentication` (plan : `docs/pr-split/feat-authentication.md`), issue du retour de review de la PR 04 (#34) sur les gardes de statut. **Branche** : `split/04b-user-status-value-object` — **Bloquée par** : #4 **À merger avant** : #5 (PR 05) — les seuls call-sites aval de `UserState.status` (`authenticate.use-case.ts`, 2 lectures) s'extrairont directement contre le nouveau modèle. **Taille** : ~4 fichiers (1 value object nouveau, `user.state.ts`, `user.aggregate.ts` + tests) ## Motivation `UserState` porte aujourd'hui un littéral `status` + 4 champs nullables (`suspensionReason`, `suspendedAt`, `anonymizedAt`, `deletedAt`) dont la validité dépend du statut — rien n'empêche un état illégal (`status: 'active'` avec `suspendedAt` renseigné). Les méthodes `decide*` du cycle de vie dupliquent 4 échelles `if status === ...` aux ordres divergents (relevé en review de la PR 04). ## Design `application/value-objects/user-lifecycle.value-object.ts` : ```ts class ActiveUser extends Schema.TaggedClass<ActiveUser>()('ActiveUser', {}) {} class AbsentUser extends Schema.TaggedClass<AbsentUser>()('AbsentUser', {}) {} class SuspendedUser extends Schema.TaggedClass<SuspendedUser>()('SuspendedUser', { reason: Schema.NonEmptyString, suspendedAt: Schema.DateTimeUtc, }) {} class AnonymizedUser extends Schema.TaggedClass<AnonymizedUser>()('AnonymizedUser', { anonymizedAt: Schema.DateTimeUtc, }) {} class DeletedUser extends Schema.TaggedClass<DeletedUser>()('DeletedUser', { deletedAt: Schema.DateTimeUtc, }) {} const UserLifecycle = Schema.Union([...]) ``` - Les 4 champs nullables migrent dans leur variante et **disparaissent de `UserState`** — les états illégaux deviennent irreprésentables. - Les échelles d'erreurs de `decideSuspendUser`/`decideReactivateUser`/`decideAnonymizeUser`/`decideDeleteUser` s'effondrent en un `Match.exhaustive` partagé — un futur statut (`locked`, …) casse la compilation partout où il faut décider au lieu de passer silencieusement. - Le contrat de non-divulgation pinné en PR 04 (statuts non-actifs → `UserNotFoundError` dans les 5 commandes active-only, describe `status disclosure`) est **conservé tel quel** — ce refactor change la modélisation, pas le comportement. - `UserNotActiveError` (qui transporte le littéral du statut) s'adapte ici ; encode du tag vers le littéral à la frontière si besoin. ## Impact aval (mesuré sur la branche de référence) - `authenticate.use-case.ts` : 2 lectures de `cursor.state.status` → adaptation à l'extraction PR 05. - Backoffice : lit `user.status` sur le read model `UserSummary` (UserDirectory), pas sur `UserState` — **non impacté**. - Pas de persistance du state (aucun snapshot User branché) — pas d'enjeu de sérialisation. ## Focus review Équivalence comportementale stricte avec la PR 04 (les 130 tests existants doivent passer sans changement d'assertions, seuls les fixtures/constructions d'état changent), exhaustivité des Match, disparition effective des champs nullables de `UserState`.
momsse 2026-07-04 12:10:53 +00:00
  • closed this issue
  • added the
    pr-split
    label
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
momsse/octant#15
No description provided.