# Back-end — Risques & vigilance

Ce fichier recense des risques back-end susceptibles de provoquer :

- incidents prod,
- failles de sécurité,
- bugs non diagnostiquables,
- régressions coûteuses,
- incohérences de données.

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

---

## Règles d’utilisation

- Chaque entrée doit dire :
  - ce qui peut mal se passer,
  - comment on le voit (symptômes),
  - comment on le maîtrise (mitigation).
- Si c’est lié à une stack / version : on note le contexte.

---

## Index

- [AuthN/AuthZ dispersée](#risque-authn-authz-dispersee)
- [Guard global manquant (request.user)](#risque-guard-global-manquant)
- [Duplication silencieuse de constantes (contracts)](#risque-duplication-constantes-contracts)
- [Contrats API implicites](#risque-contrats-api-implicites)
- [Erreurs non standardisées](#risque-erreurs-non-standardisees)
- [Migrations risquées / non reproductibles](#risque-migrations-risquees)
- [Non-idempotence sur opérations sensibles](#risque-non-idempotence)
- [Stripe : `billing_cycle_anchor` vs `current_period_end`](#risque-stripe-current-period-end)
- [PostgreSQL/Prisma : `@unique` nullable](#risque-prisma-unique-nullable)
- [Observabilité insuffisante](#risque-observabilite-insuffisante)
- [Webhooks entrants — répondre 200 pendant `processing` (event perdu)](#risque-webhook-200-processing)
- [Redis — thrash de connexion sous charge](#risque-redis-thrash-connexion)
- [Entitlements — TTL cache supérieur au SLA de propagation](#risque-entitlements-ttl-sla)
- [Guard NestJS route-level — null-check manquant sur `request.user`](#risque-guard-request-user-null)
- [Compteurs in-memory ≠ métriques persistées](#risque-compteurs-inmemory)
- [Interface provider incomplète ou divergente de ses implémentations](#risque-interface-provider-incomplete)
- [Boucle `upsert` N+1 sur synchronisation provider](#risque-upsert-n-plus-un-provider)
- [Stripe `list()` sans gestion de `has_more`](#risque-stripe-list-has-more)
- [Concurrence entre activation locale et webhook sur transition trial → payant](#risque-trial-payant-concurrence)
- [`jest.clearAllMocks()` dans des `beforeEach` imbriqués avec mocks Prisma](#risque-jest-clearallmocks-imbrique)

---

<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-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-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-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-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-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-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-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-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

---

<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-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-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-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

---

<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

---

<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-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
