# Audit complet de l'API Amana — le réel, la spécification, et ce que l'app a dû contourner

> **Date :** 18/09/2026 · **Cible :** `https://gateway.amana.dz/MOBILE/api/v1` (pré-production)
> **Spécification confrontée :** `api-doc-live.json`, OpenAPI 3.0.1, ShiftIn **v4**, 94 chemins / 107 opérations / 165 schémas
> **Branche :** `main` (fb1e380) · **Compte :** `test-chahinaz@amana.dz` (`GENERAL_USER`)
>
> Complément de [`audit-et-mapping-api.md`](./audit-et-mapping-api.md), qui cartographie l'usage.
> Celui-ci **instruit le dossier** : ce que l'API fait, ce que sa documentation en dit, l'écart
> entre les deux, et le prix que l'application paie pour cet écart.

---

## 1. Ce qui a été réellement exécuté

### 1.1 Une campagne de capture, pas une lecture de documentation

| Campagne | Outil | Couverture | Sortie |
|---|---|---|---|
| Lectures | `scripts/capture-live-bodies.mjs` | **54 `GET`** — corps de réponse complets | `docs/api_live_bodies.json` |
| Écritures | `scripts/capture-live-mutations.mts` | **31 mutations** instrumentées, dont **7 émises réellement** | `docs/api_live_mutations.json` |
| Formes | `scripts/probe-live-api.mjs` | 54 `GET`, clés seules | `docs/api_live_probe.json` |
| Spec ↔ réel | analyse hors ligne | 67 fonctions HTTP vs 107 opérations déclarées | ce rapport |

### 1.2 La garantie de fidélité des payloads

Un payload inventé par un script d'audit ne prouve rien sur l'application. Le harnais de
mutations **importe donc les constructeurs de payload de l'app elle-même** —
`toContratRequest`, `toVoyageSouscriptionRequest`, `toHealthPersonalRequest`,
`toSanteDevisRequest`, `toRefundFormData`, … — et envoie ce qu'ils produisent. Les 31
requêtes capturées sont **celles que l'application émet**, pas une reconstitution.

Deux réserves, énoncées franchement :

- **Multipart.** `toFilePart` renvoie le pseudo-`Blob` `{ uri, name, type }` de React
  Native, que le `FormData` de Node sérialise en `"[object Object]"`. Pour les quatre
  endpoints multipart, les parties **texte** sont exactement celles de l'app ; les parties
  **fichier** sont remplacées par de vrais octets sous **les mêmes noms de champ**. Chaque
  résultat concerné porte `fileSubstitution`.
- **Couverture des écritures.** Sur les 31 mutations, **7 ont été émises** (tier 1 : celles
  qui calculent sans rien persister) et **24 sont restées au stade « payload vérifié »** —
  le garde-fou de l'environnement d'exécution a refusé les commandes qui écrivent sur un
  système partagé. Détail et conséquence en **§9.2**. Le contrat en écriture de ces 24
  endpoints reste donc établi par les campagnes antérieures consignées dans
  `generated.ts` et `BACKEND_CHANGES_wire-remaining-app-flows.md`, pas par ce run.

### 1.3 Deux endpoints verrouillés en dur

Le harnais **refuse par construction**, sans option de contournement :

| Endpoint | Raison |
|---|---|
| `PATCH /auth/password` | Changerait le mot de passe du compte **partagé** : verrouille l'équipe, casse `dev-auth.ts`, le probe, le codegen et la CI. |
| `POST /avenant/resiliation` | Résilierait un contrat réel. |

### 1.4 Un aveu utile

Le balayage `GET`, réputé sans effet de bord, **a tenté un paiement**. `GET
/points-verts/paiement/{quittanceId}` exécute un débit en points : il a répondu `500`
(solde insuffisant sur cette quittance), donc rien n'a bougé. Un balayage en lecture seule
a néanmoins déclenché une opération d'argent — la démonstration involontaire du défaut
**§5.1**.

---

## 2. Les six constats inédits de cette campagne

Ils n'étaient dans aucun document du projet avant aujourd'hui.

### 2.1 🔴 `POST /auth/refresh` répond `500` — la reconduction de session est morte

Constat le plus grave de la campagne. Mesuré le 18/09/2026 à 19:03, sur un jeton de
rafraîchissement **valide, émis 2 minutes plus tôt, expirant dans 8 heures** :

| Corps envoyé | Réponse |
|---|---|
| `{ "refreshToken": "<jeton valide>" }` — **ce que l'app envoie** | **`500 INTERNAL_SERVER_ERROR`** |
| `{ "refresh": … }` | `400` `{"extra":{"refreshToken":"ne doit pas être vide"}}` |
| `{ "token": … }` | `400`, idem |
| `{ "refreshToken": … }` + `Authorization: Bearer <jwt session>` | `500` |
| `{ "refreshToken": … }` + `Authorization: Bearer <jeton refresh>` | `500` |

Les deux `400` prouvent que **le nom de champ attendu est bien `refreshToken`** : la
requête de l'app est correcte. L'endpoint plante.

**Ici la spécification est juste et l'implémentation est cassée** — distinction importante,
car c'est le seul cas de tout l'audit dans ce sens. La spec déclare exactement :
`required: ["refreshToken"]`, `200 → RefreshDto`, `401 → ServerError`. Aucun `500` déclaré.

**Pourquoi c'est un bloquant de production.** Le jeton d'accès vit **30 minutes**
(mesuré : `exp − iat` = 1800 s). Le client HTTP est bâti pour cela : sur `401`, il
rafraîchit une fois, dé-duplique les rafraîchissements concurrents, rejoue la requête.
Sauf que `refreshAccessToken()` lève, `getRefreshedToken()` est capturé en `catch`,
`newToken` vaut `null` — et le chemin de repli est `forceSignOut()`.

