# Patterns back-end validés

Ce fichier contient **uniquement** des patterns back-end :

- testés,
- validés,
- utilisés en conditions réelles.

Objectif : éviter de réinventer la roue et réduire le temps de debug.

Dernière mise à jour : 23-03-2026

---

## Index

- [Format d’erreur API standardisé](#pattern-format-derreur-api-standardise)
- [Middleware de corrélation (requestId / traceId)](#pattern-middleware-correlation-requestid-traceid)
- [Idempotency key pour opérations sensibles](#pattern-idempotency-key-operations-sensibles)
- [Pagination robuste (cursor-based) pour les listings](#pattern-pagination-robuste-cursor-based)
- [Exécution asynchrone des tâches longues (queue + outbox light)](#pattern-execution-asynchrone-taches-longues)
- [Soft delete et archivage explicite](#pattern-soft-delete-archivage-explicite)
- [Webhooks sortants robustes et idempotents](#pattern-webhooks-sortants-robustes-idempotents)
- [Contracts-First / Zod-Infer / No-DTO (monorepo TypeScript fullstack)](#pattern-contracts-first-zod-infer-no-dto)
- [Guard global NestJS — ordre d’enregistrement et décorateurs de bypass](#pattern-guard-global-nestjs)
- [Provider-Strategy pour intégrations tierces — périmètre complet](#pattern-provider-strategy-integrations-tierces)
- [Stripe — metadata sur `subscription_data`, pas sur la Session](#pattern-stripe-subscription-metadata)
- [Webhooks entrants — parsing unique (single constructWebhookEvent)](#pattern-webhook-parsing-unique)
- [Contracts-First — error codes comme contrat obligatoire](#pattern-contracts-error-codes)
- [RedisHealthService avec cache interne court](#pattern-redis-health-cache-court)
- [Sémantique explicite `Trial` vs `Paid` dans Subscription](#pattern-subscription-trial-vs-paid)
- [Restauration d’achats Stripe en 3 étapes](#pattern-restauration-achats-stripe)
- [Mapping explicite de `P2002` Prisma sur create/update de champ unique](#pattern-prisma-p2002-update-unique)
- [Autorisation interne minimale sans RBAC complet](#pattern-autorisation-interne-minimale)
- [Anti-énumération sur endpoints auth liés à un email](#pattern-anti-enumeration-auth-email)
- [Token à usage unique — génération, hash et invalidation atomique](#pattern-token-usage-unique)
- [Next.js runtime-only — orchestration en bord et logique pure testable](#pattern-nextjs-runtime-only-logique-pure-testable)
- [Guardrails multi-tenant — 403 vs 404 selon la sémantique](#pattern-guardrails-multi-tenant-403-404)
- [Repository tenant-aware — `tenantId` obligatoire dans la signature](#pattern-repository-tenant-aware)
- [Défense en profondeur — inclure `tenantId` dans les updates](#pattern-tenantid-dans-updates)
- [Next.js server-only & Server Actions — règles d'isolation](#pattern-nextjs-server-only-isolation)
- [Opérations auth sensibles — atomiques, idempotentes et cohérentes](#pattern-auth-operations-atomiques)
- [Réponse HTTP 200 avec payload métier pour les états d'accès](#pattern-http-200-payload-metier)
- [Quota journalier Redis atomique (INCR + EXPIREAT pipeline)](#pattern-quota-redis-atomique)
- [Filtrage des règles métier dans le service, pas dans le repository](#pattern-filtrage-metier-service)
- [Sérialiser les champs `Decimal` Prisma en string au niveau du repository](#pattern-decimal-prisma-serialisation)
- [Extraire les helpers de résolution tenant dans un module partagé dédié](#pattern-helper-tenant-module-partage)
- [Helper centralisé d'activation de features tenant-scoped](#pattern-helper-feature-flag-tenant)
- [Réutiliser un champ existant plutôt que créer un modèle dédié en V1](#pattern-reutiliser-champ-existant-v1)
- [Valider le protocole d'une URL externe avant de la passer à un lien public](#pattern-validation-url-externe)
- [Utilitaires purs : extraire dans un module sans `server-only`](#pattern-utilitaires-purs-module-partage)
- [EN enforcement optionnel par tenant (toggle + publish gate)](#pattern-en-enforcement-tenant)
- [Prisma — Migration manuelle sans shadow DB (P3014)](#pattern-prisma-migration-manuelle-p3014)

---

## Règle d’or

Si ce n’est pas confirmé comme fonctionnel et utile, **ça n’a rien à faire ici**.

- Pas de “bonnes pratiques” vagues
- Pas de dépendances implicites à une stack
- Si c’est spécifique à un framework / runtime / DB : on le note

---

## Périmètre couvert

- API (REST/GraphQL), services applicatifs
- authn/authz
- contrats (validation / schémas)
- gestion d’erreurs
- DB & migrations
- observabilité
- opérations sensibles (idempotence, retries)
- intégrations (webhooks, jobs async)

---

## Format standard d’un pattern

## Pattern : <Nom clair>

- Objectif : …
- Contexte : …
- Quand l’utiliser : …
- Quand l’éviter : …
- Avantage : …
- Limites / vigilance : …
- Validé le : DD-MM-YYYY
- Contexte technique : (obligatoire) ex. `Node 20 / Postgres 16` ou `Python 3.12 / FastAPI / Redis`

### Implémentation (exemple minimal)

```txt
(contenu)
```

### Checklist (si pertinente)

- Erreurs standardisées
- Validation d’entrée (schéma)
- Observabilité minimale (requestId/traceId + logs)
- Sécurité (authn/authz + secrets)
- Tests au bon niveau
- Idempotence si opération sensible

---

<a id="pattern-format-derreur-api-standardise"></a>

## Pattern : Format d’erreur API standardisé

- Objectif : fournir des erreurs prévisibles, exploitables et cohérentes pour tous les clients.
- Contexte : API consommée par front-end, automatisations ou intégrations externes.
- Quand l’utiliser : dès qu’une API est exposée à autre chose qu’un usage interne trivial.
- Quand l’éviter : jamais.
- Avantage :
  - Debug plus rapide
  - UX maîtrisée côté client
  - Observabilité améliorée
- Limites / vigilance :
  - Discipline requise pour éviter les formats ad hoc
- Validé le : 25-01-2026
- Contexte technique : API HTTP agnostique

### Implémentation (exemple minimal)

```json
{
  "error": {
    "code": "USER_NOT_FOUND",
    "message": "Utilisateur introuvable",
    "requestId": "abc-123"
  }
}
```

### Checklist

- Codes HTTP cohérents (4xx / 5xx)
- Codes d’erreur applicatifs stables
- Message utilisateur non technique
- requestId présent

---

<a id="pattern-middleware-correlation-requestid-traceid"></a>

## Pattern : Middleware de corrélation (requestId / traceId)

- Objectif : relier chaque requête aux logs et erreurs associées.
- Contexte : toute API ou service exposé.
- Quand l’utiliser : systématiquement en production.
- Quand l’éviter : jamais.
- Avantage :
  - MTTR réduit drastiquement
  - Debug cross-services possible
- Limites / vigilance :
  - Doit être propagé partout (logs, erreurs, appels sortants)
- Validé le : 25-01-2026
- Contexte technique : Backend agnostique (HTTP)

### Implémentation (exemple minimal)

```txt
- Générer un requestId à l’entrée si absent
- Le propager dans le contexte de requête
- L’inclure dans chaque log et réponse d’erreur
```

### Checklist

- requestId généré ou repris d’un header existant
- Présent dans tous les logs
- Présent dans les erreurs retournées

---

<a id="pattern-idempotency-key-operations-sensibles"></a>

## Pattern : Idempotency key pour opérations sensibles

- Objectif : empêcher les doublons lors de retries ou timeouts.
- Contexte : création de ressources, paiements, webhooks.
- Quand l’utiliser : toute opération non strictement en lecture.
- Quand l’éviter : endpoints purement GET.
- Avantage :
  - Protection contre doublons
  - Robustesse face aux retries
- Limites / vigilance :
  - Stockage et expiration des clés à gérer
- Validé le : 25-01-2026
- Contexte technique : API HTTP + DB transactionnelle

### Implémentation (exemple minimal)

```txt
- Client fournit Idempotency-Key
- Backend stocke la clé + résultat
- Retry retourne le résultat initial
```

### Checklist

- Clé obligatoire sur endpoints sensibles
- Contrainte d’unicité côté DB
- Comportement documenté

---

<a id="pattern-pagination-robuste-cursor-based"></a>

## Pattern : Pagination robuste (cursor-based) pour les listings

- Objectif : fournir des listings stables et performants sans incohérences entre pages.
- Contexte : endpoints de liste (ex. /users, /orders) avec volume potentiellement important.
- Quand l’utiliser : dès qu’un listing peut dépasser quelques dizaines/centaines d’items ou subir des écritures concurrentes.
- Quand l’éviter : listes strictement petites et statiques.
- Avantage :
  - Résultats stables malgré insertions/suppressions
  - Meilleure performance que l’offset sur gros volumes
  - Expérience client plus fiable
- Limites / vigilance :
  - Nécessite un tri déterministe (champ + tie-breaker)
  - Complexité légèrement supérieure à offset/limit
- Validé le : 25-01-2026
- Contexte technique : API HTTP + DB (Postgres/MySQL), agnostique framework

### Implémentation (exemple minimal)

```txt
- Trier par (createdAt DESC, id DESC) (exemple)
- Le client envoie cursor = dernier (createdAt,id) reçu
- Le backend renvoie nextCursor si plus de résultats
- Ne jamais exposer de cursor implicite ou non documenté
```

### Checklist

- Tri déterministe (avec tie-breaker)
- nextCursor renvoyé et documenté
- Limite max de page (protection)
- Index DB aligné avec le tri

---

<a id="pattern-execution-asynchrone-taches-longues"></a>

## Pattern : Exécution asynchrone des tâches longues (queue + outbox light)

- Objectif : sortir les opérations longues ou fragiles du chemin request/response.
- Contexte : envoi d’emails, appels SaaS, génération de PDF, traitements batch, webhooks sortants.
- Quand l’utiliser : dès qu’une opération peut dépasser la latence acceptable ou dépendre d’un service externe.
- Quand l’éviter : opérations réellement instantanées et sans dépendances externes.
- Avantage :
  - API plus rapide et plus fiable
  - Retries maîtrisés
  - Meilleure résilience aux pannes externes
- Limites / vigilance :
  - Demande une discipline stricte sur l’idempotence
  - Nécessite une stratégie minimale de dead-letter ou d’alerting
- Validé le : 25-01-2026
- Contexte technique : Backend agnostique + DB transactionnelle + worker

### Implémentation (exemple minimal)

```txt
- API écrit un job ou event en DB dans la transaction métier
- Worker lit les jobs en attente et exécute
- Retries avec backoff + compteur
- Statut FAILED ou dead-letter + alerte
- Idempotence par clé métier ou idempotency key
```

### Checklist

- Job créé dans une transaction (évite les pertes)
- Retries et backoff définis
- Dead-letter ou statut FAILED visible
- Idempotence garantie
- Logs corrélés (requestId/traceId)

---

<a id="pattern-soft-delete-archivage-explicite"></a>

## Pattern : Soft delete et archivage explicite

- Objectif : permettre la suppression logique sans perte immédiate de données.
- Contexte : données métier critiques, besoins d’audit, restauration ou conformité.
- Quand l’utiliser : dès qu’une suppression peut avoir des impacts métier ou légaux.
- Quand l’éviter : données purement techniques ou réellement éphémères.
- Avantage :
  - Restauration possible
  - Audit et traçabilité
  - Réduction des suppressions irréversibles
- Limites / vigilance :
  - Complexité accrue sur les requêtes
  - Nécessite une discipline stricte (filtres par défaut)
- Validé le : 25-01-2026
- Contexte technique : API + DB relationnelle

### Implémentation (exemple minimal)

```txt
- Champ deletedAt (nullable) ou status
- Les requêtes standards filtrent deletedAt IS NULL
- Endpoints dédiés pour restauration / purge
- Index DB tenant compte du soft delete
```

### Checklist

- Filtrage soft delete par défaut
- Restauration explicite possible
- Purge maîtrisée (cron / job)
- Index DB adaptés
- Tests sur cas supprimé / restauré

---

<a id="pattern-webhooks-sortants-robustes-idempotents"></a>

## Pattern : Webhooks sortants robustes et idempotents

- Objectif : garantir des intégrations fiables avec des systèmes externes.
- Contexte : notifications, synchronisations, événements métier sortants.
- Quand l’utiliser : dès qu’un événement doit être transmis à un tiers.
- Quand l’éviter : intégrations strictement synchrones et internes.
- Avantage :
  - Tolérance aux pannes réseau
  - Retries maîtrisés
  - Observabilité des échecs
- Limites / vigilance :
  - Gestion des retries et du volume
  - Nécessite une idempotence côté consommateur
- Validé le : 25-01-2026
- Contexte technique : Backend + HTTP + worker/queue

### Implémentation (exemple minimal)

```txt
- Événement persisté (outbox) en DB
- Envoi asynchrone via worker
- Retries avec backoff
- Signature du payload (HMAC)
- Idempotency key dans le header
```

### Checklist

- Payload signé et vérifiable
- Retries + backoff définis
- Dead-letter ou statut FAILED visible
- Idempotence documentée
- Logs corrélés (requestId/traceId)

---

<a id="pattern-contracts-first-zod-infer-no-dto"></a>

## Pattern : Contracts-First / Zod-Infer / No-DTO (monorepo TypeScript fullstack)

- Objectif : avoir une seule source de vérité pour les contrats d’interface entre API et client, sans redéfinition manuelle de types.
- Contexte : monorepo TypeScript avec un package partagé (`packages/contracts` ou équivalent), consommé par le backend et le front/mobile.
- Quand l’utiliser : dès qu’une API est consommée par un client TypeScript dans le même repo.
- Quand l’éviter : si le client est externe (autre organisation, autre langage) — dans ce cas, OpenAPI reste la référence.
- Avantage :
  - Zéro drift entre contrat et implémentation
  - Types TypeScript gratuits via `z.infer<>` — aucune réécriture
  - Changement de contrat = erreur de compilation immédiate côté client
  - Mocks de tests alignés automatiquement
- Limites / vigilance :
  - Ne pas mettre de logique métier dans `packages/contracts` (IO only)
  - Attention aux dépendances circulaires si le package grossit
- Validé le : 07-03-2026
- Contexte technique : TypeScript / Zod / NestJS + Expo (React Native) — pattern agnostique framework

### Implémentation (exemple minimal)

```typescript
// packages/contracts/src/auth/auth.schemas.ts
export const RegisterRequestSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
});
export type RegisterRequest = z.infer<typeof RegisterRequestSchema>; // type GRATUIT

// packages/contracts/src/index.ts
export * from ‘./auth/auth.schemas’;
export * from ‘./errors/error-code’;

// apps/api/src/modules/auth/auth.controller.ts
import type { RegisterRequest } from ‘@monrepo/contracts’;
// + ZodValidationPipe → validation automatique, zéro DTO manuel

// apps/mobile/src/domains/auth/auth.store.ts
import type { RegisterRequest } from ‘@monrepo/contracts’;
// même type, même schéma, zéro duplication
```

### Structure cible du package contracts

```
packages/contracts/src/
  auth/auth.schemas.ts       ← request/response auth
  users/users.schemas.ts     ← request/response users
  billing/billing.schemas.ts ← request/response billing (Epic suivant)
  errors/error-code.ts       ← enum codes d’erreur stables
  http/envelopes.ts          ← { data, meta } / { error, meta }
  index.ts                   ← re-export tout
```

### Ce qui appartient à contracts

- Schémas Zod request/response
- Types inférés (`z.infer<>`)
- Codes d’erreur applicatifs stables
- Enums et constantes partagées (ex : liste officielle de sujets/topics)

### Ce qui n’appartient PAS à contracts

- Logique métier
- Modules/services/guards framework (NestJS, etc.)
- State management client (Zustand, Redux, etc.)

### Checklist

- [ ] Zéro DTO manuel dans l’API — uniquement `z.infer<typeof Schema>`
- [ ] `ZodValidationPipe` global ou par endpoint pour la validation d’entrée
- [ ] Constantes partagées (enums, listes) dans contracts, jamais dupliquées
- [ ] Mocks de tests importent les types depuis contracts

---

<a id="pattern-guard-global-nestjs"></a>

## Pattern : Guard global NestJS — ordre d’enregistrement et décorateurs de bypass

- Objectif : protéger tous les endpoints par défaut, avec un mécanisme explicite pour les exceptions.
- Contexte : API NestJS avec plusieurs guards globaux (authn, authz, feature flags...).
- Quand l’utiliser : dès qu’on a 2+ guards globaux dont l’un dépend du résultat de l’autre.
- Quand l’éviter : si un seul guard suffit.
- Avantage :
  - Sécurité par défaut (opt-out, pas opt-in)
  - Ordre d’exécution garanti et explicite
  - Bypass documenté et traçable via décorateurs
- Limites / vigilance :
  - L’ordre des `APP_GUARD` dans `providers[]` est l’ordre d’exécution — ne pas inverser
  - Exporter le service depuis son module si injecté dans un guard global d’un autre module
- Validé le : 07-03-2026
- Contexte technique : NestJS v10+

### Implémentation (exemple minimal)

```typescript
// app.module.ts
providers: [
  { provide: APP_GUARD, useClass: AuthGuard },          // 1er : peuple request.user
  { provide: APP_GUARD, useClass: EmailVerifiedGuard }, // 2ème : lit request.user
  { provide: APP_GUARD, useClass: EntitlementsGuard },  // 3ème : lit request.user + entitlements
]

// skip-auth.decorator.ts
export const SKIP_AUTH = ‘skipAuth’;
export const SkipAuth = () => SetMetadata(SKIP_AUTH, true);

// auth.guard.ts
const skip = this.reflector.getAllAndOverride<boolean>(SKIP_AUTH, [
  context.getHandler(),
  context.getClass(), // permet @SkipAuth() au niveau classe
]);
if (skip) return true;
```

### Checklist

- [ ] AuthGuard enregistré en premier dans `providers[]`
- [ ] AuthModule exporte AuthService si AuthGuard est dans AppModule
- [ ] Décorateur `@SkipAuth()` sur tous les endpoints publics (auth, health, docs)
- [ ] Tests unitaires sur le guard avec reflector mocké

---

<a id="pattern-provider-strategy-integrations-tierces"></a>

## Pattern : Provider-Strategy pour intégrations tierces — périmètre complet

- Objectif : isoler intégralement la logique propre à un prestataire (Stripe, Brevo, Firebase…) derrière une interface stable, pour éviter la contamination du domaine par le SDK tiers.
- Contexte : backend NestJS/TypeScript avec 1+ prestataires externes (paiement, email, storage…).
- Quand l’utiliser : dès qu’un service applicatif dépend d’un SDK tiers (et plus encore s’il y a des webhooks).
- Quand l’éviter : intégration ponctuelle non critique sans effet de bord (rare) — sinon on perd vite le contrôle.
- Avantage :
  - Testabilité : mock du provider, pas du SDK
  - Remplacement du prestataire sans refactor “en cascade”
  - Responsabilités claires : provider = “parle Stripe”, service = “parle domaine”
- Limites / vigilance :
  - L’interface doit exposer des **types normalisés** (pas de types Stripe)
  - Le provider gère aussi les webhooks : validation signature, parsing event, mapping
- Validé le : 09-03-2026
- Contexte technique : NestJS v10+ / intégration Stripe (webhooks) — pattern généralisable

### Implémentation (exemple minimal)

```typescript
// billing-provider.interface.ts (pas d'import Stripe)
export type BillingPlan = 'MONTHLY' | 'ANNUAL';

export type BillingWebhookResult = {
  userId: string;
  externalId: string;
  plan: BillingPlan;
  status: 'ACTIVE' | 'INACTIVE' | 'CANCELLED';
  currentPeriodEnd: Date | null;
};

export interface BillingProvider {
  createCheckoutSession(userId: string, plan: BillingPlan): Promise<{ checkoutUrl: string }>;
  cancelSubscription(externalId: string): Promise<void>;
  handleWebhook(rawBody: Buffer, signature: string): Promise<BillingWebhookResult | null>;
}

// billing.service.ts (domaine uniquement)
async handleWebhook(rawBody: Buffer, signature: string): Promise<void> {
  const result = await this.billingProvider.handleWebhook(rawBody, signature);
  if (!result) return;
  await this.prisma.subscription.upsert({ /* données normalisées */ });
}
```

---

<a id=”pattern-stripe-subscription-metadata”></a>

## Pattern : Stripe — metadata sur `subscription_data`, pas sur la Session

- Objectif : garantir que `userId` (ou tout identifiant métier) soit accessible dans les events `customer.subscription.*`, pas seulement dans `checkout.session.completed`.
- Contexte : intégration Stripe Checkout avec webhooks abonnement.
- Quand l’utiliser : systématiquement dès qu’on crée une Checkout Session liée à une Subscription.
- Risque si ignoré : `metadata.userId` absent des events `customer.subscription.updated/deleted` → silent failure en prod.
- Validé le : 09-03-2026
- Contexte technique : Stripe API v17+ / NestJS

### Implémentation

```typescript
stripe.checkout.sessions.create({
  metadata: { userId }, // pour checkout.session.completed
  subscription_data: { metadata: { userId } }, // pour customer.subscription.*
});
```

---

<a id=”pattern-webhook-parsing-unique”></a>

## Pattern : Webhooks entrants — parsing unique (single `constructWebhookEvent`)

- Objectif : appeler `constructWebhookEvent` une seule fois par requête, puis router vers des extracteurs purs.
- Contexte : endpoint webhook recevant des events de plusieurs types (subscription, pack, facture…).
- Quand l’utiliser : dès qu’on a 2+ handlers webhook sur le même endpoint.
- Risque si ignoré : double vérification de signature + états partiels possibles (sub OK / pack KO).
- Validé le : 09-03-2026
- Contexte technique : Stripe / NestJS

### Implémentation

```typescript
// 1. Parser unique — 1 seul constructWebhookEvent(rawBody, sig) → event opaque
// 2. Extracteurs purs, sans effet de bord :
handleSubscriptionWebhookEvent(event): WebhookResult | null
handlePackWebhookEvent(event): PackWebhookResult | null
// 3. Orchestrateur unique appelle les extracteurs, persiste les résultats
```

---

<a id=”pattern-contracts-error-codes”></a>

## Pattern : Contracts-First — error codes comme contrat obligatoire

- Objectif : maintenir les codes d’erreur API dans `packages/contracts` pour éviter les clients stringly-typed.
- Contexte : monorepo TypeScript avec `packages/contracts/src/errors/error-code.ts`.
- Règle : toute nouvelle erreur API ⇒ ajout obligatoire dans `error-code.ts` **avant merge**, pas après.
- Risque si ignoré : clients qui testent des strings hardcodées au lieu d’importer l’enum → drift silencieux.
- Validé le : 09-03-2026
- Contexte technique : TypeScript / NestJS + Expo (React Native)

### Checklist

- [ ] Nouvel `error.code` → ajout dans `packages/contracts/src/errors/error-code.ts` en même commit
- [ ] Clients importent l’enum, pas une string littérale
- [ ] PR review : vérifier `error-code.ts` à chaque ajout d’endpoint d’erreur

---

<a id="pattern-redis-health-cache-court"></a>

## Pattern : RedisHealthService avec cache interne court

- Objectif : exposer un état Redis exploitable par les guards globaux sans ping Redis à chaque requête.
- Contexte : backend Node/NestJS avec Redis consulté dans le chemin de décision d’écriture.
- Quand l’utiliser : quand plusieurs requêtes concurrentes doivent consulter l’état Redis.
- Quand l’éviter : si Redis n’est pas consulté dans le chemin request/response.
- Avantage :
  - réduit fortement le flood de `PING`
  - garde un signal d’état suffisamment frais
- Limites / vigilance :
  - la fenêtre de cache doit rester courte
  - l’état initial doit être explicite et assumé
- Validé le : 10-03-2026
- Contexte technique : NestJS / Redis

### Implémentation (exemple minimal)

```txt
- Mémoriser lastStatus et lastCheck
- Si le dernier check a moins de 5s, retourner l’état en cache
- Sinon exécuter un vrai PING et mettre le cache à jour
- Utiliser un état initial optimiste (`up`) si le produit ne doit pas bloquer les écritures au boot
```

### Checklist

- Cache court documenté
- Pas de ping Redis à chaque requête
- Comportement initial explicite

---

<a id="pattern-subscription-trial-vs-paid"></a>

## Pattern : Sémantique explicite `Trial` vs `Paid` dans Subscription

- Objectif : aligner le modèle métier, les guards et les jeux de tests sur une définition unique de l’abonnement payant actif.
- Contexte : modèle `Subscription` où `trialEndsAt` matérialise un essai.
- Quand l’utiliser : dès qu’un même enregistrement supporte trial et abonnement payant.
- Quand l’éviter : si trial et abonnement payant sont modélisés par des entités distinctes.
- Avantage :
  - évite les incohérences silencieuses dans les guards
  - rend les fixtures et mocks e2e cohérents avec la règle métier
- Limites / vigilance :
  - toute logique `isActive` doit préciser si elle signifie “trial ou paid” ou “paid only”
- Validé le : 10-03-2026
- Contexte technique : Backend agnostique / modèle d’abonnement

### Implémentation (exemple minimal)

```txt
- Un abonnement payant actif n’est pas seulement status = ACTIVE
- Il doit aussi avoir trialEndsAt = null
- Les fixtures et mocks e2e d’un abonnement payant fixent toujours trialEndsAt: null
```

### Checklist

- Règle métier explicitée
- Guards alignés sur la sémantique choisie
- Fixtures et seeds cohérents

---

<a id="pattern-restauration-achats-stripe"></a>

## Pattern : restauration d’achats Stripe en 3 étapes

- Objectif : reconstruire un état local cohérent à partir de Stripe sans dépendre d’une hypothèse fragile.
- Contexte : flux de restore purchases mobile/web avec état local potentiellement désynchronisé.
- Quand l’utiliser : dès qu’un utilisateur peut restaurer des achats depuis un nouveau device ou après désynchronisation.
- Quand l’éviter : si l’état Stripe n’est pas la source de vérité.
- Avantage :
  - rend la réconciliation explicite
  - supporte retries et restaurations tardives
- Limites / vigilance :
  - la pagination Stripe et l’idempotence d’écriture restent obligatoires
- Validé le : 10-03-2026
- Contexte technique : Stripe API / backend Node/NestJS

### Implémentation (exemple minimal)

```txt
1. Résolution du customer Stripe (ID persisté en DB, fallback robuste si absent)
2. Reconstruction de l’état Stripe utile au domaine
3. Réconciliation et écritures locales idempotentes
```

### Checklist

- `stripeCustomerId` persistant côté app
- Réconciliation explicite documentée
- Upsert ou écriture idempotente

---

<a id="pattern-prisma-p2002-update-unique"></a>

## Pattern : mapping explicite de `P2002` Prisma sur create/update de champ unique

- Objectif : transformer un conflit d’unicité prévisible en erreur métier exploitable plutôt qu’en 500 opaque.
- Contexte : `create`, `update` ou `upsert` Prisma sur un champ `@unique` alimenté par une source externe, concurrente, ou après un pre-check.
- Quand l’utiliser : dès qu’un champ unique peut entrer en collision — à la création ET à la modification.
- Quand l’éviter : jamais si le champ peut réellement entrer en collision.
- Avantage :
  - réponse client stable
  - diagnostic métier plus rapide
- Limites / vigilance :
  - le mapping doit rester cohérent avec le format d’erreur API standardisé
- Validé le : 10-03-2026
- Contexte technique : Prisma / PostgreSQL / NestJS

### Implémentation (exemple minimal)

```txt
- Catch explicite de PrismaClientKnownRequestError code P2002
- Mapping vers une erreur métier stable
- Conserver requestId et format d’erreur standardisé
```

### Implémentation (exemple complet)

```typescript
import { Prisma } from "@prisma/client";

try {
  await prisma.item.create({ data: { ... } });
  // ou: await prisma.item.update({ where: { id }, data: { ... } });
} catch (err) {
  if (err instanceof Prisma.PrismaClientKnownRequestError && err.code === "P2002") {
    throw new HttpError("Un élément avec ce nom existe déjà.", { status: 409 });
  }
  throw err;
}
```

**Important :** un pre-check applicatif (`findUnique` avant `create`) ne suffit pas contre les race conditions. Le `try/catch P2002` est le seul garde-fou fiable. S’applique à `create`, `update`, `updateMany`, `upsert`.

### Checklist

- `P2002` intercepté sur les creates ET les updates sensibles
- Code d’erreur métier stable (409 Conflict)
- Pas de 500 générique sur conflit prévisible

---

<a id="pattern-autorisation-interne-minimale"></a>

## Pattern : Autorisation interne minimale sans RBAC complet

- Objectif : sécuriser une capacité interne sensible sans ouvrir trop tôt un chantier RBAC complet.
- Contexte : application avec peu de rôles, besoin ponctuel d’une capacité admin ou opérateur clairement identifiée.
- Quand l’utiliser : quand une story métier demande un pouvoir interne limité mais réel.
- Quand l’éviter : si les permissions deviennent nombreuses, hiérarchiques ou contextuelles.
- Avantage :
  - sécurisation rapide et lisible d’une capacité sensible
  - source de vérité backend explicite
  - chemin d’évolution propre vers un RBAC plus complet
- Limites / vigilance :
  - ne pas laisser proliférer des rôles ad hoc non gouvernés
  - ne remplace pas un vrai modèle de permissions si le domaine grossit
- Validé le : 10-03-2026
- Contexte technique : NestJS / auth par session ou JWT / API métier interne

### Implémentation (exemple minimal)

```txt
- introduire un enum de rôle minimal côté backend (ex. USER | ADMIN)
- propager ce rôle dans la session ou le token d’auth
- créer un décorateur + guard dédiés pour la capacité sensible
- interdire les booléens front, emails hardcodés ou `if` dispersés dans les contrôleurs
```

### Checklist

- Le rôle vit dans la source de vérité backend
- Le rôle est propagé dans le mécanisme d’auth existant
- Les endpoints sensibles passent par un guard dédié
- Aucun contrôle d’accès critique n’est piloté par le front
- Le passage à RBAC reste possible sans casser le contrat existant

---

### Notes importantes

- On préfère 5 patterns solides à 50 “bons conseils”.
- Un pattern = une idée actionnable + son cadre d’utilisation.

---

<a id="pattern-anti-enumeration-auth-email"></a>
## Pattern : Anti-énumération sur endpoints auth liés à un email

- Objectif : empêcher qu’un endpoint auth révèle si un compte existe, n’existe pas ou n’est pas éligible.
- Contexte : reset de mot de passe, invitation, vérification de compte, login ou tout flux qui part d’un email utilisateur.
- Quand l’utiliser : dès qu’une requête auth touche un identifiant de type email.
- Quand l’éviter : jamais sur une surface exposée.
- Avantage :
  - réduit la fuite d’information sur les comptes existants
  - homogénéise les réponses côté client
  - se combine bien avec les garde-fous anti-abus
- Limites / vigilance :
  - ne protège pas seul contre le brute-force, à combiner avec du rate-limiting
  - les logs internes doivent conserver la vraie cause sans l’exposer au client
- Validé le : 16-03-2026
- Contexte technique : Node.js / auth applicative / API HTTP

### Implémentation (exemple minimal)

```txt
- retourner la même réponse HTTP 200 qu’un compte existe ou non
- ne jamais distinguer "email inconnu", "email connu" ou "compte OAuth-only" dans la réponse
- journaliser la cause réelle côté serveur
- ajouter un rate-limiting basé sur email + IP
```

### Checklist

- Réponse client uniforme pour les cas compte connu/inconnu/non éligible
- Aucune fuite d’existence dans le message ou le code d’erreur
- Rate-limiting présent sur les endpoints exposés
- Logs internes exploitables

---

<a id="pattern-token-usage-unique"></a>
## Pattern : Token à usage unique — génération, hash et invalidation atomique

- Objectif : standardiser la création et la consommation de tokens sensibles sans stocker de secret brut en base.
- Contexte : invitation, reset de mot de passe, vérification d’email, lien magique ou tout token one-shot.
- Quand l’utiliser : pour tout token à usage unique transmis à l’utilisateur.
- Quand l’éviter : sessions longues ou secrets devant être relus en clair côté serveur.
- Avantage :
  - réduit l’impact d’une fuite de base
  - garde des tokens URL-safe
  - favorise une consommation atomique et réutilisable
- Limites / vigilance :
  - la consommation doit rester atomique
  - la politique d’expiration doit être explicite
- Validé le : 16-03-2026
- Contexte technique : Node.js `crypto` / Prisma / email ou URL signée

### Implémentation (exemple minimal)

```txt
- générer le token avec `crypto.randomBytes(32).toString("base64url")`
- stocker uniquement le hash SHA-256 du token en base
- transmettre le token brut uniquement via URL ou email
- recalculer le hash côté serveur lors de la consommation
- invalider le token dans une transaction atomique après usage
```

### Checklist

- Token brut jamais persisté en base
- Hash recalculé côté serveur pour la vérification
- Expiration explicite
- Invalidation atomique après consommation

---

<a id="pattern-nextjs-runtime-only-logique-pure-testable"></a>
## Pattern : Next.js runtime-only — orchestration en bord et logique pure testable

- Objectif : préserver la testabilité unitaire et la lisibilité du code serveur Next.js en limitant les dépendances runtime-only aux couches d’orchestration.
- Contexte : applications Next.js avec Server Actions, route handlers, modules email/auth et logique métier testée côté Node.
- Quand l’utiliser : dès qu’un flux serveur mélange APIs Next.js runtime-only (`cookies()`, `headers()`, `redirect()`, `server-only`) et logique métier réutilisable.
- Quand l’éviter : petits modules purement runtime sans logique métier notable, ou fonctions triviales sans intérêt à être testées séparément.
- Avantage :
  - garde la logique métier importable dans un runner Node standard
  - évite que `server-only` contamine des modules purs
  - facilite les tests unitaires sans mocks lourds du runtime Next.js
  - clarifie la responsabilité des Server Actions et handlers serveur
- Limites / vigilance :
  - demande une discipline de découpage
  - peut introduire une indirection inutile si la logique extraite est réellement triviale
  - les frontières d’injection doivent rester simples pour éviter un excès d’abstraction
- Validé le : 19-03-2026
- Contexte technique : Next.js / Server Actions / Node test runner / modules backend injectables

### Implémentation (exemple minimal)

```txt
- réserver `import "server-only"` aux fichiers qui utilisent réellement des APIs runtime Next.js
- garder la Server Action, route handler ou module email comme couche d’orchestration fine
- extraire la logique métier pure dans une fonction ou un service sans dépendance à `cookies()`, `headers()`, `redirect()` ou `server-only`
- injecter explicitement les dépendances utiles (client DB, token, callback de redirect, logger, etc.)
- tester unitairement le module pur dans le runner Node ; tester l’orchestrateur plus légèrement
```

### Checklist

- `server-only` absent des modules de logique pure
- APIs Next.js runtime-only limitées aux couches d’entrée
- Logique métier principale testable sans runtime Next.js
- Dépendances injectées explicitement quand utile
- Server Action ou handler fin et lisible

---

<a id="pattern-guardrails-multi-tenant-403-404"></a>
## Pattern : Guardrails multi-tenant — 403 vs 404 selon la sémantique

- Objectif : éviter les fuites d’information inter-tenant tout en gardant une sémantique d’erreur claire.
- Contexte : API multi-tenant avec ressources métier isolées et surfaces internes ou opérateur.
- Quand l’utiliser : dès qu’une vérification d’appartenance tenant peut soit refuser explicitement l’accès, soit masquer l’existence d’une ressource.
- Quand l’éviter : contexte mono-tenant ou endpoints purement internes sans enjeu de fuite.
- Avantage :
  - clarifie la convention de sécurité
  - évite les réponses incohérentes selon les modules
  - facilite les tests d’isolation tenant
- Limites / vigilance :
  - la convention doit être documentée et appliquée partout
  - un mauvais choix entre 403 et 404 peut révéler une information sensible
- Validé le : 16-03-2026
- Contexte technique : API multi-tenant / HTTP / services métier

### Implémentation (exemple minimal)

```txt
- `assertTenantMatch(actor, expectedTenantId)` -> 403 quand la ressource est connue mais l’accès refusé
- `assertResourceBelongsToTenant(actor, resourceTenantId)` -> 404 quand il faut masquer l’existence d’une ressource d’un autre tenant
- documenter la convention dans le module
- couvrir les deux sémantiques par des tests dédiés
```

### Checklist

- Convention 403 vs 404 documentée
- Helpers distincts selon la sémantique métier
- Aucune fuite d’existence cross-tenant sur les ressources métier
- Tests dédiés sur les deux comportements

---

<a id="pattern-repository-tenant-aware"></a>
## Pattern : Repository tenant-aware — `tenantId` obligatoire dans la signature

- Objectif : rendre impossible par construction une query non scopée sur un domaine multi-tenant.
- Contexte : repositories ou services d’accès aux données sur ressources tenant-scoped.
- Quand l’utiliser : dès qu’un domaine métier est massivement filtré par tenant.
- Quand l’éviter : domaines réellement globaux ou méthodes volontairement cross-tenant.
- Avantage :
  - force le scoping dès la signature TypeScript
  - réduit les oublis de filtre tenant dans les call sites
  - rend les exceptions cross-tenant visibles
- Limites / vigilance :
  - les exceptions cross-tenant doivent être rares et documentées explicitement
  - ne dispense pas d’un second garde-fou dans les mutations sensibles
- Validé le : 16-03-2026
- Contexte technique : TypeScript / Prisma / architecture repository

### Implémentation (exemple minimal)

```txt
- chaque méthode métier tenant-scoped prend `tenantId` en paramètre obligatoire
- les méthodes réellement cross-tenant sont nommées et documentées comme exception
- les call sites Prisma directs sur ces domaines sont interdits ou supprimés
```

### Checklist

- `tenantId` obligatoire sur les méthodes tenant-scoped
- Exceptions cross-tenant documentées
- Appels directs concurrents à Prisma supprimés
- Tests sur scoping tenant au niveau repository

---

<a id="pattern-tenantid-dans-updates"></a>
## Pattern : Défense en profondeur — inclure `tenantId` dans les updates

- Objectif : éviter une mutation cross-tenant même si un identifiant a été mal résolu en amont.
- Contexte : `update` ou `updateMany` sur une ressource tenant-scoped.
- Quand l’utiliser : dès qu’une mutation dépend d’un `id` reçu ou résolu dans un flux multi-tenant.
- Quand l’éviter : ressources globales non liées à un tenant.
- Avantage :
  - ajoute une seconde barrière côté base
  - réduit l’impact d’un call site mal scopé
  - rend la mutation plus sûre sans complexité forte
- Limites / vigilance :
  - ne remplace pas le scoping en lecture ni la vérification d’autorisation
  - suppose que `tenantId` soit disponible au moment de la mutation
- Validé le : 16-03-2026
- Contexte technique : Prisma / multi-tenant / mutations métier

### Implémentation (exemple minimal)

```txt
- préférer `where: { id, tenantId }` à `where: { id }` sur les updates tenant-scoped
- appliquer la même règle sur `updateMany` et opérations de révocation
- conserver les vérifications métier amont, mais ne pas leur déléguer toute la sécurité
```

### Checklist

- `tenantId` présent dans les clauses `where` des updates sensibles
- Pas de mutation tenant-scoped basée sur `id` seul
- Revue explicite des exceptions documentées

---

<a id="pattern-nextjs-server-only-isolation"></a>
## Pattern : Next.js server-only & Server Actions — règles d'isolation

- Objectif : permettre les tests unitaires Node tout en gardant les contraintes runtime Next.js là où elles sont nécessaires.
- Contexte : monorepo Next.js App Router avec logique métier testée en Node runner natif.
- Quand l'utiliser : dès qu'un module mixe logique pure et dépendances runtime Next.js.
- Quand l'éviter : modules purement UI côté client.
- Avantage :
  - logique pure testable sans friction (runner Node natif)
  - Server Action fine et lisible — orchestration uniquement
  - `server-only` explicite et intentionnel, pas par habitude
- Limites / vigilance :
  - ne pas mettre `server-only` dans les repositories purs — casse les tests Node hors Next.js
- Validé le : 16-03-2026
- Contexte technique : Next.js App Router / Node.js test runner

### Règles

```txt
- `server-only` uniquement sur les modules qui appellent des APIs Next.js runtime
  (cookies(), headers(), redirect()) — pas sur les repositories ni la logique pure
- Logique pure extraite dans un module injectable sans `server-only` :
  deleteSession({ prismaClient, sessionToken })
  → testable avec le runner Node sans friction
- Server Action = orchestration mince, elle appelle les modules purs injectés
  et gère les dépendances Next.js runtime uniquement
- Logique de validation / sanitisation (safeHttpUrl, etc.) → module utilitaire séparé,
  sans import nodemailer / server-only
```

### Checklist

- [ ] `server-only` absent des repositories et modules de logique pure
- [ ] Server Action ≤ 10 lignes, délègue au module pur injectable
- [ ] Modules purs couverts par des tests `.spec.ts` Node sans config spéciale
- [ ] La logique pure ne dépend pas du runtime pour être exécutée

---

<a id="pattern-auth-operations-atomiques"></a>
## Pattern : Opérations auth sensibles — atomiques, idempotentes et cohérentes

- Objectif : garantir que les opérations multi-étapes auth (reset, logout, révocation) ne laissent jamais un état incohérent.
- Contexte : tout flux auth qui combine plusieurs writes : hash de mot de passe, invalidation de token, suppression de session.
- Quand l'utiliser : systématiquement sur toute opération qui touche plusieurs tables auth en séquence.
- Quand l'éviter : opérations de lecture pure.
- Avantage :
  - pas de token valide après reset de mot de passe si l'opération est interrompue
  - suppression de session idempotente (P2025 absorbé silencieusement)
  - comportement prévisible même en cas de retry ou de concurrence
- Limites / vigilance :
  - `$transaction` Prisma ne couvre pas les effets de bord réseau (email, cookies) — ces étapes restent hors transaction
- Validé le : 16-03-2026
- Contexte technique : Node.js / Prisma / auth par session ou token

### Implémentation (exemple minimal)

```typescript
// consumePasswordReset — atomique dans une transaction
await prisma.$transaction([
  prisma.passwordResetToken.update({
    where: { tokenHash },
    data: { consumedAt: new Date() },
  }),
  prisma.user.update({
    where: { id: userId },
    data: { passwordHash: newHash },
  }),
  prisma.session.deleteMany({ where: { userId } }),
]);

// Suppression de session — idempotente (P2025 absorbé)
try {
  await prisma.session.delete({ where: { sessionToken } });
} catch (err) {
  if (err?.code !== 'P2025') throw err; // session déjà supprimée → OK
}
```

### Checklist

- [ ] Toute opération hash + update + delete dans une `$transaction`
- [ ] `P2025` absorbé silencieusement sur les suppressions de session
- [ ] Effets de bord hors transaction documentés (cookie, email)
- [ ] Tests couvrant le cas d'une session déjà expirée

---

<a id="pattern-http-200-payload-metier"></a>
## Pattern : Réponse HTTP 200 avec payload métier pour les états d'accès

- Objectif : éviter les codes 4xx pour des états métier normaux qui nécessitent un rendu côté client.
- Contexte : endpoints dont la réponse varie selon les droits ou l'état d'abonnement, sans que l'absence de contenu soit une erreur.
- Quand l'utiliser : paywall, trial read-only, quota soft, état d'accès partiel — quand le client doit décider du rendu.
- Quand l'éviter : accès réellement interdit côté serveur (403), non authentifié (401), endpoint inexistant (404).
- Avantage :
  - pas de gestion d'exception côté client mobile pour des états courants
  - rendu conditionnel (paywall, teaser, empty) piloté par le payload
  - log serveur propre — 4xx réservés aux erreurs techniques/sécurité
- Limites / vigilance :
  - ne pas généraliser aux vraies erreurs de sécurité — 401/403/404 gardent leur sémantique HTTP
- Validé le : 20-03-2026
- Contexte technique : NestJS / Expo React Native — app-alexandrie story 4.1

### Implémentation (exemple minimal)

```typescript
// GET /community/forums
// Sans abonnement → 200 + { data: { forums: [], paywallRequired: true }, meta }
// Avec abonnement → 200 + { data: { forums: [...], paywallRequired: false }, meta }

// ❌ Anti-pattern
return res.status(402).json({ error: { code: 'SUBSCRIPTION_REQUIRED' } });

// ✅ Pattern correct
return res.status(200).json({
  data: { forums: [], paywallRequired: true },
  meta: { total: 0 },
});
```

### Règle

- **4xx** = erreur technique ou de sécurité (401 non authentifié, 403 accès interdit, 404 introuvable)
- **200 + flag métier** = état métier normal que le client doit interpréter pour le rendu

---

<a id="pattern-quota-redis-atomique"></a>
## Pattern : Quota journalier Redis atomique (INCR + EXPIREAT pipeline)

- Objectif : implémenter un quota d'action journalier sans race condition ni clé TTL orpheline.
- Contexte : quota par utilisateur sur une fenêtre calendaire UTC (posts, requêtes, actions sensibles).
- Quand l'utiliser : toute limite d'action journalière avec Redis disponible.
- Quand l'éviter : si Redis est down — prévoir un mode dégradé permissif (voir implémentation).
- Avantage :
  - atomicité garantie : `INCR + EXPIREAT` dans un pipeline `MULTI/EXEC`
  - pas de clé sans TTL même en cas de deux requêtes simultanées (`count === 1` concurrent)
  - mode dégradé explicite si Redis down (`count === null` → permissif)
- Limites / vigilance :
  - compensation `incrBy(-1)` en cas de dépassement — ne couvre pas les crashes entre INCR et la vérification
  - la fenêtre expire à minuit UTC, pas à minuit local
- Validé le : 20-03-2026
- Contexte technique : Redis / NestJS / app-alexandrie story 4.2

### Implémentation (exemple minimal)

```typescript
// RedisService — méthode dédiée
async incrWithExpireAt(key: string, expireAtMs: number): Promise<number | null> {
  const pipeline = this.client.multi();
  pipeline.incr(key);
  pipeline.expireAt(key, Math.floor(expireAtMs / 1000));
  const results = await pipeline.exec();
  return results[0] as number; // valeur post-INCR
}

// Service métier
const today = new Date().toISOString().split('T')[0]; // yyyy-mm-dd UTC
const midnight = new Date(Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1));
const quotaKey = `app:quota:post:${userId}:${today}`;
const count = await redis.incrWithExpireAt(quotaKey, midnight.getTime());

if (count !== null && count > QUOTA_MAX) {
  await redis.incrBy(quotaKey, -1); // compensation
  throw new HttpException({ error: { code: 'QUOTA_EXCEEDED' } }, HttpStatus.TOO_MANY_REQUESTS);
}
// count === null → Redis down → mode dégradé permissif
```

### Checklist

- [ ] Vérifier le quota AVANT la création en DB
- [ ] `INCR + EXPIREAT` dans un pipeline atomique
- [ ] Mode dégradé permissif si `count === null` (Redis down)
- [ ] Clé nommée `{app}:quota:{action}:{userId}:{yyyy-mm-dd}` (date UTC)
- [ ] Anti-pattern évité : `incrBy` + `setEx` séparés (race condition si count === 1 concurrent)

---

<a id="pattern-filtrage-metier-service"></a>
## Pattern : Filtrage des règles métier dans le service, pas dans le repository

- Objectif : séparer la couche d'accès aux données (repository) des règles de visibilité métier (service).
- Contexte : entités publiques avec règles de filtrage (`isVisible`, `isActive`), qui varient selon le contexte appelant (public vs admin).
- Quand l'utiliser : dès qu'une règle de visibilité dépend du contexte d'appel.
- Quand l'éviter : filtres de performance (pagination, tenant scoping) — ceux-là restent dans le `where`.
- Avantage :
  - la règle est testable unitairement sans Prisma (mock de données brutes)
  - la requête DB reste simple et stable entre contextes
  - les cas futurs (ex: admin voit les invisibles) ne nécessitent pas de modifier la requête
- Validé le : 17-03-2026
- Contexte technique : Prisma / Node.js / Next.js — app-template-resto

### Implémentation (exemple minimal)

```typescript
// Repository — charge tout ce qui est candidat
async findCategories(tenantId: string) {
  return prisma.category.findMany({ where: { tenantId } }); // pas de filtre isVisible
}

// Service — applique la règle métier et mappe vers DTO
const raw = await repo.findCategories(tenantId);
return raw.filter(c => c.isVisible).map(toPublicDto);

// Admin : même repo, filtre différent dans le service admin
return raw.map(toAdminDto); // retourne tout, visible ou non
```

---

<a id="pattern-decimal-prisma-serialisation"></a>
## Pattern : Sérialiser les champs `Decimal` Prisma en string au niveau du repository

- Objectif : éviter que les objets `Decimal` Prisma traversent les couches et causent des erreurs de sérialisation JSON silencieuses.
- Contexte : tout champ `Decimal` en Prisma (ex: `price`) retourné via API ou Server Action.
- Quand l'utiliser : systématiquement sur tout champ `Decimal` dans les repositories.
- Risque si ignoré : `Decimal` n'est pas JSON-sérialisable nativement — comportement varie selon Node vs browser vs test runner.
- Validé le : 17-03-2026
- Contexte technique : Prisma / Node.js — app-template-resto

### Implémentation

```typescript
// Repository — convertir avant de retourner
return {
  ...dish,
  price: dish.price?.toString() ?? null, // Decimal → string
};

// DTO public
type DishDto = {
  price: string | null; // pas Decimal
};
```

---

<a id="pattern-helper-tenant-module-partage"></a>
## Pattern : Extraire les helpers de résolution tenant dans un module partagé dédié

- Objectif : éviter les couplages sémantiques incorrects entre domaines en centralisant les utilitaires transverses tenant.
- Contexte : toute fonction de résolution de tenant utilisée par plusieurs domaines métier.
- Quand l'utiliser : dès qu'un helper est importé par plus d'un module métier.
- Risque si ignoré : un module métier devient dépendance implicite d'un autre domaine distinct.
- Validé le : 17-03-2026
- Contexte technique : Next.js / TypeScript — app-template-resto

### Implémentation

```typescript
// ✅ src/server/tenant/resolvePublicTenant.ts
export function resolvePublicTenantSelection(env: NodeJS.ProcessEnv) { ... }

// ✅ Rétrocompatibilité depuis l'ancien emplacement si nécessaire
export { resolvePublicTenantSelection } from "@/server/tenant/resolvePublicTenant";
```

---

<a id="pattern-helper-feature-flag-tenant"></a>
## Pattern : Helper centralisé d'activation de features tenant-scoped

- Objectif : centraliser la logique d'activation/désactivation de pages ou modules par tenant dans un helper pur.
- Contexte : app multi-tenant avec features activables (pages publiques, modules optionnels, intégrations).
- Quand l'utiliser : dès qu'une feature peut être activée/désactivée par tenant.
- Avantage :
  - helper pur et testable sans I/O
  - comportement par défaut sain (`null`/`undefined` → tout activé)
  - composants de navigation et pages importent ce helper, jamais Prisma directement
- Validé le : 17-03-2026
- Contexte technique : Next.js App Router / TypeScript — app-template-resto

### Implémentation

```typescript
// src/server/public/publicPagesConfig.ts
export function isPublicPageEnabled(
  config: PublicPagesConfigRecord | null | undefined,
  pageKey: PublicPageKey
): boolean {
  if (!config) return true; // config absente = tout activé par défaut
  return config[PAGE_KEY_TO_CONFIG_FIELD[pageKey]];
}
```

**Règle :** `null`/`undefined` → tout activé. Évite les régressions si la config n'a pas été provisionnée.

---

<a id="pattern-reutiliser-champ-existant-v1"></a>
## Pattern : Réutiliser un champ existant plutôt que créer un modèle dédié en V1

- Objectif : éviter la sur-ingénierie en V1 en réutilisant un champ existant quand le besoin est simple.
- Contexte : early-stage, besoin de stocker une configuration simple (URL, flag, valeur unique).
- Quand l'utiliser : quand la donnée a le même cycle de vie qu'un modèle existant et ne nécessite pas de relations.
- Quand l'éviter : si la configuration a son propre cycle de vie, des cardinalités multiples, ou des relations distinctes.
- Avantage : zéro migration supplémentaire, zéro scope creep
- Validé le : 17-03-2026
- Contexte technique : Prisma / Node.js — app-template-resto

### Règle

```txt
- Avant de créer un modèle ReservationConfig, vérifier si PublicHomeProfile.reservationUrl suffit
- Un champ optionnel dans le modèle le plus proche est suffisant en V1
- Ne créer un modèle dédié que si : cycle de vie distinct, relations, ou cardinalités multiples
```

---

<a id="pattern-validation-url-externe"></a>
## Pattern : Valider le protocole d'une URL externe avant de la passer à un lien public

- Objectif : prévenir les injections `javascript:` et URLs malformées dans les `<a href>` ou `<img src>` publics.
- Contexte : toute URL venant d'une config tenant, DB ou saisie utilisateur, rendue dans le HTML.
- Quand l'utiliser : systématiquement sur tout champ URL libre stocké en DB et rendu côté HTML.
- Risque si ignoré : injection `javascript:`, URL malformée, potentiel XSS.
- Validé le : 17-03-2026
- Contexte technique : Node.js / Next.js — app-template-resto

### Implémentation

```typescript
function isSafeUrl(url: string): boolean {
  try {
    const { protocol } = new URL(url);
    return protocol === "https:" || protocol === "http:";
  } catch {
    return false;
  }
}

// Validation complète en service/repository
if (mediaUrl) {
  try { new URL(mediaUrl); } catch { throw new HttpError("URL invalide.", { status: 400 }); }
  if (!mediaUrl.startsWith("https://") && !mediaUrl.startsWith("http://"))
    throw new HttpError("URL doit commencer par https://.", { status: 400 });
  if (mediaUrl.length > 500)
    throw new HttpError("URL trop longue.", { status: 400 });
}
// Retourner null si invalide — le composant gère l'absence d'URL
```

### Checklist

- [ ] Validation format (`new URL()`) + protocole + longueur max
- [ ] Retourner `null` si invalide, jamais passer la string brute
- [ ] Composant UI reçoit `string | null`, jamais une string non vérifiée

---

<a id="pattern-utilitaires-purs-module-partage"></a>
## Pattern : Utilitaires purs — extraire dans un module sans `server-only`

- Objectif : permettre aux repositories et aux tests d'importer la même implémentation des utilitaires purs sans friction.
- Contexte : fonctions pures (slugify, formatters, validators) utilisées par des repositories qui ont `server-only`.
- Quand l'utiliser : dès qu'une fonction pure est utilisée dans un repository ET dans des tests.
- Risque si ignoré : logique dupliquée dans les tests qui diverge silencieusement de l'implémentation réelle.
- Validé le : 21-03-2026
- Contexte technique : Node.js / Next.js — app-template-resto

### Implémentation

```
src/server/menuAdmin/
  allergensRepository.ts   ← import { slugify } from "./slugify"
  slugify.ts               ← export function slugify() {} // pas de "server-only"

tests/
  allergens-admin.test.ts  ← import { slugify } from "../src/server/menuAdmin/slugify.ts"
```

---

<a id="pattern-en-enforcement-tenant"></a>
## Pattern : EN enforcement optionnel par tenant (toggle + publish gate)

- Objectif : permettre à un tenant d'activer l'obligation de remplir les champs traduits EN, avec une gate à la publication.
- Contexte : app multi-tenant avec internationalisation optionnelle.
- Quand l'utiliser : dès qu'un tenant peut choisir d'activer/désactiver une exigence de contenu i18n.
- Validé le : 21-03-2026
- Contexte technique : Prisma / Next.js App Router — app-template-resto

### Implémentation

```typescript
// 1. Modèle Tenant
enableEn Boolean @default(false)

// 2. Vérification dans chaque action mutante (create/update)
const { enableEn } = await getEnConfig(tenantId);
if (enableEn && !labelEn) throw new HttpError("Traduction EN requise.", { status: 400 });

// 3. Gate publish — vérification de complétude
const result = await checkEnCompleteness(tenantId); // 4 requêtes en Promise.all
// Exclut : isSystem:true, tenantId:null, isVisible:false
if (!result.complete) throw new HttpError("Contenu EN incomplet.", { status: 422 });
```

**Règles :**
- `isVisible: false` n'est pas inclus dans le check (une entité masquée ne bloque pas la publication)
- `revalidatePath` sur **toutes** les pages menu après toggle du flag (pas seulement `/settings`)

---

<a id="pattern-prisma-migration-manuelle-p3014"></a>
## Pattern : Prisma — Migration manuelle sans shadow DB (P3014)

- Objectif : créer et appliquer une migration Prisma quand la shadow database est interdite (DB managée, permissions restreintes).
- Contexte : DB managées — Supabase, PlanetScale, Railway avec rôle limité, RDS sans superuser.
- Quand l'utiliser : quand `prisma migrate dev` échoue avec `P3014 Prisma Migrate could not create the shadow database`.
- Risque si ignoré : blocage complet de la migration sur env managé.
- Validé le : 23-03-2026
- Contexte technique : Prisma v7+ — app-alexandrie / Supabase

### Implémentation

```bash
# 1. Écrire le SQL manuellement
mkdir -p prisma/migrations/<timestamp>_<nom>
# Créer migration.sql à la main

# 2. Appliquer le SQL directement en DB
npx prisma db execute --file prisma/migrations/<timestamp>_<nom>/migration.sql

# 3. Marquer la migration comme appliquée dans _prisma_migrations
npx prisma migrate resolve --applied <timestamp>_<nom>

# Note Prisma v7 : ne pas utiliser --schema= (option supprimée), utiliser prisma.config.ts
```

**Ne pas utiliser `prisma db push` en production** — il ne versionne pas les migrations.
