Refonte Structure

This commit is contained in:
MaksTinyWorkshop
2026-03-25 08:32:13 +01:00
parent d8a947eb79
commit 9b7af9f1b0
55 changed files with 4743 additions and 4906 deletions

View File

@@ -0,0 +1,18 @@
# Backend — Patterns validés — Index
Patterns backend testés et validés en conditions réelles.
Avant toute proposition backend, identifie le fichier dont le nom et la description matchent le domaine traité, puis lis-le.
---
| Fichier | Domaine | Entrées clés |
|---------|---------|--------------|
| `auth.md` | Auth, sessions, tokens, erreurs API, corrélation | Format erreur standardisé, middleware requestId, anti-énumération, token usage unique, autorisation interne, opérations atomiques |
| `contracts.md` | Contrats API, Zod, error codes, HTTP sémantique | Contracts-First/Zod-Infer/No-DTO, error codes comme contrat, HTTP 200 payload métier |
| `prisma.md` | Prisma, DB, migrations, pagination | Soft delete, pagination cursor, idempotency key, P2002 unique, Decimal sérialisation, migration manuelle P3014, filtrage métier dans service |
| `stripe.md` | Stripe, paiements, webhooks entrants, subscriptions | Provider-Strategy, metadata subscription_data, parsing webhook unique, restauration achats, Trial vs Paid |
| `nestjs.md` | NestJS, guards, Redis, quotas | Guard global APP_GUARD, RedisHealthService cache court, quota INCR+EXPIREAT atomique |
| `multi-tenant.md` | Multi-tenant, isolation, feature flags | 403 vs 404, repository tenant-aware, tenantId dans updates, helper tenant partagé, feature flag tenant, EN enforcement |
| `nextjs.md` | Next.js App Router, Server Actions, isolation | Runtime-only logique pure, server-only isolation, utilitaires purs sans server-only, réutiliser champ V1, validation URL externe |
| `async.md` | Jobs async, webhooks sortants, queues | Exécution asynchrone outbox light, webhooks sortants HMAC + retries idempotents |

View File

@@ -0,0 +1,79 @@
# Backend — Patterns : Async
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
---
<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-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)

View File

@@ -0,0 +1,230 @@
# Backend — Patterns : Auth
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
---
<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-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-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
---
<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

View File

@@ -0,0 +1,138 @@
# Backend — Patterns : Contracts
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
---
<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-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-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

View File

@@ -0,0 +1,188 @@
# Backend — Patterns : Multi-tenant
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
---
<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-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-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`)

View File

@@ -0,0 +1,138 @@
# Backend — Patterns : NestJS
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
---
<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-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-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)

View File

@@ -0,0 +1,166 @@
# Backend — Patterns : Next.js
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
---
<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-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-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-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

View File

@@ -0,0 +1,247 @@
# Backend — Patterns : Prisma
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
---
<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-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-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-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-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-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.
---
<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
```

View File