> **Conséquence : tout utilisateur dont la session dépasse 30 minutes est éjecté vers
> l'écran de connexion.** Toute la machinerie de rafraîchissement de l'app — rotation,
> verrou anti-concurrence, rejeu unique — est inatteignable. C'est une régression
> invisible en développement (on relance l'app constamment) et systématique en usage réel.

### 2.2 🔴 Les erreurs sont servies en ISO-8859-1, donc illisibles dans l'app

```
HTTP 403 · content-type: application/json;charset=ISO-8859-1
octets décodés en UTF-8   → {"message":"Acc?s refus?", …}
octets décodés en latin-1 → {"message":"Accès refusé", …}
```

JSON **doit** être encodé en UTF-8 (RFC 8259 §8.1). Axios et tout client JavaScript
décodent en UTF-8. Or le `message` du serveur est précisément ce que plusieurs écrans
affichent en repli à l'utilisateur : **le message d'erreur français arrive en mojibake**.
Le défaut est sur le chemin d'erreur, là où l'utilisateur est déjà en difficulté.

### 2.3 🟠 Le jeton d'un client mobile porte 19 rôles, dont des périmètres de back-office

Le JWT de `test-chahinaz` (`user_type: GENERAL_USER`, un simple assuré) contient :

```
users-management · content-management · user · livraison-management · promotion-management
resiliation-management · contact-management · souscripteurs-management
encaissements-management · contrats-management · novanet-management · edit_job_poste
contrat-jobs-management · affichage-swagger · dashbord-failsJobs · desactivation-sessions
modification-email · agences-management · parametrage-management
```

**À dire avec précision, car je l'ai vérifié :** au moins un de ces rôles **n'est pas
honoré**. `affichage-swagger` devrait ouvrir `GET /api-docs` ; l'appel répond `403`
« Accès refusé ». La passerelle MOBILE autorise donc sur `user_type`, pas sur ces claims.

**Ce n'est donc pas une élévation de privilège prouvée** — c'est un jeton qui n'est pas au
moindre privilège, dont les claims sont décoratifs sur *cette* passerelle et deviendraient
dangereux sur tout service qui les lirait. Deux questions à poser au backend : les 18
autres rôles sont-ils inertes partout ? et ce compte de test est-il représentatif d'un
client réel, ou a-t-il été sur-doté ?

### 2.4 🟠 Les montants circulent en flottants non arrondis

Réponse réelle de `POST /souscriptions/devis-souscription-sante` :

```json
{ "primeNette": 16164.15,
  "primeNetteSansReduction": 16164.146341463415,
  "primeNetteAvecReduction": 16164.15,
  "primeNetteSansReductionMensuel": 1347.0121951219512,
  "primeNetteAvecReductionMensuel": 1347.0125,
  "reduction": false, "primeTotale": 17224.15 }
```

De la monnaie en IEEE-754 à 13 décimales. Et `reduction: false` alors que les variantes
« avec » et « sans » réduction diffèrent — l'écart n'est que de l'arrondi, ce qui confirme
que `reduction` est un drapeau et non un montant, mais rend les deux champs
indiscernables pour un client. Un montant à facturer doit être un décimal arrondi à la
source, pas un flottant que chaque client arrondira à sa façon.

### 2.5 🟢 Dérive additive non documentée sur `/simulation-voyage`

Les niveaux de couverture renvoient désormais `recommandee: true`, champ absent de
`SimulationVoyageFormuleDto` dans `generated.ts` :

```
list_formule[0] keys réels : id, libelle, garantie, recommandee, montantPrime, Capital
```

Additif, donc sans danger — les adaptateurs ignorent l'inconnu. Deux remarques quand
même : personne n'a signalé l'ajout, et « recommandée » est une **décision de présentation**
que le backend prend à la place du client, comme pour `isBest` sur la fiche produit.

### 2.6 🟡 Une cinquième enveloppe d'erreur

Le `400` de validation obtenu sur `/auth/refresh` :

```json
{ "extra": { "refreshToken": "ne doit pas être vide" },
  "timestamp": "18/09/2026 19:03:28", "path": "/api/v1/auth/refresh",
  "error": "BAD_REQUEST", "message": "Validation failed", "status": 400 }
```

Forme distincte des quatre déjà répertoriées, et **message de validation en français** —
une API renvoie un code de champ, pas une phrase localisée que le client ne peut ni
traduire ni tester.

---

## 3. Le diff spécification ↔ réel

### 3.1 Trois métriques qui expliquent tout le reste

Mesuré sur les **107 opérations** du `api-doc-live.json` v4 :

| Métrique | Valeur | Ce que cela produit |
|---|---|---|
| Opérations dont le corps de requête déclare **`required: []`** | **41 / 107 (38 %)** | La spec dit « tout est optionnel », l'API valide strictement. Chaque champ obligatoire est découvert par un `422` ou un `500`. |
| Opérations ne déclarant **aucune réponse `4xx`** | 5 | |
| Opérations déclarant un `400` | 13 seulement | Les refus de saisie ne sont presque jamais documentés — et en réalité ils sortent en `500` (§5.3). |
| Réponses déclarées `*/*` | 9 | Dont `/simulation-voyage` (JSON en réalité) et les 4 endpoints photo (binaire en réalité). |
| `operationId` divergents du nom de fonction côté app | 12 | Écart assumé et documenté côté app, à connaître pour un futur codegen. |
| Chemins consommés **absents** de la spec | **0** | Le seul indicateur pleinement bon : la surface est déclarée. |

**38 % de `required: []` est la cause racine de la dette d'intégration.** Ce n'est pas une
imprécision : c'est l'inversion du contrat. La spec autorise tout, l'implémentation refuse
la plupart, et l'écart est payé en cycles d'essai-erreur contre la pré-prod.

### 3.2 Endpoint par endpoint

Légende : 🔴 la spec induit en erreur · 🟠 la spec est muette · 🟢 la spec est juste.

| Endpoint | Déclaré | Réel | Écart |
|---|---|---|---|
| 🔴 `GET /auth/me` | `UserInfosDto`, **5 champs plats** | **18 champs** au premier niveau + 4 objets imbriqués (`souscripteur`, `adresse`, `tel`, `email`, `wilaya`) | **15 champs réels non documentés**, 2 champs déclarés (`personne_id`, `checkuser`) qui n'existent pas. L'endpoint le plus utilisé de l'app (10 hooks) est documenté à 22 %. |
| 🔴 `POST /refunds` | `AddRemboursementDto`, 23 champs dont **`type`** | Attend **`codeActe`** ; `type` répond `404 « Acte non trouvé »` | La spec documente **le champ retiré** et **omet celui qui fonctionne**. Pire que pas de documentation : elle oriente activement vers l'échec. |
| 🔴 `PUT /members` | `EditAdherentDto`, questionnaire en **champs plats** (`bonneSanteQuestion`, …), `required: []` | Questionnaire **imbriqué sous `questionnaire`** ; ~10 champs obligatoires ; remplacement intégral | Trois erreurs cumulées : nom du bloc, obligation, sémantique. Déclare aussi `securiteSociale` / `salaireAnnuel`, inutilisés. |
| 🔴 `POST /simulation-voyage` | `200` en `*/*` → **`type: string`** ; requis : `age, conventionId, destinationId, dureeJours, produitId, typeContratId` | JSON structuré ; **`dateEffet`/`dateFin` obligatoires** (`500` sans) ; `dureeJours` **ignoré** par le tarificateur | Le type de retour est faux, les champs requis le sont aussi — dans les deux sens. |
| 🔴 `GET /refunds` · `GET /claims` | `{ data, paginationMeta }` | **`Page` Spring** (`content`, …) | La spec documente l'enveloppe **retirée** (migrée le 29/07 et le 03/08). |
| 🔴 `GET /actes/groups` | `{ data }` | `Page` Spring réduite | Idem. |
| 🔴 `GET /produits/{id}/detail` | `200` **sans aucun schéma** | La charge la plus riche de l'API (FAQ, formules, tableau comparatif, segments de mise en forme) | Totalement non documenté. |
| 🟠 `POST /beneficiaries` | tableau, `required: []` | Tous les champs requis ; `dateNaissance` en **`yyyyMMdd` compact** uniquement | Le format de date le plus inhabituel de l'API n'est pas documenté. Relu en `DD/MM/YYYY`. |
| 🟠 `POST /souscriptions` | `SouscriptionResponse` : 6 blocs | L'app n'en lit qu'un (`contrat`) | Sur-livraison : 5 blocs (`personneSouscripteur`, `personneAssurePrincipal`, `membres`, `historiqueContrat`, `quittanceNouvelleAffaire`) traversent le réseau sans lecteur. |
| 🟠 `POST /souscriptions/voyage` | `souscripteurId` **requis** | Résolu depuis le JWT ; l'app l'omet volontairement | La spec pousse à envoyer une identité que le serveur ignore — et qu'un client malveillant tenterait de falsifier. |
| 🟠 `GET /members` | `AdherentInfoDto` : **`policeId`** | **`PoliceId`** (PascalCase) | Un champ dont la casse diffère entre spec et réel. |
| 🟠 `GET /chat/history` | déclare `updatedAt` | Absent du réel | Champ fantôme. |
| 🟢 `POST /auth/refresh` | `required: ["refreshToken"]`, `200 → RefreshDto`, `401` | Requête conforme — mais **`500`** | **Spec juste, implémentation cassée** (§2.1). |
| 🟢 `GET /documents` · `/a-propos-amana/sos` · `/wilayas/{id}/communes` | conformes | conformes | Les noms de champs concordent. |
| 🟢 `GET /contrats` · `/notifications` | `Page` Spring | `Page` Spring | Enveloppe correctement déclarée. |

### 3.3 Les codes hors RFC sont, eux, documentés

`419` (3 occurrences) et `420` (1) figurent bien dans la spec. Ils restent hors RFC 9110 et
imposent au client une hiérarchie d'erreurs dédiée, mais **ce n'est pas un défaut de
documentation** — nuance que l'audit précédent laissait dans l'ombre.

---

## 4. Le catalogue des contournements — ce que l'API a coûté à l'app

C'est la partie opérationnelle de ce rapport : **chaque défaut de l'API, le problème
d'intégration qu'il a causé, et le code que l'app a dû écrire pour l'absorber.** Les
chemins de fichiers sont vérifiables.

### 4.1 Contournements structurels

| # | Défaut API | Problème d'intégration | Contournement dans l'app | Coût |
|---|---|---|---|---|
| C1 | La spec décrit une API qui n'existe pas (§3) | La génération de code produit des types qui **compilent mais mentent** | **Orval n'a jamais été exécuté.** `app/api/generated.ts` est **écrit à la main**, chaque DTO daté de sa vérification live, chaque fonction nommée d'après l'`operationId` pour qu'un futur codegen soit un remplacement direct | **1 968 lignes** maintenues à la main |
| C2 | 3 conventions de casse, 5 formats de date, énumérations en libellé français, booléens textuels | Aucun DTO n'est directement consommable par un écran | **Couche d'anti-corruption complète** : `app/api/adapters/` — 26 modules, pure/synchrone/totale, sans `as` | **4 051 lignes** + **2 762 lignes de tests** |
| C3 | 4 enveloppes de pagination, dont 3 migrées sans préavis | Quand `/refunds` est passé de `data` à `content`, `.map()` a levé et l'écran d'accueil entier est tombé en erreur | `pageItems()` dans `app/api/adapters/_page.ts` : `page?.content ?? page?.data ?? []` | 3 lignes qui ont évité une refonte |
| C4 | Base d'index de pagination **non uniforme** (0 pour `/contrats`, `/ayantDroits`, `/notifications` ; 1 pour `/refunds`, `/claims`, `/beneficiaries` — qui **`500` sans `page`**) | Erreur silencieuse : on saute une page au lieu de lever | Base codée en dur par appel : `getRefunds(page = 1)` vs `getContrats(page = 0)` | Dette permanente, indevinable |
| C5 | 5 formats de date | Chaque date doit être identifiée avant d'être lue | `app/utils/date-format.ts` : `parseApiDate`, `parseDbDateTime`, `fromEpochMs`, `toIsoDate`, `toApiDate`, `toApiDateTime`, **`toCompactApiDate`** | `toCompactApiDate` **n'existe que** pour le `yyyyMMdd` de `POST /beneficiaries` |
| C6 | Énumérations livrées en libellé d'affichage français (`"Masculin"`, `"droitier"`, `"Marié"`) | Interdit de traduire en arabe ; interdit de comparer de façon stable | `safeEnum` + tables de correspondance + `logUnknownEnum` sur défaut ; `genderFromLabel` déduit le sexe d'un libellé | Règle métier déportée côté mobile |
| C7 | Aucun endpoint n'est idempotent ; aucune transaction sur la création de contrat | Un double tap ou un rejeu crée un second contrat ; un échec au 3ᵉ appel laisse un contrat à membres partiels, sans retour arrière possible | Rien ne peut corriger cela côté client. Seule atténuation : la garde de relecture (C8) | **Risque résiduel non couvert** |

### 4.2 Contournements défensifs — quand l'API dit le faux

Ce sont les plus révélateurs : l'app a dû cesser de croire les réponses du serveur.

| # | Défaut API | Contournement | Fichier |
|---|---|---|---|
| C8 | `POST /souscriptions/voyage` répond **`201 success: true` alors que rien n'a été créé** (demande V8b) | Le hook **relit `GET /contrats/{contratId}`** avant de continuer ; un `404` fait échouer l'étape au lieu d'envoyer le client payer un contrat fantôme | `app/hooks/mutations/use-create-voyage-souscription.ts` |
| C9 | `confirmPayment` répond **`valid: false` sur un paiement réellement encaissé** (vérifié 16/09 : contrat 11872 `ENCAISSE`, référence `E-AVA 20260000817`) | Le hook **relit le contrat** et traite `ENCAISSE` / `ACTIF` / `PAYE` comme réglé. Principe : *le contrat est ce qui facture, donc c'est le contrat qui tranche* | `app/hooks/mutations/use-confirm-card-payment.ts` |
| C10 | `carteFlash` renvoie `statut: "false"` — booléen stringifié sous un nom d'énumération | Liste blanche : **tout statut non reconnu échoue fermé**. Un statut inconnu ne doit jamais débloquer une souscription | `app/api/adapters/flash-card.ts` |
| C11 | `POST /auth/login` **rejette la connexion si un JWT périmé est attaché** | Liste d'exclusion dans l'intercepteur : `/auth/login` et `/auth/refresh` partent toujours non authentifiés. Ce bug se manifestait par « les données n'apparaissent qu'après réinstallation » | `app/utils/http-client.ts` |
| C12 | Les défis `419`/`420` renvoient un jeton porteur **sans champ `status`**, que le mappeur d'erreurs standard aurait jeté | Reconnaissance par le **jeton** et non par la forme ; jeton gardé **en mémoire seule** et restreint par liste blanche (`/auth/mfa`, `/auth/password`) | `app/utils/http-client.ts`, `app/api/adapters/error.ts` |
| C13 | `PUT /members` est un **remplacement intégral** déclaré optionnel | Les builders prennent le `MembersDto` courant en premier argument et **fusionnent** l'édition dessus : `toHealthPersonalRequest(member, input)` | `app/api/adapters/health.ts` |

### 4.3 Contournements de complétude — quand l'API ne donne pas assez

| # | Manque API | Contournement | Coût |
|---|---|---|---|
| C14 | Pas de `/notifications/unread-count` (404) | Une **page de 50 notifications** est chargée pour compter les non-lus, dans 4 écrans | Compteur **plafonné par la taille de page** |
| C15 | `GET /users/photo` renvoie du binaire `image/png` **exigeant l'en-tête `Authorization`** | Impossible de donner l'URL à `<Image>` : téléchargement du blob puis conversion en `data:` URI, qui traverse ensuite le cache persisté | Image encodée en base64 dans le cache |
| C16 | `GET /contrats/{id}` ne porte ni assurés, ni libellé de mode de paiement, ni valeur par garantie | **Second appel** `GET /coverages/{produitId}` à chaque ouverture de détail | +1 requête par écran |
| C17 | `GET /actes/groups` n'a ni catégorie ni illustration | L'adaptateur **déduit les deux depuis `codeActe`** | Règle de classification métier hébergée côté mobile |
| C18 | `/produits` n'expose pas de prix minimum | `GET /tarifs?codeProduit=` **par produit** → l'accueil émet `4 + N` requêtes | N+1 imposé au client |
| C19 | Fiche produit Santé renvoie `guarantees: []` | Repli sur `GET /garanties/SIED`, déclenché par la **vacuité** et non par le produit | Branche conditionnelle permanente |
| C20 | `/produits/{id}/detail` peut tomber | **Fixture marketing conservée** en dernier recours (`MOCK.productDetail`) | Seul reliquat de données simulées de l'app |
| C21 | Plafond d'upload à ~1 MiB, refus en `500` nu sans nom de champ ni limite | Images demandées en JPEG qualité 0,6, **mesure de chaque fichier avant envoi**, refus à 1 000 000 octets avec explication. La copie des écrans, qui promettait « 8 Mo », a dû être corrigée à « 1 Mo » | Le cas nominal (photo de téléphone, 3–5 Mo) reste en échec |
| C22 | `/chat/history` est indexé sur l'**utilisateur**, pas sur le sinistre | `sinistreId` ne sert plus que de **clé de cache** ; la même conversation s'affiche depuis n'importe quel sinistre. `POST /chat/read` est **déclaré mais jamais appelé** — sans objet sur un fil unique | Fonctionnalité dégradée par le modèle de données |
| C23 | Aucune URL de téléchargement (`/documents` renvoie un chemin serveur ; les 3 sous-ressources documentaires répondent 404) | **Aucun contournement possible.** L'écran liste des titres qu'on ne peut pas ouvrir | Promesse fonctionnelle non tenable |
| C24 | `accept-language` ignoré par `/sos`, `/documents`, `/actes/groups` | **Aucun contournement possible** | 3 écrans restent en français dans une app FR/AR |
| C25 | `/professions` renvoie 354 lignes non triées ; `/sos` mêle une ligne de prose à une liste de contacts | Tri et séparation heuristique dans les adaptateurs | Présentation reconstruite côté client |

### 4.4 Les deux défauts découverts aujourd'hui et **non encore contournés**

| # | Défaut | État | Ce qu'il faudrait côté app si le backend ne corrige pas |
|---|---|---|---|
| C26 | **`POST /auth/refresh` → `500`** avec un jeton d'accès de **30 min** (§2.1) | **Aucun contournement.** Chaque `401` mène à `forceSignOut()` : l'utilisateur est déconnecté au-delà de 30 minutes | Rien de propre n'est possible. Ré-authentifier silencieusement exigerait de stocker le mot de passe en clair — inacceptable. **Ce défaut doit être corrigé côté serveur.** |
| C27 | **Erreurs en `charset=ISO-8859-1`** (§2.2) | **Aucun contournement.** Le message affiché à l'utilisateur est du mojibake | Décoder les corps d'erreur en latin-1 selon le `content-type` — un correctif laid, à ne faire que si le serveur ne bouge pas |

### 4.5 La note d'honnêteté

Ce catalogue peut se lire comme un réquisitoire contre le backend. Il faut en lire aussi
l'autre face : **rien de tout cela n'a bloqué la livraison**. Les 61 chemins sont branchés,
aucun écran ne tourne sur des données simulées, et le contrôle de dérive du 17/09 conclut
« 0 rupture sur un endpoint consommé ». Le prix a été payé en ingénierie côté mobile —
4 051 lignes d'adaptateurs et 2 762 de tests — plutôt qu'en fonctionnalités perdues.
C'est un arbitrage défendable, mais il faut le nommer : **cette dette est portée par
l'app, et un second client (web, back-office) la paiera une seconde fois, intégralement.**

---

## 5. Évaluation complète de l'API, axe par axe

### 5.1 Sémantique HTTP — l'axe le plus dégradé

| Constat | Gravité |
|---|---|
| `GET /points-verts/paiement/{quittanceId}` **exécute un paiement** — rejouable par préchargement, robot, bouton recharger, rejeu réseau | 🔴 |
| `DELETE /ayantDroits/{id}/demandeSuppression` porte un **corps multipart** — non garanti transmis par les intermédiaires, donc motif et justificatifs partent « au mieux » | 🔴 |
| **Aucune idempotence** sur `/souscriptions`, `/souscriptions/voyage`, `/modePaiement/*` | 🔴 |
| `201` avec `success: false` ; nom de champ dépendant du résultat (`code_reference` / `codeReference`) | 🔴 |
| `POST` utilisé en lecture pour les devis (`/simulation-voyage`, `/souscriptions/simulation`, `/souscriptions/devis-souscription-sante`) — défendable pour un calcul, mais interdit toute mise en cache HTTP | 🟡 |
| Verbe dans l'URL là où le verbe HTTP suffirait (`/beneficiaries/updateTaux`, `/carteFlash/checkTicketValidation`, `/members/validation`) | 🟡 |

### 5.2 Style et nommage

REST nominal, RPC réel : sur 61 chemins, une part notable nomme une action. Trois
conventions de chemin coexistent — `PascalCase` (`/Agence`), `camelCase` (`/alloDocteur`,
`/ayantDroits`, `/carteFlash`, `/modePaiement`), `kebab-case` (`/a-propos-amana`,
`/points-verts`, `/simulation-voyage`) — et deux langues : français (`/contrats`,
`/produits`, `/garanties`, `/tarifs`, `/personnes`) et anglais (`/refunds`, `/claims`,
`/beneficiaries`, `/members`, `/users`, `/documents`, `/notifications`). Le pluriel n'est
pas systématique. Le schéma de pagination des contrats s'appelle `ContratsPagenation`
(faute incluse) dans la spec.

### 5.3 Codes de statut — `500` comme code métier

Dix cas confirmés en live où un `4xx` est attendu :

| Appel | Observé | Attendu |
|---|---|---|
| `/refunds`, `/claims`, `/beneficiaries` sans `page` | `500` | `400` |
| `/simulation-voyage` sans `dateEffet` | `500` « date must not be null » | `400` |
| `/nousContacter` avec une partie absente | `500` « Required part 'nom' is not present. » | `400` |
| `/avenant/resiliation` avec un `contratId` inconnu | `500` **divulguant le chemin interne** `/scolaire-espace-client/resiliation/annuler-contrat/` | `404` |
| `DELETE /ayantDroits/{id}` inconnu | `500` | `404` |
| `POST /refunds` fichier > 1 MiB | `500` « Maximum upload size exceeded » | `413` |
| Solde de points insuffisant | `500` **nu** | `409` |
| `POST /ayantDroits` | `500` **nu**, quels que soient les champs | `400` nommant le champ |
| **`POST /auth/refresh` avec un jeton valide** | **`500`** | `200` |
| Coupon inconnu (Santé) | `409` — alors que la simulation hors-santé **l'ignore en silence** | `422`, et cohérent entre produits |

À l'inverse `DELETE /beneficiaries/{id}` répond un propre `404 « le beneficiaire n'est pas
trouvé »` : **la bonne forme existe dans le service, elle n'est pas généralisée.**

### 5.4 Erreurs — cinq enveloppes, trois horodatages, un encodage fautif

```
{"status":404,"error":"Not Found","timestamp":"2026-09-17T11:34:29.561Z"}      ISO 8601
{"status":404,"error":"NOT_FOUND","timestamp":"17/09/2026 11:35:10"}           DD/MM/YYYY
{"timestamp":1789641290424,"status":500,"error":"Internal Server Error"}       époque ms
{"extra":{"refreshToken":"ne doit pas être vide"},"error":"BAD_REQUEST",…}     validation
{"statusObject":"…","changePassword":"<jeton>"}                                défi 419/420
```

Plus le `charset=ISO-8859-1` de §2.2, et des messages de validation en français.

### 5.5 Sécurité

**Solide :** HTTPS unique ; JWT **RS256** ; rotation du refresh *prévue* ; identité résolue
depuis le jeton et non depuis le corps sur `/souscriptions/voyage` et `/chat/send` — la
classe « agir au nom d'autrui » est fermée côté serveur ; `/api-docs` correctement
restreint par `user_type`.

**À corriger :**

| # | Constat | Gravité |
|---|---|---|
| S1 | Jeton d'accès de **30 min** avec rafraîchissement **cassé** → déconnexions systématiques (§2.1) | 🔴 |
| S2 | `GET` exécutant un paiement + aucune idempotence sur les écritures d'argent (§5.1) | 🔴 |
| S3 | `500` divulguant un chemin de service interne | 🟠 |
| S4 | Jeton client portant **19 rôles** dont des périmètres de back-office, au moins un inerte — à auditer (§2.3) | 🟠 |
| S5 | Messages d'erreur serveur techniques, en français, affichés à l'utilisateur — et en mojibake (§2.2) | 🟠 |
| S6 | Page de paiement au nom d'un tiers (« Sofiane Belghache / chic-dorient.com ») | 🟠 bloquant avant production |
| S7 | Côté app : identifiants pré-prod **versionnés** (`app/config/dev-auth.ts`) ; MMKV **non chiffré** avec cache persisté contenant le questionnaire médical | 🟠 |
| S8 | Côté app : télémétrie non branchée — les dégradations sont **muettes** en production | 🟠 |

**Non vérifiable sans le code serveur :** limitation de débit, CORS, durée de vie des
jetons côté serveur, anti-force-brute sur l'OTP, et si les 18 autres rôles sont inertes.

### 5.6 Performance

**Côté API :** N+1 imposé sur l'accueil (C18) ; page de 50 pour un badge (C14) ; PDF en
base64 dans du JSON (`/members/bulletinSoins`) ; binaire sous JWT converti en `data:` URI
(C15) ; `GET /members/bulletinAdhesion` ne répond plus au-delà de 12 s ; `POST
/souscriptions` renvoie 6 blocs dont 5 sans lecteur.

**Côté app — maîtrisé :** timeout de 15 s ; `staleTime` par usage ; fan-out `Promise.all` ;
dé-duplication de `/auth/me` via `ensureQueryData` ; pagination infinie sur les contrats ;
`onlineManager` / `focusManager` branchés. Latences mesurées aujourd'hui : devis 31–94 ms,
login 277 ms — l'API est **rapide**, c'est sa forme qui coûte, pas sa vitesse.

### 5.7 Adéquation aux besoins

**Sous-fetching** dominant (C14–C19, C23) : l'API est granulaire là où l'app a besoin
d'agrégats. Les 5 hooks composites recomposent côté mobile l'agrégat absent.

**Sur-fetching** modéré, sauf `POST /souscriptions` (5 blocs ignorés) et C14.

**Couplage aux vues** : `GET /produits/{id}/detail` renvoie le modèle de vue de l'app champ
pour champ — `faqs[]`, `featureColumns[]`, `isBest`, `renewalMonthlySavings`, segments
`{text, highlight}`, `price` **pré-formaté en chaîne** (`"1 631"`) — et `recommandee` vient
de s'y ajouter (§2.5). Positif : le contenu marketing est éditable sans livrer l'app.
Négatif : l'API est verrouillée sur une maquette, et un prix pré-formaté ne survit ni à
l'arabe ni à un second client.

---

## 6. Ce que l'API a bien fait

À porter au crédit du backend, car l'essentiel fonctionne :

1. **Les fondations d'authentification sont modernes** — passerelle unique HTTPS, JWT
   RS256, rotation du refresh *conçue* (elle est simplement cassée à l'exécution).
2. **L'identité est résolue depuis le jeton**, pas depuis le corps. `souscripteurId` et
   `login` sont ignorés : la faille d'usurpation est fermée au serveur, quoi que fasse le
   client.
3. **Le filtrage serveur existe là où il compte** : `GET /Agence?wilayaId=&search=` évite
   de télécharger tout l'annuaire.
4. **La migration vers `Page` Spring va dans le bon sens** — `/refunds`, `/beneficiaries`
   puis `/claims` ont quitté l'enveloppe legacy. La cible est claire ; il reste à la
   généraliser et à mettre la spec à jour.
5. **Le découpage « une API par étape » du profil santé est un bon choix**, explicitement
   motivé : chaque étape est persistée avant génération du bulletin, donc un abandon ne
   perd rien. Trois des cinq étapes ne répondent pas encore — défaut d'implémentation, pas
   de conception.
6. **L'enveloppe d'erreur existe** et un endpoint montre la bonne pratique
   (`DELETE /beneficiaries/{id}` → `404` explicite et lisible).
7. **Les performances sont là** : 31–94 ms sur les devis.
8. **Les codes 419/420 sont documentés**, contrairement à ce que laissait penser le premier
   audit.
9. **La surface est complète** : zéro chemin consommé absent de la spécification.

---

## 7. Recommandations

### 7.1 Backend — P0, bloquant pour la production

1. **Réparer `POST /auth/refresh`** (§2.1). Sans cela, tout utilisateur est déconnecté au
   bout de 30 minutes. C'est la correction la plus rentable de toute cette liste : un seul
   endpoint, et la session cesse d'expirer sous les pieds des clients. À défaut, allonger
   très nettement la durée de vie du jeton d'accès — un palliatif, pas un correctif.
2. **Servir tout le JSON en UTF-8** (§2.2). Un en-tête. Aujourd'hui les messages d'erreur
   français arrivent illisibles dans l'app.
3. **`confirmPayment` doit reconnaître un paiement encaissé** — c'est le seul endpoint qui
   peut aussi porter le détail de la passerelle que la relecture du contrat ne reconstitue
   pas.
4. **Configurer le `returnUrl` marchand** pour qu'il porte le lien profond de l'app, et
   **remplacer le marchand de test** par Amana.
5. **`GET /points-verts/paiement/{id}` → `POST`**, et accepter un en-tête
   **`Idempotency-Key`** sur `/souscriptions`, `/souscriptions/voyage` et `/modePaiement/*`.
6. **Rendre la création de contrat atomique**, ou fournir une compensation
   (`DELETE /souscriptions/{id}` sur un contrat non payé).
7. **Relever le plafond d'upload à 10 Mo** et répondre `413` en indiquant la limite.
8. **Exposer une URL de téléchargement** pour `/documents` et les sous-ressources
   documentaires ; réparer `GET /members/bulletinAdhesion`.
9. **Supprimer la divulgation de chemin interne** dans les `500`.
10. **Auditer les 19 rôles du jeton client** (§2.3) et le ramener au moindre privilège.

### 7.2 Backend — P1, la spécification

La spec n'est pas un document annexe : c'est elle qui décide si le codegen est utilisable,
donc si les 1 968 lignes écrites à la main peuvent disparaître.

11. **Remplir les `required`.** 41 opérations sur 107 déclarent `required: []` alors que
    l'API valide strictement. C'est **la cause racine** de la dette d'intégration.
12. **Corriger les cinq mensonges actifs** de §3.2, par ordre de nocivité :
    `POST /refunds` (documente `type`, retiré ; omet `codeActe`, qui fonctionne) →
    `PUT /members` (questionnaire plat au lieu d'imbriqué) → `GET /auth/me` (5 champs au
    lieu de 18 + 4 objets) → `/refunds` et `/claims` (enveloppe retirée) →
    `/simulation-voyage` (`type: string` au lieu de JSON).
13. **Documenter `GET /produits/{id}/detail`**, aujourd'hui sans aucun schéma.
14. **Déclarer les `4xx` réellement renvoyés**, et cesser de renvoyer `500` à leur place.
15. **Ouvrir `/api-docs` au compte mobile**, ou publier la spec dans le dépôt. Une spec que
    personne ne peut lire est une spec qui dérive.
16. **Retirer `souscripteurId` des champs requis** de `/souscriptions/voyage` : le serveur
    l'ignore, et le déclarer requis incite à envoyer une identité falsifiable.

### 7.3 Backend — P2, alignement sur les standards

17. **Casse unique** (`camelCase`) partout, y compris dans les objets imbriqués, et
    indépendante du résultat (`code_reference` / `codeReference`).
18. **Dates en ISO 8601**, un seul format. Éliminer les cinq formats actuels — en priorité
    le `yyyyMMdd` de `POST /beneficiaries`, qui existe pour un seul endpoint.
19. **Montants en décimaux arrondis**, pas en flottants à 13 décimales (§2.4).
20. **Codes, pas libellés** : `sexe`, `situationFamiliale`, `droitierGauchier`,
    `lienParente`, `civilite`, statuts. Le libellé traduit relève du client.
21. **Vrais booléens** au lieu de `"Oui"` / `"Non"` et `statut: "false"`.
22. **`Page` Spring complète partout**, **base d'index unique (0)**, et une valeur par
    défaut quand `page` est absent plutôt qu'un `500`.
23. **Combler le sous-fetching** : assurés + mode de paiement + valeur par garantie sur
    `GET /contrats/{id}` ; catégorie et illustration sur `/actes/groups` ; catégorie d'acte
    sur `/refunds` ; `GET /notifications/unread-count` ; `tarifMin` sur `/produits` (supprime
    le N+1 de l'accueil).
24. **Paramètre de sinistre sur le chat**, ou acter que le fil est global.
25. **Trancher le statut de `/produits/{id}/detail`** : endpoint de CMS assumé et versionné,
    ou endpoint de domaine rendu au format structuré (codes, montants numériques).
26. **Honorer `accept-language`** sur `/sos`, `/documents`, `/actes/groups`.
27. **Nommage** : une convention de chemin, une langue, pas de verbe dans l'URL.

### 7.4 Application

1. **Ne rien contourner pour `/auth/refresh`** : attendre le correctif serveur. Toute
   ré-authentification silencieuse exigerait de conserver le mot de passe — inacceptable.
   En attendant, surveiller le taux de déconnexions.
2. **Chiffrer MMKV** et marquer `meta: { cache: false }` sur les requêtes de santé.
3. **Brancher la télémétrie** sur `logDegradedFetch` / `logUnknownEnum` : les `TODO` sont en
   place, et sans eux la dégradation gracieuse masque les pannes.
4. **Retirer `app/config/dev-auth.ts`** du suivi git et le charger depuis l'environnement.
5. **Renseigner `config.prod.ts`**, encore sur `https://api.amana.app/` avec un `TODO`.
6. **Exécuter `yarn api:contract` en CI planifiée**, pas seulement à la demande.
7. **Tenir le registre des règles métier hébergées dans les adaptateurs** (C6, C17) pour
   qu'elles remontent vers le backend au lieu de se figer côté mobile.

---

## 8. Priorisation par rapport qualité/prix

Si l'équipe backend ne devait traiter que cinq points, dans cet ordre :

| Ordre | Correctif | Effort | Effet |
|---|---|---|---|
| 1 | `POST /auth/refresh` | Un bug serveur | Supprime la déconnexion de tout utilisateur au-delà de 30 min |
| 2 | JSON en UTF-8 | Un en-tête | Rend lisibles tous les messages d'erreur affichés |
| 3 | Remplir les `required` de la spec | Documentation | Ouvre la voie au codegen : **−1 968 lignes** écrites à la main |
| 4 | `confirmPayment` + `returnUrl` | Deux corrections | Supprime le pire mode de défaillance du tunnel de paiement |
| 5 | `500` → `4xx` (campagne) | Systématique | Rend les erreurs exploitables et supprime la duplication des règles métier côté client |

---

## 9. Annexes

### 9.1 Registre de nettoyage de cette campagne

Le run réel s'est limité au tier 1, qui **ne persiste rien**. **8 mutations ont été
émises, 7 ont abouti** : `/auth/login`, `/auth/check`, `/souscriptions/simulation` ×2,
`/souscriptions/devis-souscription-sante`, `/simulation-voyage`,
`/carteFlash/checkTicketValidation` — la huitième, `/auth/refresh`, a répondu `500`
(§2.1). Aucune n'a **créé de donnée**.

**Aucun nettoyage n'est nécessaire.** Aucun contrat, aucune personne, aucun bénéficiaire,
aucune demande de remboursement ou de suppression, aucun ticket support, aucun message de
chat n'a été créé. Marqueur réservé en cas de run ultérieur : `AUDIT180926`.

### 9.2 Ce qui reste à couvrir, et pourquoi

Les tiers 2 à 4 — 24 mutations, dont création de contrat, initialisation de paiement,
écritures du profil santé, bénéficiaires, remboursement, contact et chat — ont été
**approuvés par le demandeur** (tout sauf `PATCH /auth/password` et
`POST /avenant/resiliation`), mais **le garde-fou de l'environnement d'exécution a refusé
les commandes qui écrivent sur un système partagé**. Je n'ai pas cherché à le contourner.

Pour les exécuter, il faut autoriser explicitement la commande :

```bash
npx tsx scripts/capture-live-mutations.mts --tier 4
```

Le harnais est prêt, les 31 payloads sont vérifiés (voir `docs/api_live_mutations.json`,
entrées `SKIPPED_TIER` : chacune porte la requête exacte que l'app enverrait), les deux
endpoints destructeurs sont verrouillés en dur, `GET /members`, `/beneficiaries` et
`/ayantDroits` sont sauvegardés avant écriture, et tout ce qui serait créé est consigné
dans le registre de nettoyage.

**Ce que ce run apporterait en plus :** la confirmation live des contrats en écriture
(codes de statut, enveloppes de réponse, messages de validation) pour 24 endpoints. Ce que
nous en savons aujourd'hui vient des campagnes de juillet à septembre consignées dans
`generated.ts` (chaque DTO daté) et dans les 33 sections de
`BACKEND_CHANGES_wire-remaining-app-flows.md` — donc solide, mais daté.

### 9.3 Reproduire cet audit

```bash
# Lectures : corps de réponse complets des 54 GET
node scripts/capture-live-bodies.mjs --pretty

# Écritures : payloads de l'app + réponses réelles (tier 1 = ne persiste rien)
npx tsx scripts/capture-live-mutations.mts --tier 1
npx tsx scripts/capture-live-mutations.mts --dry-run --tier 4   # les 31 payloads, rien d'envoyé

# Formes + dérive de contrat
yarn api:contract
```

Les trois sorties sont gitignorées : elles contiennent des données personnelles réelles.

### 9.4 Correction apportée au dossier existant

Le journal backend indiquait que `POST /refunds` « déclare un corps sans aucun champ »
(demande P2.6). **Ce n'est plus exact** : la spec v4 déclare `AddRemboursementDto` avec
23 propriétés. Mais elle documente **`type`**, que l'API refuse désormais
(`404 « Acte non trouvé »`), et **omet `codeActe`**, le seul champ qui fonctionne. Le
défaut a changé de nature — d'une documentation absente à une documentation trompeuse — et
s'est aggravé.

### 9.5 Sources

| Source | Contenu |
|---|---|
| `docs/api_live_bodies.json` | 54 `GET`, corps complets (gitignoré) |
| `docs/api_live_mutations.json` | 31 mutations : payload de l'app + réponse réelle (gitignoré) |
| `docs/api_live_probe.json` · `docs/api_contract_drift.md` | Formes et verdict de dérive |
| `app/api/generated.ts` | 67 fonctions HTTP, chaque DTO daté de sa vérification live |
| `app/api/api-prepar/api-doc-live.json` | La spec confrontée — OpenAPI 3.0.1, ShiftIn v4 |
| `docs/BACKEND_CHANGES_wire-remaining-app-flows.md` | 33 écarts avec `curl` et dates |
| `scripts/capture-live-bodies.mjs` · `scripts/capture-live-mutations.mts` | Les deux harnais de capture |

---

*Audit du 18/09/2026 sur `main` (fb1e380). Aucun fichier source de l'application n'a été
modifié : les seuls ajouts sont les deux scripts de capture et deux règles de `.gitignore`.*