@@ -0,0 +1,160 @@
# Backend — Patterns : Stripe
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/patterns/README.md` pour l'index complet.
---
<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-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-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``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

View File

@@ -0,0 +1,18 @@
# Backend — Risques & vigilance — Index
Risques backend susceptibles de provoquer des incidents prod, failles de sécurité, bugs non diagnostiquables, ou régressions coûteuses.
Avant toute proposition backend, identifie le fichier dont le nom et la description matchent le domaine traité, puis lis-le.
---
| Fichier | Domaine | Entrées clés |
|---------|---------|--------------|
| `auth.md` | Auth, sessions, guards, accès | AuthN/AuthZ dispersée, guard global manquant, null-check request.user, AdminRoleGuard sans @RequireAdminRole, GET sans contrôle accès, cookie après révocation, mock session sans expiresAt, buildApp partagé e2e |
| `contracts.md` | Contrats, validation, codes erreur | Contrats implicites, erreurs non standardisées, duplication constantes, schema orphelin, code erreur générique 409, ForbiddenException pour validation |
| `prisma.md` | Prisma, DB, transactions, migrations | @unique nullable, TOCTOU transaction, OR tenantId null, nextOrder race condition, tenantId sans FK, schema divergence spec, getter manquant, init module build, clearAllMocks imbriqué, cursor non validé |
| `stripe.md` | Stripe, paiements, webhooks, subscriptions | billing_cycle_anchor vs current_period_end, list() sans has_more, concurrence trial→payant, non-idempotence, 200 pendant processing |
| `nestjs.md` | NestJS, controllers, providers | TooManyRequestsException NestJS 11, controller corrompu insertions, repository dead layer, interface provider incomplète |
| `redis.md` | Redis, cache, quotas, TTL | Thrash connexion sous charge, entitlements TTL > SLA, compteurs in-memory, TTL heure locale ±12h |
| `nextjs.md` | Next.js, build, routing | Prisma init au chargement module, server-only dans repositories, redirect boucle infinie feature flags |
| `general.md` | Observabilité, migrations, performance | Observabilité insuffisante, migrations non reproductibles, upsert N+1 provider |

View File

@@ -0,0 +1,225 @@
# Backend — Risques & vigilance : Auth
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
---
<a id="risque-authn-authz-dispersee"></a>
## AuthN/AuthZ dispersée (contrôles d'accès au fil de l'eau)
### Risques
- Règles de permissions incohérentes selon endpoints
- Failles "oubliées" sur un endpoint secondaire
- Audit impossible
### Symptômes
- Utilisateurs qui accèdent à des ressources non prévues
- Correctifs en urgence "on ajoute un if ici"
- Bugs qui réapparaissent après refactor
### Bonnes pratiques / mitigations
- Centraliser authn/authz (middleware/policies)
- Tests sur règles critiques
- Logs/audit des décisions d'accès
---
<a id="risque-guard-global-manquant"></a>
## Guard global manquant (request.user jamais peuplé)
### Risques
- Chaîne auth bâtie sur une fondation inopérante (tout "a l'air OK" en dev/tests, mais casse en prod)
- Guards aval qui dépendent de `request.user` en erreur (ou contournements involontaires)
- Découvert tard (souvent uniquement en code review ou en prod)
### Symptômes
- `request.user` vaut `undefined` dans un guard supposé "après auth"
- Endpoints qui passent alors qu'ils devraient être refusés (si les guards aval se désactivent/retournent true par défaut)
- Tests "verts" car trop mockés (pas de test e2e qui valide le pipeline complet)
### Bonnes pratiques / mitigations
- Poser explicitement le guard global dès les foundations (au moins `AuthGuard`)
- Vérifier l'ordre des `APP_GUARD` (AuthGuard avant tout guard qui lit `request.user`)
- Ajouter au minimum 1 test d'intégration/e2e qui prouve que `request.user` est bien peuplé sur un endpoint protégé
---
<a id="risque-guard-request-user-null"></a>
## Guard NestJS route-level — null-check manquant sur `request.user`
### Risques
- Un guard route-level qui lit `request.user.userId` sans null-check lève une `TypeError` (500) si `request.user` est absent
- Mauvaise registration de module, test d'intégration mal configuré, ou middleware custom peuvent produire cet état
### Symptômes
- `TypeError: Cannot read properties of undefined (reading 'userId')` en prod
- Tests "verts" car `request.user` mocké globalement, mais pas le guard isolé
### Bonnes pratiques / mitigations
```typescript
const user = (request as any).user as { userId: string } | undefined;
if (!user?.userId) {
throw new UnauthorizedException({ error: { code: 'UNAUTHENTICATED', message: '...' } });
}
```
- **Règle** : les guards route-level ne font pas confiance aux guards globaux pour leurs invariants — ils se défendent eux-mêmes.
- Contexte technique : NestJS v10+ — 09-03-2026
---
<a id="risque-cookie-apres-revocation-db"></a>
## Suppression du cookie après révocation DB sur logout
### Risques
- Si la révocation DB échoue avant la suppression du cookie, l'utilisateur garde un cookie local devenu incohérent
- L'utilisateur peut rester bloqué dans un état où il ne peut plus se déconnecter proprement
- Le comportement diffère selon la disponibilité de la base
### Symptômes
- Logout qui échoue par intermittence quand la DB est instable
- Cookie de session toujours présent côté navigateur après erreur serveur
- Réessais de logout qui produisent des états difficiles à diagnostiquer
### Bonnes pratiques / mitigations
- Toujours supprimer le cookie en premier, même si la révocation DB échoue ensuite
- Traiter la suppression côté DB en best-effort ou avec gestion d'idempotence adaptée
- Vérifier en test qu'un échec DB ne laisse pas l'accès browser actif
- Contexte technique : Next.js / auth par cookie / session persistée — 16-03-2026
---
<a id="risque-get-sans-controle-acces"></a>
## Endpoints GET sans contrôle d'accès sur ressource protégée
### Risques
- Un endpoint de lecture expose des données premium/protégées à tout utilisateur authentifié
- La règle "seuls les writes vérifient les droits" est un anti-pattern qui cause des fuites silencieuses
### Symptômes
- `getCategories`, `getThreads` ou équivalent accessible sans vérification d'entitlements
- Endpoint write protégé par `assertForumAccess` mais GET correspondant non protégé
### Bonnes pratiques / mitigations
- Tout endpoint retournant des données liées à une ressource protégée (forum pack, contenu premium) doit appeler `assertForumAccess` ou équivalent, même pour les GET
- **Checklist review** : pour chaque nouveau GET, vérifier qu'il passe par le guard/helper d'accès si la ressource appartient à un scope protégé
- Contexte technique : NestJS / app-alexandrie — 23-03-2026
---
<a id="risque-adminroleguard-sans-decorateur"></a>
## NestJS `@UseGuards(AdminRoleGuard)` sans `@RequireAdminRole()` — silencieusement ouvert
### Risques
- `AdminRoleGuard.canActivate()` lit la metadata `REQUIRE_ADMIN_ROLE_KEY` posée par `@RequireAdminRole()`
- Si le décorateur est absent, `requiresAdmin = false/undefined` → le guard retourne `true` et laisse passer sans vérification
### Symptômes
- Endpoint admin accessible à tout utilisateur authentifié
- Zéro erreur de compilation ou de démarrage — le bug est silencieux
### Bonnes pratiques / mitigations
```typescript
// ✅ Correct — les deux décorateurs ensemble
@Post('admin/ressource')
@UseGuards(AdminRoleGuard)
@RequireAdminRole()
async createRessource(...) {}
// ❌ Silencieusement non protégé — @RequireAdminRole() manquant
@Post('admin/ressource')
@UseGuards(AdminRoleGuard)
async createRessource(...) {}
```
- Règle : s'applique à tout guard NestJS qui délègue la décision à une metadata de décorateur
- **Checklist review** : vérifier systématiquement les endpoints admin que `@RequireAdminRole()` est présent
- Contexte technique : NestJS / guards metadata — app-alexandrie 23-03-2026
---
<a id="risque-mock-session-sans-expiresat"></a>
## Mock Prisma session sans filtre `expiresAt` — divergence test/prod
### Risques
- Le mock `session.findFirst` omet de filtrer `expiresAt` → des sessions expirées passent en test alors qu'elles seraient rejetées en prod
- Masque des régressions sur la logique d'expiration de session
### Symptômes
- Tests e2e verts avec un token de session expiré
- Bug découvert uniquement en prod quand la TTL est dépassée
### Bonnes pratiques / mitigations
Le mock doit répliquer **tous** les critères de `getUserByToken()` en prod : `revokedAt === null` ET `expiresAt > now` :
```typescript
// ✅ Mock complet fidèle à la prod
findFirst: jest.fn().mockImplementation(({ where }) => {
const session = store[where.accessToken];
if (!session) return null;
if (where.revokedAt === null && session.revokedAt !== null) return null;
if (where.expiresAt?.gt && session.expiresAt <= where.expiresAt.gt) return null;
return session;
})
```
- **Règle** : `seedSession()` doit initialiser `expiresAt` à +30j par défaut. Ajouter un helper `seedExpiredSession()` si des tests de session expirée sont nécessaires.
- Contexte technique : NestJS / Prisma mock / e2e — app-alexandrie 24-03-2026
---
<a id="risque-tests-e2e-buildapp-partage"></a>
## Tests e2e autorisation : scénarios non-abonné avec `buildApp` partagé
### Risques
- Un `describe` e2e avec `buildApp` partagé en `beforeAll` (entitlements actifs) rend impossible le test de scénarios non-abonné sans pollution entre tests
- Tenter de surcharger le mock partagé (`jest.fn().mockResolvedValueOnce(...)`) dans un `it` intermédiaire est fragile et crée des effets de bord
### Symptômes
- Scénario "non-abonné → 403" n'est jamais testé, ou pollue les autres tests si le mock est modifié en cours de describe
### Bonnes pratiques / mitigations
Créer une instance `buildApp` isolée pour les scénarios d'autorisation alternatifs :
```typescript
it('retourne 403 si subscription inactive', async () => {
const isolatedApp = await buildApp({
getEntitlementsForUser: jest.fn().mockResolvedValue({
subscription: { isActive: false, plan: 'free' }
})
});
// ... tests
await isolatedApp.close();
});
```
- **Règle** : ne jamais tenter de surcharger un mock partagé dans un `it` — créer un `buildApp` isolé avec `app.close()` en fin de test
- Contexte technique : NestJS / Jest e2e — app-alexandrie 24-03-2026

View File

@@ -0,0 +1,165 @@
# Backend — Risques & vigilance : Contracts
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
---
<a id="risque-contrats-api-implicites"></a>
## Contrats API implicites (validation faible ou absente)
### Risques
- Entrées non validées → erreurs bizarres / vulnérabilités
- Changements qui cassent le front et les intégrations
### Symptômes
- 500 sur erreurs utilisateur
- Incohérences de format de réponse
- "Ça marche en staging, pas en prod" (données réelles)
### Bonnes pratiques / mitigations
- Schémas (OpenAPI/JSON Schema) + validation serveur
- Formats de réponse cohérents
- Versionner/éviter breaking changes
---
<a id="risque-erreurs-non-standardisees"></a>
## Erreurs non standardisées (4xx/5xx incohérents)
### Risques
- Front et automatisations impossibles à rendre robustes
- Debug long (pas de codes internes, pas de corrélation)
### Symptômes
- Clients qui "retry" sur des 4xx
- Messages techniques exposés aux utilisateurs
- Logs inexploitables
### Bonnes pratiques / mitigations
- Mapping HTTP standard + format d'erreur stable
- Codes internes d'erreurs applicatives
- requestId/traceId partout
---
<a id="risque-duplication-constantes-contracts"></a>
## Duplication silencieuse de constantes partagées (contracts) via fichier orphelin
### Risques
- Deux sources de vérité qui divergent silencieusement (ex : topics officiels, enums métier, slugs)
- Bug non détecté par TypeScript si la duplication est dans un fichier non importé (code mort)
### Symptômes
- Incohérences entre API et client sur des listes/enums "censées être partagées"
- "Ça marche chez moi" selon l'endroit où la constante est importée
- Un fichier de config existe dans `apps/*` mais n'est jamais importé/greffé au runtime
### Bonnes pratiques / mitigations
- Toute constante partagée vit dans `packages/contracts/src/` et est importée depuis là (jamais recopiée dans `apps/*`)
- En review : repérer les fichiers "config/constants" ajoutés dans `apps/*` sur des domaines déjà couverts par `contracts`
- (Optionnel) Outillage : intégrer une étape de détection de code mort / exports inutilisés au CI si ça devient récurrent
---
<a id="risque-contracts-schema-orphelin"></a>
## Contracts : schema orphelin / type de retour désynchronisé
### Risques
- Un `RequestSchema` défini dans `packages/contracts` mais jamais importé dans le controller ni le service mobile → dead code silencieux qui crée une fausse confiance
- Un type de retour inline (`string` brut) à la place du type contracts → désynchronisation silencieuse entre contrat et implémentation
### Symptômes
- `grep` du nom du schema ne trouve aucun `import` en dehors de sa définition
- Service retourne `Promise<{ status: string }>` au lieu de `Promise<CurationResponse>` — le `status` n'est pas validé comme `CurationStatus`
- Endpoints `POST /action` sans body ayant un schema `{ pathParam: string }` — le param vient du path, pas du body
### Bonnes pratiques / mitigations
À chaque story qui ajoute des schemas dans `packages/contracts`, vérifier en review :
1. Chaque `RequestSchema` est utilisé dans un `ZodValidationPipe` (API) ou importé dans le service mobile.
2. Les `ResponseSchema` correspondent au type de retour typé du service (`Promise<TheType>`, pas un type inline).
3. Les endpoints sans body (`POST /action`) définissent `z.object({})` ou omettent le body schema — ne jamais placer les path params dans le body schema.
```typescript
// ❌ Anti-pattern — type inline, status non typé
async showcaseThread(...): Promise<{ threadId: string; status: string }> { ... }
// ✅ Pattern correct — type contracts importé
import type { CurationResponse } from '@app-alexandrie/contracts';
async showcaseThread(...): Promise<CurationResponse> { ... }
```
- Contexte technique : NestJS / Zod / contracts-first — app-alexandrie 23-03-2026
---
<a id="risque-code-erreur-generique-409"></a>
## Code d'erreur générique sur statut HTTP sémantique (409 CONFLICT)
### Risques
- Utiliser `VALIDATION_ERROR` ou `INTERNAL_ERROR` sur un 409 rend les erreurs indistinguables côté client et monitoring
- Les clients (mobile, monitoring, tests) ne peuvent pas brancher une logique conditionnelle sans un code sémantique
### Symptômes
- Tous les conflits métier remontent le même code → impossible de distinguer "alias déjà résolu" de "handle déjà pris"
- Tests forcés à matcher le message texte au lieu du code → fragiles
### Bonnes pratiques / mitigations
Chaque scénario métier distinct doit avoir son propre code dans `error-code.ts` :
```typescript
// ❌ Anti-pattern — code générique sur 409
throw new ConflictException({ error: { code: 'VALIDATION_ERROR', message: '...' } });
// ✅ Correct — code sémantique spécifique
throw new ConflictException({ error: { code: 'ALIAS_ALREADY_RESOLVED', message: '...' } });
throw new ConflictException({ error: { code: 'HANDLE_ALREADY_TAKEN', message: '...' } });
```
- **Règle** : 1 scénario métier distinct = 1 code d'erreur distinct
- **Checklist review** : tout 409/422 doit avoir un code dans `error-code.ts`, jamais `VALIDATION_ERROR` ou `INTERNAL_ERROR`
- Contexte technique : NestJS / error-code.ts — app-alexandrie 24-03-2026
---
<a id="risque-forbidden-pour-validation"></a>
## `ForbiddenException` (403) utilisé pour des erreurs de validation
### Risques
- Les clients qui filtrent par HTTP 400 manquent les erreurs de validation lancées en 403
- Sémantique API incorrecte → comportements clients imprévisibles
### Symptômes
- `ForbiddenException` lancée pour des tags invalides, des formats incorrects, des liens HTTP
- Clients API qui ignorent ces erreurs ou les traitent comme des refus d'accès
### Bonnes pratiques / mitigations
Tableau de correspondance :
| Cas | Exception correcte | Code HTTP |
|---|---|---|
| Tags invalides, contenu trop long, format incorrect | `BadRequestException` | 400 |
| Accès refusé explicitement (accès forum, trial read-only) | `ForbiddenException` | 403 |
| Quota dépassé | `HttpException(429)` via `HttpStatus.TOO_MANY_REQUESTS` | 429 |
- **Règle** : HTTP 403 = "tu n'as pas le droit d'effectuer cette action". HTTP 400 = "ta requête est mal formée".
- Contexte technique : NestJS / HTTP — 20-03-2026

View File

@@ -0,0 +1,72 @@
# Backend — Risques & vigilance : Général
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
---
<a id="risque-observabilite-insuffisante"></a>
## Observabilité insuffisante (logs non structurés, pas de corrélation)
### Risques
- MTTR très élevé : on devine
- Incapacité à mesurer l'impact utilisateur
### Symptômes
- Logs "ça a crash" sans contexte
- Impossible de relier une requête à une erreur
- Latence qui dérive sans alerte
### Bonnes pratiques / mitigations
- Logs structurés + requestId/traceId
- Métriques de base (latence, erreurs, throughput)
- Alertes simples sur 5xx/latence
---
<a id="risque-migrations-risquees"></a>
## Migrations risquées / non reproductibles
### Risques
- Downtime
- Perte de données
- Incohérence entre environnements
### Symptômes
- "Ça marche en local" mais pas en prod
- Migration qui échoue à mi-chemin
- Rollback impossible
### Bonnes pratiques / mitigations
- Migrations versionnées + tests staging
- Stratégie expand/contract si besoin
- Plan de rollback/mitigation
---
<a id="risque-upsert-n-plus-un-provider"></a>
## Boucle `upsert` N+1 sur synchronisation provider
### Risques
- Latence multipliée par le nombre d'items
- Charge DB inutile
- Timeouts ou contention sur gros volumes
### Symptômes
- Une boucle applicative exécute un `upsert` par item
- Temps de traitement qui explose avec le volume
- Logs SQL répétitifs et séquentiels
### Bonnes pratiques / mitigations
- Batcher quand c'est possible
- Précharger les données nécessaires avant boucle
- Mesurer explicitement le coût d'un `upsert` unitaire dans les flux de sync
- Contexte technique : Prisma / synchronisation provider — 10-03-2026

View File

@@ -0,0 +1,102 @@
# Backend — Risques & vigilance : NestJS
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
---
<a id="risque-nestjs-toomanyrequest"></a>
## NestJS 11 — `TooManyRequestsException` inexistante
### Risques
- `TooManyRequestsException` n'est pas exportée par `@nestjs/common` en NestJS ≥ 11
- Erreur de compilation ou 500 si utilisée directement
### Symptômes
- `Cannot find name 'TooManyRequestsException'` à la compilation
- Test qui passe sur NestJS 10 mais échoue sur 11+
### Bonnes pratiques / mitigations
```typescript
// Pattern sûr pour HTTP 429
throw new HttpException(
{ error: { code: 'QUOTA_EXCEEDED', message: '...' } },
HttpStatus.TOO_MANY_REQUESTS,
);
```
- Contexte technique : NestJS v11+ — 20-03-2026
---
<a id="risque-controller-corrompu-insertions"></a>
## Controller NestJS corrompu par insertions multiples
### Risques
- Des méthodes imbriquées, décorateurs orphelins ou routes dupliquées cassent la syntaxe TypeScript sans que le compilateur ne l'attrape toujours
- La story est marquée "completed" alors que le code ne compile pas
### Symptômes
- `@Get('/route')` apparaît dans le corps d'une autre méthode
- La même route est déclarée 2-3 fois dans le même controller
- Erreur NestJS au runtime mais pas à la compilation
### Bonnes pratiques / mitigations
- Quand on ajoute >3 endpoints à un controller existant, réécrire le fichier entier en partant du fichier original
- Ne jamais insérer par blocs séparés — la concaténation casse la structure AST
- **Checklist review** : grep `@Get\|@Post\|@Patch\|@Delete` dans le controller et vérifier qu'aucune route n'est dupliquée
- Contexte technique : NestJS / TypeScript — app-alexandrie 20-03-2026
---
<a id="risque-repository-dead-layer"></a>
## Repository layer non branché (dead layer)
### Risques
- Donner une impression de sécurité alors que le code métier continue d'appeler l'ORM directement
- Multiplier les chemins d'accès aux données avec des règles différentes
- Payer le coût d'une abstraction qui n'a aucun effet réel
### Symptômes
- Un repository est créé mais les anciens call sites Prisma restent en place
- Les nouvelles règles de scoping ou de sécurité ne s'appliquent pas partout
- La review montre des fichiers de repository peu ou jamais importés
### Bonnes pratiques / mitigations
- Vérifier qu'une nouvelle couche d'abstraction est réellement branchée dans les call sites existants
- Rechercher explicitement les appels directs restants lors de la review
- Refuser l'introduction d'une couche repository tant que la migration effective n'est pas faite
- Contexte technique : TypeScript / Prisma / refactor d'accès aux données — 16-03-2026
---
<a id="risque-interface-provider-incomplete"></a>
## Interface provider incomplète ou divergente de ses implémentations
### Risques
- Une implémentation expose des méthodes non déclarées dans le contrat commun
- Les appelants contournent l'interface et se couplent à un provider concret
- Une stratégie provider devient non interchangeable en pratique
### Symptômes
- Appels avec cast ou accès direct à une implémentation spécifique
- Méthodes présentes dans une classe mais absentes de l'interface
- Régression lors d'un changement de provider
### Bonnes pratiques / mitigations
- Toute capacité commune attendue par les appelants doit être déclarée dans l'interface
- Interdire les méthodes "cachées" consommées hors contrat
- Tester au moins une implémentation par le contrat abstrait
- Contexte technique : TypeScript / provider strategy — 10-03-2026

View File

@@ -0,0 +1,75 @@
# Backend — Risques & vigilance : Next.js
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
---
<a id="risque-prisma-init-module-build"></a>
## Prisma initialisé au chargement de module — casse le build Next.js
### Risques
- Un import global qui initialise Prisma immédiatement peut faire échouer la collecte de pages/routes au build si `DATABASE_URL` n'est pas disponible dans l'environnement de build
### Symptômes
- `PrismaClientInitializationError` ou `Error: Environment variable not found: DATABASE_URL` au `next build`
- L'app tourne en dev mais le build CI échoue
### Bonnes pratiques / mitigations
- Préférer une initialisation lazy-safe : retarder l'accès DB au moment de l'appel métier
- Retourner un proxy qui lève une erreur claire uniquement lors du premier accès réel à la DB
- Ne jamais instancier `new PrismaClient()` au top-level d'un module importé par Next.js
- Contexte technique : Next.js App Router / Prisma — app-template-resto 16-03-2026
---
<a id="risque-server-only-repositories-tests"></a>
## `server-only` dans les repositories — bloque les tests unitaires
### Risques
- `import "server-only"` empêche l'exécution des fichiers hors runtime Next.js
- Les tests Node.js échouent avec `Error: This module cannot be imported from a Client Component module`
### Symptômes
- Tests qui passent via le dev server mais échouent via `jest` en mode node
- Erreur au `require()` d'un repository depuis un test unitaire
### Bonnes pratiques / mitigations
- Ne mettre `server-only` que dans les fichiers qui utilisent des APIs Next.js runtime (`cookies()`, `headers()`, `redirect()`)
- **Ne pas** mettre `server-only` dans les repositories purs (qui n'appellent que Prisma)
- Alternative de secours : créer un stub `node_modules/server-only/index.js` no-op pour les tests
- Contexte technique : Next.js App Router / Jest — app-template-resto 16-03-2026
---
<a id="risque-redirect-boucle-infinie"></a>
## Redirect vers la page désactivée elle-même (boucle infinie feature flags)
### Risques
- Une page désactivée redirige vers elle-même via le fallback — boucle infinie silencieuse absorbée par Next.js mais UX cassée
### Symptômes
- Page `/` désactivée → redirect vers `buildLocalizedPath("home")` = `/` → boucle
- Next.js absorbe la boucle mais l'utilisateur voit un écran bloqué ou vide
### Bonnes pratiques / mitigations
```typescript
// Si la page est sa propre destination de fallback, ne pas rediriger
if (pageKey === "home") return null; // évite redirect home → home
return buildLocalizedPath(locale, "home");
```
- Règle : dans tout mécanisme de redirection sur page désactivée, toujours vérifier que `pageKey !== fallbackKey`
- Retourner `null` (accès non bloqué) plutôt que de boucler
- Contexte technique : Next.js App Router / feature flags tenant — app-template-resto 17-03-2026

View File

@@ -0,0 +1,306 @@
# Backend — Risques & vigilance : Prisma
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
---
<a id="risque-prisma-unique-nullable"></a>
## PostgreSQL / Prisma : `@unique` sur champ nullable (idempotence cassée)
### Risques
- Doublons en base malgré un "unique" attendu (PostgreSQL autorise plusieurs `NULL` dans un index UNIQUE)
- Upserts non idempotents si la clé peut être `null` (`where: { externalId: null }` crée plusieurs lignes)
### Symptômes
- Plusieurs enregistrements "équivalents" avec `externalId = NULL`
- Rejouer un webhook / retry réseau crée une nouvelle ligne au lieu d'upsert
### Bonnes pratiques / mitigations
- Toute clé utilisée dans un `where` d'`upsert` doit être **non-nullable**
- Si un identifiant externe peut légitimement être `null`, ne pas l'utiliser comme clé d'idempotence : choisir une autre clé unique non-nullable
---
<a id="risque-prisma-transaction-toctou-tenantid"></a>
## Prisma `$transaction` : fenêtres TOCTOU (check hors transaction)
### Risques
- Un pre-check + une `$transaction` avec un `update` non sécurisé crée une fenêtre TOCTOU
- Deux appels concurrents peuvent tous deux passer le check et agir simultanément
- En multi-tenant : un bug upstream peut permettre une écriture cross-tenant malgré le guard applicatif
### Symptômes
- Double action sur un état booléen (ex : double mise en vitrine) si le check n'est pas dans la transaction
- Écriture sur une ressource d'un autre tenant possible en race condition
### Bonnes pratiques / mitigations
**Cas 1 — Multi-tenant : inclure `tenantId` dans chaque écriture**
```typescript
// ❌ Anti-pattern — check OK mais écriture sans tenantId
const existing = await prisma.item.findMany({ where: { id: { in: ids }, tenantId } });
await prisma.$transaction(
ids.map((id, idx) => prisma.item.update({ where: { id }, data: { sortOrder: idx + 1 } }))
);
// ✅ Défense en profondeur — tenantId dans chaque écriture
await prisma.$transaction(
ids.map((id, idx) => prisma.item.updateMany({ where: { id, tenantId }, data: { sortOrder: idx + 1 } }))
);
```
- Règle : toute écriture Prisma sur une ressource tenant-aware doit inclure `tenantId` dans le WHERE, même dans une transaction précédée d'un check
- Utiliser `updateMany`/`deleteMany` pour inclure `tenantId` sans exception si 0 lignes
**Cas 2 — Idempotence / plafond : re-check d'état à l'intérieur de la transaction**
```typescript
// ❌ Anti-pattern : check d'état hors transaction
if (resource.isActive) throw ...;
await prisma.$transaction(async (tx) => {
// resource.isActive a pu changer entre-temps
return tx.resource.update(...);
});
// ✅ Pattern correct : check ET update dans la transaction
await prisma.$transaction(async (tx) => {
const current = await tx.resource.findUnique({ where: { id } });
if (current?.isActive) throw ...; // re-check atomique
const count = await tx.resource.count(...);
if (count >= LIMIT) throw ...;
return tx.resource.update(...);
});
```
- Règle : tout guard métier de type "déjà fait / plafond atteint" doit être vérifié à l'intérieur de la transaction, pas avant
- Contexte technique : Prisma / multi-tenant — app-template-resto 21-03-2026 ; NestJS / Prisma — app-alexandrie 23-03-2026
---
<a id="risque-prisma-or-tenantid-null"></a>
## Prisma OR multi-tenant : `tenantId: null` manquant sur la branche système
### Risques
- Sur un modèle à `tenantId` nullable distinguant ressources "système" et "tenant", un filtre `{ isSystem: true }` sans `tenantId: null` expose des ressources corrompues à tous les tenants
### Symptômes
- Un tag `isSystem: true` avec `tenantId` non-null est exposé à tous les tenants
- Bug de sécurité difficile à détecter car le comportement nominal semble correct
### Bonnes pratiques / mitigations
```typescript
// ❌ Trop permissif
OR: [{ isSystem: true }, { tenantId, isSystem: false }]
// ✅ Défense en profondeur — double condition sur la branche système
OR: [{ isSystem: true, tenantId: null }, { tenantId, isSystem: false }]
```
- Règle : sur tout modèle `tenantId?` (nullable) + flag `isSystem`/`isGlobal`/`isPublic`, la branche "ressource publique" du filtre OR doit toujours inclure `tenantId: null`
- Contexte technique : Prisma / multi-tenant — app-template-resto 21-03-2026
---
<a id="risque-nextorder-hors-transaction"></a>
## Calcul de `nextOrder` hors transaction (race condition `sortOrder`)
### Risques
- Deux requêtes concurrentes obtiennent le même `MAX(sortOrder)` et créent deux entités avec le même `sortOrder`
### Symptômes
- Deux items avec le même `sortOrder` dans la même catégorie/scope
- Bug aléatoire selon la charge — invisible en dev, présent en prod
### Bonnes pratiques / mitigations
```typescript
// ✅ Calcul dans la transaction interactive
return prisma.$transaction(async (tx) => {
const maxOrder = await tx.entity.aggregate({
where: { tenantId, scopeId },
_max: { sortOrder: true },
});
const nextOrder = (maxOrder._max.sortOrder ?? 0) + 1;
return tx.entity.create({ data: { ..., sortOrder: nextOrder } });
});
```
- Règle : ne jamais calculer `maxOrder` hors de la transaction qui crée l'entité
- Contexte technique : Prisma / transactions — app-template-resto 21-03-2026
---
<a id="risque-tenantid-sans-fk-relation"></a>
## Champ `tenantId` sans FK ni relation Prisma vers `Tenant`
### Risques
- Un `tenantId TEXT NOT NULL` sans relation Prisma ne génère aucune FK en DB
- L'isolation multi-tenant n'est pas enforced au niveau base de données
### Symptômes
- Migration SQL sans `ALTER TABLE ... ADD CONSTRAINT ... REFERENCES "tenants"`
- Prisma ne génère pas de FK automatiquement sans `@relation` déclarée
### Bonnes pratiques / mitigations
Tout modèle tenant-scoped doit avoir les trois :
1. `tenant Tenant @relation(fields: [tenantId], references: [id], onDelete: Cascade)` dans le modèle Prisma
2. La relation inverse dans `Tenant` (ex: `menuCategories MenuCategory[]`)
3. La FK correspondante dans la migration SQL
- **Checklist review** : vérifier systématiquement que les nouveaux modèles respectent ce guardrail
- Contexte technique : Prisma / multi-tenant — app-template-resto 17-03-2026
---
<a id="risque-schema-divergence-spec-story"></a>
## Divergence schéma Prisma / spec story (champ déclaré ✅ mais absent)
### Risques
- Une tâche de story cochée ✅ implique un champ (ex: `consumedAt`, `tokenHash`) qui n'existe pas dans `schema.prisma`
- Le code compile ou passe en review sans que le champ soit réellement présent en DB
### Symptômes
- Erreur à l'exécution sur un champ inexistant malgré une story marquée "done"
- `schema.prisma` ne contient pas le champ mentionné dans les tâches
### Bonnes pratiques / mitigations
- Avant de marquer une tâche ✅, croiser avec `schema.prisma` pour confirmer que le champ existe réellement
- Une story peut décrire un champ comme stratégie de conception sans l'avoir intégré — toujours vérifier
- Contexte technique : Prisma / app-template-resto — 16-03-2026
---
<a id="risque-prismaservice-getter-manquant"></a>
## PrismaService — getter explicite manquant sur nouveau modèle
### Risques
- L'ajout d'un modèle dans `schema.prisma` sans son getter dans `PrismaService` casse le typecheck
- Erreur silencieuse si les modules sont peu typés
### Symptômes
- `Property 'forum' does not exist on type 'PrismaService'` à la compilation
- Module fonctionnel sur le `PrismaClient` direct mais cassé via `PrismaService`
### Bonnes pratiques / mitigations
Tout ajout de modèle Prisma = **deux actions** :
1. Ajouter le modèle dans `schema.prisma`
2. Ajouter le getter dans `prisma.service.ts`
```typescript
// apps/api/src/infra/prisma/prisma.service.ts
get forum() {
return this.client.forum;
}
```
- **Checklist review** : à chaque nouvelle migration Prisma, vérifier que `prisma.service.ts` est mis à jour.
- Contexte technique : NestJS / PrismaService encapsulé — app-alexandrie 20-03-2026
---
<a id="risque-prisma-init-module-build"></a>
## Prisma initialisé au chargement de module — casse le build Next.js
### Risques
- Un import global qui initialise Prisma immédiatement peut faire échouer la collecte de pages/routes au build si `DATABASE_URL` n'est pas disponible dans l'environnement de build
### Symptômes
- `PrismaClientInitializationError` ou `Error: Environment variable not found: DATABASE_URL` au `next build`
- L'app tourne en dev mais le build CI échoue
### Bonnes pratiques / mitigations
- Préférer une initialisation lazy-safe : retarder l'accès DB au moment de l'appel métier
- Retourner un proxy qui lève une erreur claire uniquement lors du premier accès réel à la DB
- Ne jamais instancier `new PrismaClient()` au top-level d'un module importé par Next.js
- Contexte technique : Next.js App Router / Prisma — app-template-resto 16-03-2026
---
<a id="risque-jest-clearallmocks-imbrique"></a>
## `jest.clearAllMocks()` dans des `beforeEach` imbriqués avec mocks Prisma
### Risques
- Remise à zéro d'un setup attendu par un scope de test plus profond
- Tests verts ou rouges pour de mauvaises raisons
- Forte difficulté à comprendre l'état réel des mocks
### Symptômes
- Comportement différent selon l'ordre ou le niveau d'imbrication des `describe`
- Mocks Prisma "perdus" entre deux tests
- Corrections locales qui cassent d'autres blocs de tests
### Bonnes pratiques / mitigations
- Centraliser la stratégie de reset des mocks
- Éviter les `clearAllMocks()` concurrents à plusieurs niveaux de nesting
- Préférer un setup explicite et local par scénario quand les mocks Prisma sont structurants
- Contexte technique : Jest / Prisma / tests NestJS — 10-03-2026
---
<a id="risque-cursor-pagination-opaque"></a>
## Cursor de pagination opaque — validation manquante (500 au lieu de 400)
### Risques
- Un cursor base64url+JSON non validé crash en HTTP 500 si malformé ou corrompu
- Exposé à des attaques par input malveillant sur les endpoints paginés publics ou semi-publics
### Symptômes
- `JSON.parse` ou décodage base64 lève une exception non catchée → 500 en prod
- Les logs montrent une stack trace sur un endpoint paginé avec un cursor externe
### Bonnes pratiques / mitigations
```typescript
// ❌ DANGEREUX — crash 500 si cursor corrompu
const decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString());
// ✅ CORRECT — validation avec code d'erreur sémantique
let decoded = null;
if (cursor) {
try {
decoded = JSON.parse(Buffer.from(cursor, 'base64url').toString());
if (!decoded.createdAt || !decoded.id) throw new Error('Champs manquants');
} catch {
throw new BadRequestException({ error: { code: 'INVALID_CURSOR', message: 'Cursor de pagination invalide.' } });
}
}
```
- **Règle** : ajouter un test unitaire "cursor invalide → 400" sur tout endpoint paginé par cursor
- Contexte technique : NestJS / pagination — app-alexandrie 24-03-2026

View File

@@ -0,0 +1,105 @@
# Backend — Risques & vigilance : Redis
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
---
<a id="risque-redis-thrash-connexion"></a>
## Redis — thrash de connexion sous charge
### Risques
- Connexions concurrentes multiples si `connect()` est appelé "à la demande" sans lock
- Spam logs + saturation connexions quand Redis est down ou lent
### Symptômes
- N appels simultanés → N tentatives de connexion en parallèle
- Logs "Redis connection failed" en rafale au démarrage ou lors d'un restart Redis
### Bonnes pratiques / mitigations
```typescript
// Pattern single-flight + cooldown + fallback DB best-effort
if (!this.connectPromise) {
this.connectPromise = this.client.connect().finally(() => { this.connectPromise = null; });
}
await this.connectPromise;
// Si échec → nextConnectRetryAtMs = now + 1000 → return false → fallback DB
```
- Contexte technique : Redis / NestJS — 09-03-2026
---
<a id="risque-entitlements-ttl-sla"></a>
## Entitlements — TTL cache supérieur au SLA de propagation
### Risques
- TTL cache > SLA propagation → un webhook raté viole mécaniquement le SLA (accès stale plus long que garanti)
- Utilisateur avec accès périmé ou sans accès dû, pendant toute la durée du TTL résiduel
### Symptômes
- Accès premium encore actif après annulation (ou inversement)
- NFR "propagation ≤ 60s" non respecté en cas de webhook manqué
### Bonnes pratiques / mitigations
- TTL cache ≤ SLA cible (ex : NFR "≤ 60s" → TTL = 60s max)
- Toujours coupler TTL + invalidation explicite via webhook (les deux, pas l'un ou l'autre)
- Contexte technique : Redis / entitlements / NestJS — 09-03-2026
---
<a id="risque-compteurs-inmemory"></a>
## Compteurs in-memory ≠ métriques persistées
### Risques
- Compteurs in-memory remis à zéro au restart (perte de données)
- Non agrégables sur plusieurs instances (données partielles par pod)
### Symptômes
- Métriques qui "repartent de 0" à chaque déploiement
- Dashboards incorrects en environnement multi-instance
### Bonnes pratiques / mitigations
- V1 low-cost : `Redis INCRBY` best-effort par `eventType` → persisté et agrégé multi-instances
- Évolutif vers Prometheus/OTel sans changer l'interface (abstraction dès le départ)
- Contexte technique : Redis / NestJS — 09-03-2026
---
<a id="risque-ttl-redis-heure-locale"></a>
## TTL Redis quota calculé en heure locale (dérive jusqu'à ±12h)
### Risques
- Le reset du quota journalier dérive selon le timezone du serveur, pouvant aller jusqu'à ±12h d'écart par rapport à minuit UTC
### Symptômes
- Quota qui se remet à zéro à des heures inattendues selon l'environnement de déploiement
- Comportement différent en dev local (TZ machine) et en prod (TZ container)
### Bonnes pratiques / mitigations
```typescript
// ✅ CORRECT — UTC midnight garanti
const midnight = new Date(
Date.UTC(now.getUTCFullYear(), now.getUTCMonth(), now.getUTCDate() + 1),
);
const ttlMs = midnight.getTime() - now.getTime();
// ❌ RISQUÉ — heure locale du serveur
const endOfDay = new Date();
endOfDay.setHours(23, 59, 59, 999); // dérive selon TZ serveur
```
- Règle : tout `expireAt` ou `TTL` de quota journalier doit utiliser `Date.UTC()` — vérifier systématiquement en review
- Contexte technique : Redis / NestJS — app-alexandrie 20-03-2026

View File

@@ -0,0 +1,116 @@
# Backend — Risques & vigilance : Stripe
> Extrait de la base de connaissance Lead_tech. Voir `knowledge/backend/risques/README.md` pour l'index complet.
---
<a id="risque-stripe-current-period-end"></a>
## Stripe (v17+) : confusion `billing_cycle_anchor` vs `current_period_end`
### Risques
- Stocker une date de fin de période incorrecte en DB (bug silencieux)
- État d'abonnement incohérent (UI, relances, accès premium)
### Symptômes
- `currentPeriodEnd` correspond à une date "bizarre" (souvent proche de la création), ou à un jour du mois
- Des accès premium expirent trop tôt / trop tard
### Bonnes pratiques / mitigations
- Ne jamais interpréter `billing_cycle_anchor` comme une date de fin de période
- Utiliser `subscription.current_period_end` (timestamp) pour la fin de période courante
- Ajouter un test sur un événement webhook/Subscription qui vérifie la date persistée
---
<a id="risque-stripe-list-has-more"></a>
## Stripe `list()` sans gestion de `has_more`
### Risques
- Pagination tronquée silencieusement
- Réconciliation incomplète d'abonnements, achats ou moyens de paiement
- Décisions métier prises sur un jeu de données partiel
### Symptômes
- Comportement correct sur petits comptes mais faux sur comptes plus chargés
- Premiers éléments traités, les suivants ignorés
- Absence de boucle de pagination ou d'auto-pagination
### Bonnes pratiques / mitigations
- Traiter explicitement `has_more`
- Utiliser l'auto-pagination Stripe si adaptée
- Tester au moins un cas avec plusieurs pages de résultats
- Contexte technique : Stripe API — 10-03-2026
---
<a id="risque-trial-payant-concurrence"></a>
## Concurrence entre activation locale et webhook sur transition trial → payant
### Risques
- Double création ou double attachement d'une ressource unique
- Conflit `P2002`
- État local différent de l'état Stripe pendant la transition
### Symptômes
- La transition fonctionne parfois, puis échoue aléatoirement
- Un webhook Stripe et une action applicative écrivent la même mutation métier
- Erreurs d'unicité lors de l'activation payante
### Bonnes pratiques / mitigations
- Définir une seule source autorisée pour chaque transition d'état
- Rendre les écritures idempotentes
- Sérialiser ou réconcilier explicitement les transitions pilotées à la fois par action utilisateur et webhook
- Contexte technique : Stripe / Prisma / trial subscription — 10-03-2026
---
<a id="risque-non-idempotence"></a>
## Non-idempotence sur opérations sensibles
### Risques
- Doubles paiements / doubles créations
- Webhooks rejoués qui cassent l'état
### Symptômes
- Doublons de lignes en DB
- Actions exécutées 2 fois après timeout/retry
- Incidents difficiles à reproduire
### Bonnes pratiques / mitigations
- Idempotency key sur endpoints critiques
- Protection anti-doublon côté DB (contraintes uniques)
- Comportement défini en cas de retry
---
<a id="risque-webhook-200-processing"></a>
## Webhooks entrants — répondre 200 pendant `processing` (event perdu)
### Risques
- Le provider (Stripe, etc.) arrête ses retries après un 2xx, même si le premier worker a échoué
- Event non appliqué mais marqué "traité" → état incohérent silencieux
### Symptômes
- Webhook reçu, 200 retourné, mais l'état en base n'est pas mis à jour
- Aucun retry du provider → impossible à détecter sans monitoring actif
### Bonnes pratiques / mitigations
- Lock DB (`WebhookEvent`) avec machine d'état : `pending``processing``processed` / `failed`
- Si `processing` détecté (concurrent) : attendre brièvement la transition `processed`, sinon répondre **non-2xx** (force retry provider)
- Ne jamais passer à `processed` sans preuve d'un traitement effectif
- Contexte technique : Stripe / NestJS — 09-03-2026