# Backend changes needed — remaining app-side wiring

> Compiled while wiring the last mocked flows to the real API
> (`https://gateway.amana.dz/MOBILE/api/v1`) on branch
> `feat/wire-remaining-app-flows`, using the adapter pattern from
> [`API_IMPLEMENTATION_GUIDE.md`](./API_IMPLEMENTATION_GUIDE.md).
>
> Each item lists **what the frontend had to do as a workaround** and **what we
> need from the backend** to remove it.
>
> Verified live on **2026-09-15** and **2026-09-16** with test account
> `test-chahinaz@amana.dz` (`GENERAL_USER`, health adherent 415, police 306).
>
> ⚠️ **The write endpoints were exercised for real on 2026-09-16**, because the
> spec turned out to be wrong about most of them. What that left on the shared
> pre-prod account is listed in §20.

---

## Severity legend

| Tag         | Meaning                                                       |
| ----------- | ------------------------------------------------------------- |
| 🔴 BLOCKER  | Forces a hand-written workaround / prevents codegen           |
| 🟠 DATA GAP | View-model field has no real source; value is faked/heuristic |
| 🟡 CONTRACT | Inconsistency or undocumented behavior; works but fragile     |

---

## What this branch wired

| Flow                              | Endpoint(s)                                                                    | Note                                   |
| --------------------------------- | ------------------------------------------------------------------------------ | -------------------------------------- |
| Contacts utiles (SOS)             | `GET /a-propos-amana/sos`                                                      | was hard-coded in the route            |
| Documents utiles                  | `GET /documents`                                                               | list only — see §3                     |
| Notifications badge               | `GET /notifications`                                                           | counted client-side — see §4           |
| Actes de remboursement            | `GET /actes/groups`                                                            | category/illustration derived — see §5 |
| Garanties de déclaration          | `GET /garanties/{codeProduit}`                                                 | shared with the product sheet          |
| Personnes assurées                | `GET /ayantDroits`                                                             | replaces `/beneficiaries` — see §6     |
| Espace santé                      | `/members` + `/ayantDroits` + `/beneficiaries` + `/refunds` + `/notifications` | composite                              |
| Profil santé — identité           | `PUT /members`                                                                 | see §7                                 |
| Profil santé — questionnaire      | `PUT /members`                                                                 |                                        |
| Bénéficiaires                     | `POST /beneficiaries`, `DELETE /beneficiaries/{id}`                            | see §8                                 |
| Répartition des capitaux          | `PUT /beneficiaries/updateTaux`                                                |                                        |
| Retrait d'un ayant droit          | `DELETE /ayantDroits/{id}/demandeSuppression`                                  | see §9                                 |
| Résiliation                       | `POST /avenant/resiliation`                                                    | justificatif not sent — see §10        |
| Carte Flash                       | `POST /carteFlash/checkTicketValidation`                                       | see §11                                |
| Contactez-nous                    | `POST /nousContacter`                                                          | see §12                                |
| Déclaration de remboursement      | `POST /refunds`                                                                | see §13                                |
| Chat service client               | `GET /chat/history`, `POST /chat/send`, `POST /chat/read`                      | per user, not per sinistre — see §15   |
| Pièces du profil santé            | `POST /members/addPieceJointe`                                                 | **not wirable** — see §22              |
| Dépendants du profil santé        | `GET /ayantDroits`                                                             | wizard seeded live (2026-09-16)        |
| Renouvellement — assuré principal | `/auth/me` + `/members`                                                        | prefill live (2026-09-16)              |
| Adjonction — contrat & nom        | `GET /contrats/{id}` + `/auth/me`                                              | prefill live (2026-09-16)              |

**Nothing is left on `mockQuery`.** The service-client chat was wired on request
despite the modelling mismatch in §15.

---

## 1. 🟠 Editorial content is French-only

`GET /a-propos-amana/sos`, `GET /documents` and `GET /actes/groups` return the
same French payload whatever `accept-language` carries (verified live with
`ar`).

**Frontend workaround.** Section titles and subtitles stay in the app's own
translation files; only the rows come from the API. In Arabic the app therefore
shows translated headings around French rows.

**Needed.** Honour `accept-language` on the content endpoints, or expose an
`ar` variant of `label` / `titre` / `name`.

---

## 2. 🟡 `GET /a-propos-amana/sos` mixes a prose row into a contact list

Row `id: 9` carries the opening hours in `value` ("Notre service client est à
votre service, du Dimanche…") while every other row carries a phone number.

**Frontend workaround.** Rows whose `value` contains a letter are filtered out
(`adaptSosDirectory`), because the screen shows that text as its own translated
section subtitle.

**Needed.** A `type` discriminator on the row (`phone` vs `note`), or move the
prose out of the contact collection.

---

## 3. 🔴 `GET /documents` exposes no downloadable URL

`fichier` is a server-side filesystem path
(`opt/amana-digitale-preprod/documentUtilDigital/…/06.pdf`). It is not
reachable: `GET /assets?path=<fichier>` answers `404`, with or without a leading
slash.

**Frontend workaround.** The list is rendered from the API, but the download
button is hidden (`downloadUrl` is empty). Nothing can be opened.

**Needed.** Either a `GET /documents/{id}` binary route, or a public/`/assets`
path that actually resolves. An `id` per document would also help — the rows
currently have none, so the app keys them by file path.

---

## 4. 🟠 No unread-notifications count (request P3.2, still open)

`GET /notifications/unread-count` answers `404`.

**Frontend workaround.** The badge counts `unOpened: true` over the first page
(`size=50`) of `GET /notifications`, in `adaptUnreadNotificationsCount`. The
count is therefore capped by the page size.

**Needed.** `GET /notifications/unread-count` → `{ count: integer }`.

Related: `/notifications` returns the **reduced** Spring page (no `pageable`,
no `last`), unlike `/contrats` and `/refunds` (request P2.4).

---

## 5. 🟠 `GET /actes/groups` has no category and no illustration (request P3.1)

Each acte carries `id`, `name`, `codeActe` and `requiredFiles[]`, but no
`category` / `categoryLabel` and no illustration. **`codeActe` is not unique**:
the eight "Prestation sociale" actes all share the code `Evenement`.

**Frontend workaround** (`app/api/adapters/actes.ts`):

- the category is derived from the **group id** (1 Maladie, 2 Maternité,
  3 Dentaire & Optique, 4 Prestation sociale, 5 Actes médicaux);
- the illustration is derived from the **acte id**, with a per-category
  fallback for an unknown one;
- `categoryLabel` is the server's `nameGroup`, shown as-is.

Both maps break silently if the ids are renumbered.

**Needed.** A stable `category` (or the guarantee that group ids are frozen),
an illustration/icon key per acte, and a unique `codeActe`.

---

## 6. 🟡 `POST /refunds` keys on `ayantDroitId`, not on a beneficiary

The reimbursement is filed for a **covered person**, so the picker must list
`GET /ayantDroits` — not `GET /beneficiaries`, which is the death-benefit
beneficiary list with capital shares. The app previously read the wrong one.

**Frontend fix.** `useGetRemboursementBeneficiaries` and
`useGetDeclarationInsuredPersons` both read `/ayantDroits` now.

**Needed.** Nothing — recorded so the naming is not "fixed" the wrong way
later. Renaming the request field to `personneCouverteId` would remove the
ambiguity.

---

## 7. 🔴 `PUT /members` — the spec is wrong on three counts

Exercised live 2026-09-16. `EditAdherentDto` describes a patch with optional
fields; the endpoint is nothing of the sort.

**a. It is a full-record replace.** Sending only the block a screen owns
answers `422` listing everything else:

```
{codePostal=[le code postal doit comporter 5 chiffres ],
 ville=[la ville est obligatoire ],
 dateEntreeService=[il s'agit d'un cas d'une santé groupe, la date d'entré au service est obligatoire],
 questionnaire.bonneSanteQuestion=[la réponse sur la bonneSanteQuestion est obligatoire ], …}
```

**b. The questionnaire block is named `questionnaire`,** not
`bonneSanteQuestion` as the spec declares — the validation errors give it away.

**c. The questionnaire answers are the strings `"Oui"` / `"Non"`,** not the
booleans the spec models. `GET /members` returns them as strings too, so **the
app read every medical answer as "no"** until this branch. Worth checking in any
other consumer of `/members`.

Undocumented validation, all discovered by 422: `telephone` exactly 10 digits,
`codePostal` exactly 5, `ville` required, `dateEntreeService` required for a
santé-groupe adherent.

**Frontend workaround.** Both profile mutations now `GET /members` first and
merge their edit onto the full record (`currentRecord` in
`app/api/adapters/health.ts`), pad the stored phone to 10 digits, and fall back
to `dateEntree` when `dateEntreeService` is null. The wilaya the screen collects
still has no destination in the contract and stays in the local patch.

**Needed.** Either honour a partial update, or document the full-record contract
and its validation rules; name the questionnaire block consistently between the
spec, the request and the response; and declare the answers as the strings they
are (or switch both directions to booleans).

> The pre-prod account could not be saved at all before this: its stored
> `telephone` ("550123252", 9 digits) and `codePostal` ("1600", 4 digits) both
> fail the endpoint's own validation. See §20.

---

## 8. 🟡 `/beneficiaries` has no bulk replace and no transaction

The screen edits the beneficiary list as a whole; the API only offers
`POST /beneficiaries` (array, create) and `DELETE /beneficiaries/{id}`.

**Frontend workaround.** `useSaveHealthProfileBeneficiaries` diffs the list,
creates first and deletes last, so an interruption leaves extra rows rather
than missing ones. There is no rollback.

**Needed.** A replace-the-list endpoint, or a transactional batch — same family
as request P1-B on the voyage souscription.

---

## 9. 🔴 `DELETE /ayantDroits/{id}/demandeSuppression` declares a body with no fields

The spec declares `multipart/form-data` for this route with **zero properties**,
while the screen collects a removal motif and supporting documents.

**Frontend workaround.** The motif and request date are sent as form parts
(`motif`, `dateDemande`) on a best-effort basis; documents are ready to attach
under `justificatifs` but nothing confirms the server reads any of it. Because
the dependant keeps appearing in `GET /ayantDroits` until the back office acts,
the "pending" state is a local flag.

**Needed.** Document the accepted parts, and expose the request status (a
`removalRequested` flag on `/ayantDroits`) so the pending state can come from
the server.

---

## 10. 🔴 `POST /avenant/resiliation` takes a base64 string, not multipart

`AvenantDTO.justificatif` is a `string` with `extension` / `extensionLong`
alongside, unlike every other upload on this API (`/refunds`,
`/nousContacter`, `/users/photo`), which are `multipart/form-data`.

**Frontend workaround.** The cancellation is filed with its motif and comment;
**the attachments are not sent**. Base64-encoding a device file would need a new
native dependency (`expo-file-system`), which a wiring change should not
introduce.

`motifId` is also an undocumented `int32` with no listing endpoint: the app
sends 1–4 in screen order and repeats the motif label inside
`commentaireClient` so a mismatch stays recoverable.

**Needed.** A multipart variant of the endpoint (or a `justificatif` file part),
and the list of `motifId` values — ideally `GET /avenant/motifs`.

---

## 11. 🟠 `POST /carteFlash/checkTicketValidation` — `statut` is free text

`TicketResponseDTO.statut` is a plain string with no documented enum, and the
ticket carries no insured count and no prefilled identity, which the flash-card
funnel used to display.

**Frontend workaround.** A small allow-list (`VALIDE`, `VALID`, `OK`, …) decides
validity and **anything else fails closed** — an unknown status must never
unlock a subscription. The prefill block is left empty.

**Needed.** A documented enum (or a boolean `valide`), and the commercial
product name rather than only `codeProduit`.

---

## 12. 🟡 `POST /nousContacter` requires identity the form does not collect

`nom`, `email`, `tel`, `sujet`, `message` are all required, but the
"Contactez-nous" screen only asks for a subject, a message and an optional file.

**Frontend workaround.** The identity trio is read from the signed-in profile
(`/auth/me`), which matches the screen's own footer. `sujet` is sent as the
French label shown in the picker, since no enum is documented.

**Needed.** Resolve the sender from the JWT (the endpoint is authenticated
anyway), and document the accepted `sujet` values.

---

## 13. 🔴 `POST /refunds` — the acte field was renamed without notice

The spec declares `multipart/form-data` with an empty schema. The parts below
were confirmed live on 2026-09-16 by creating refund **1113345** on the test
account.

| Part                 | Value                                                   |
| -------------------- | ------------------------------------------------------- |
| `codeActe`           | the code from `/actes/groups` (e.g. `Consultation`)     |
| `ayantDroitId`       | id from `/ayantDroits`                                  |
| `dateSoins`          | `"DD/MM/YYYY"`                                          |
| `montant`            | amount claimed                                          |
| one per justificatif | named after `requiredFiles[].type` (`bulletinSoins`, …) |

Two traps:

- **The field is `codeActe`, not `type`.** The API team's 2026-07-28 message
  said `type`; that field is now ignored and the call answers
  `404 "Acte non trouvé"` — with no hint that the name changed. The rename
  shipped silently, which is precisely the failure mode that costs days.
- **File parts are keyed by `requiredFiles[].type`, not by the label.** The 422
  quotes the French label back (`{Bulletins=[Le fichier Bulletins est
obligatoire]}`), which reads as if the part should be called `Bulletins`. It
  must not be.

Response is `201` with its own envelope
(`{status, timestamp, message, path, details}`), unlike the `{message}` other
writes return.

**Needed.** Declare the parts in the spec, and announce a field rename on an
endpoint another team consumes.

---

## 14. 🟠 Espace santé has no cover-tier field and no card number

`EspaceSanteData` needs the subscription tier (individuelle / famille / groupe)
and a third-party-payer card number. `/members` exposes neither.

**Frontend workaround** (`app/api/adapters/espace-sante.ts`):

- tier inferred — `santeIndividuelle === true` → individuelle, else an
  `entreprise` → groupe, else famille;
- card number = `PoliceId`;
- dependants reuse the holder's card with their own identity on it;
- `hasTiersPayant` = the account is an adherent **and** has a police.

**Needed.** An explicit cover tier and a real card number on `/members`.

---

## 15. 🔴 The service-client chat is keyed on the USER, not on the sinistre

`GET /chat/history` takes no sinistre parameter (only `pageSize` and `beforeId`),
and every message on the live account carries the same `conversationId: 5`.
`POST /chat/send` likewise has nowhere to say which claim the message concerns.

**Consequence in the app.** Opening the chat from any sinistre shows the **same
single thread**, and a message sent from one claim appears under all of them. A
customer discussing two claims will see both conversations interleaved, with no
way to tell which reply answers which.

**Frontend state.** Wired as-is on request. `sinistreId` is carried through the
view model and keys the React Query cache only — it changes nothing about what
the server returns.

**Needed.** Either a `sinistreId` / `demandeId` parameter on `/chat/history` and
`/chat/send`, or an explicit product decision that the chat is a single
customer-support thread — in which case it should be reached from the menu, not
from a claim.

Contract notes, all verified live 2026-09-16:

| Observed                                                                 | Spec says      |
| ------------------------------------------------------------------------ | -------------- |
| `POST /chat/send` answers **200**                                        | 201            |
| `login` in the body is **not required** (sender resolved from the JWT)   | declared field |
| `POST /chat/read` answers **204**, and marks the _other_ side's messages | undocumented   |
| `senderType` is `"adherent"` \| `"support"`                              | matches        |
| feed is newest-first, `beforeId` is an exclusive cursor                  | undocumented   |

Sending as another user should stay impossible: the app deliberately omits
`login` rather than passing one.

---

## 16. 🟠 `POST /beneficiaries` takes a compact `yyyyMMdd` date, and nothing else

> **Correction.** An earlier edition of this document called this endpoint
> inoperable. It is not — the format had simply not been found. Probed again on
> 2026-09-16, and confirmed by creating and deleting a row.

| `dateNaissance` sent                               | Result                                      |
| -------------------------------------------------- | ------------------------------------------- |
| `01/01/1990` (the spec's StrictDate)               | `400` Jackson: _not a valid representation_ |
| `1990-01-01`, `1990-01-01T00:00:00.000Z`, RFC-1123 | `422` _n'est pas une date valide_           |
| epoch millis                                       | `422` same                                  |
| **`19900101`**                                     | **`201` "Bénéficiaires créés avec succès"** |

So the Jackson layer and the validator behind it accept exactly one common
shape, `yyyyMMdd`, which is documented nowhere. The endpoint then **reads the
date back as `DD/MM/YYYY`** — the asymmetry is the server's.

`DELETE /beneficiaries/{id}` answers `204` and removes the row cleanly.

**Frontend state.** Wired. `toCompactApiDate` converts at the boundary and the
"Compléter profil santé" wizard now creates its beneficiaries.

Two undocumented enums remain, discovered by probing:

- **`LienParenteEnum` accepts only `Conjoint`, `Enfant`, `Ascendant`.** The
  picker also offers _frère/sœur_ and _autre_, which have no representation —
  they are sent as `Ascendant`, and `isRelationshipSupported` lets the screen
  warn.
- **`SexeEnum` accepts only `Masculin` / `Féminin`.**

**Needed.** Document the date format (or accept ISO, like every other endpoint),
and publish both enums.

---

## 17. 🟡 `PUT /beneficiaries/updateTaux` requires the shares to total 100

`409 CONFLICT — "La somme des taux doit être égale à 100%"`. The rule is sound,
but it is undocumented, and **the pre-prod account does not satisfy it** (two
beneficiaries at 10 % and 19 %), so no distribution can be saved from it as-is.

**Frontend workaround.** The mutation refuses before the round trip and the
screen explains why, rather than surfacing a bare conflict.

**Needed.** Document the rule; seed a coherent distribution on the test account.

---

## 18. 🟠 Errors are `500` where they should be `4xx`

Three more cases found while probing, all of the same family as the P0 list in
`DEMANDES_BACKEND.md`:

| Call                                                             | Observed                                                                                  | Expected |
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------------------- | -------- |
| `POST /avenant/resiliation` with an unknown `contratId`          | `500`, leaking the downstream path `/scolaire-espace-client/resiliation/annuler-contrat/` | `404`    |
| `DELETE /ayantDroits/{id}/demandeSuppression` with an unknown id | `500`                                                                                     | `404`    |
| `POST /nousContacter` with a missing part                        | `500 "Required part 'nom' is not present."`                                               | `400`    |

`DELETE /beneficiaries/{id}`, by contrast, answers a clean
`404 "le beneficiaire n'est pas trouvé"` — that is the shape the others should
follow.

---

## 19. 🟡 `POST /carteFlash/checkTicketValidation` — `statut` is `"false"`

An invalid code answers `200` with `statut: "false"` and a usable
`message: "Ce code est invalide"`. So the field is a stringified boolean rather
than the status enum its name suggests.

**Frontend workaround.** An allow-list decides validity and **anything
unrecognised fails closed** — an unknown status must never unlock a
subscription. The server's `message` is shown when it refuses.

**Needed.** Confirm that a valid ticket answers `"true"`, or expose a real
boolean.

---

## 21. 🔴 Uploads are capped at ~1 MiB per file, and the refusal is a bare 500

Reported from a real device on 2026-09-16: attaching a photo to a reimbursement
claim answered

```
500 {"message": "Maximum upload size exceeded", "path": "/api/v1/refunds"}
```

Measured against `POST /refunds` the same day:

| File size               | Result                                                       |
| ----------------------- | ------------------------------------------------------------ |
| 1 024 KiB               | `201` — created                                              |
| 1 200 KiB               | `500 "Maximum upload size exceeded"`                         |
| 1 536 KiB               | `500`                                                        |
| 2 048 KiB / 4 096 KiB   | `500`                                                        |
| 2 × 600 KiB in one call | passed the multipart layer (`422` on a _missing_ third file) |

So the cap is **per file, around 1 MiB** — not per request. It is the Spring
default, left unconfigured.

**Two problems, not one.**

1. **The limit is far too low for its purpose.** A photo of a care bulletin taken
   on any recent phone is 3–5 MB. The feature is _photograph your justificatif_;
   at 1 MiB the nominal case fails.
2. **The refusal carries no field name and no limit**, and it is a `500` — so
   the app cannot tell the user which document was rejected, nor by how much.

**Frontend workaround.** Images are now requested from the camera and the
gallery at JPEG quality 0.6, and every file is measured before it is attached:
over 1 000 000 bytes it is refused on the spot with an explanation, instead of
letting the user fill in a whole form and lose it to an unexplained failure. The
screens' copy, which promised "8 Mo par fichier", was wrong and has been
corrected to 1 Mo.

Compression alone is not a guarantee: it lowers quality without resizing, so a
very large photo can still exceed the cap. Resizing would need a new native
dependency and is deliberately out of scope for a wiring change.

**Needed.**

- Raise `spring.servlet.multipart.max-file-size` (and `max-request-size`) to
  something a phone photo fits in — 10 MB is the usual figure for this kind of
  upload.
- Answer `413 Payload Too Large` (or a `422` naming the part) rather than `500`,
  and state the limit in the body so the app can quote it.

Until the limit is raised, expect users to be turned away on legible photos.

---

## 20. What the live testing left on the pre-prod account

Because the spec was wrong on most write endpoints, they had to be exercised for
real. On `test-chahinaz@amana.dz`:

| Change                                                                     | Why                                                |
| -------------------------------------------------------------------------- | -------------------------------------------------- |
| `telephone` `550123252` → `0550123252`                                     | the record could not be saved at all with 9 digits |
| `codePostal` `1600` → `16000`                                              | same — the endpoint demands 5 digits               |
| `dateEntreeService` set to `07/03/2023`                                    | required for a groupe adherent; was null           |
| Refund **1113345** created (Consultation, 1 200 DA, 01/09/2026)            | the only way to learn the multipart contract       |
| Contact message **43** created, labelled `[test intégration mobile 16/09]` | same, for `/nousContacter`                         |
| Chat message **139** posted, labelled `[test intégration mobile 16/09]`    | same, for `/chat/send`                             |

The first three are corrections of data that was invalid against the backend's
own rules. The last three are test artefacts QA should ignore. **No beneficiary
was created or deleted, no dependant removed, and no contract cancelled** — the
destructive endpoints were probed with non-existent identifiers only.

---

## 22. 🔴 `POST /members/addPieceJointe` — contrat indécouvrable

Announced ready by the API team on 2026-08-09. The endpoint exists and answers,
but **nothing states what to send**, and no amount of probing reveals it.

| Probe                                          | Result                                                                                                              |
| ---------------------------------------------- | ------------------------------------------------------------------------------------------------------------------- |
| Spec `requestBody`                             | `multipart/form-data` with **zero properties**                                                                      |
| Empty multipart                                | `422 "The given data was invalid."` — no field named                                                                |
| 25+ candidate part names, as files and as text | the same generic `422`                                                                                              |
| `rib` sent as a **file**                       | `400` leaking the DTO: _Failed to convert … MultipartFile … to required type `java.lang.String` for property `rib`_ |
| `rib` sent as **text** (a real RIB)            | `422` — accepted, but other required fields remain                                                                  |

So exactly one property is identifiable — `rib`, a `String` — and only because a
type mismatch leaked it. The file parts the screen must attach (RIB, bulletin
d'adhésion signé, fiche familiale, ouverture des droits, non-affiliation du
conjoint) cannot be named.

Contrast with `POST /refunds`, which answers
`422 {Bulletins=[Le fichier Bulletins est obligatoire]}` — that message is what
made the refund contract discoverable in minutes.

**Frontend state.** **Not wired, and not wirable.** The "Compléter profil santé"
screen collects real files since 2026-09-16, but they have nowhere to go. This
was mis-classified as remaining application work in the first edition of this
document; it is a backend blocker.

**Needed**, in order of usefulness:

1. The list of parts, with their names and which are required — a single working
   `curl` would be enough.
2. Failing that, make the validator name the missing fields, as `/refunds` does.
3. Declare the parts in the spec (request P2.6, which this endpoint shares).

---

## 23. 🔴 `POST /claims` — discoverable, then blocked on an unexposed referential

The sinistre declaration is **never filed**: the screen collects a guarantee, a
date, the circumstances and attachments, then navigates to a success page built
from a fixture. `POST /claims` (`addDemandeSinistreImplement`) exists, so this
was wired — until the last field proved impossible to fill.

Its validator names its fields, which made most of the contract discoverable in
minutes (unlike §22):

```
{garantie=[…], produit=[…], nom=[…], prenom=[…], date_naiss=[…],
 police=[…], circonstance=[…], date_sinistre=[…], path_files=[…]}
```

**Trap 1 — the validator and the binder disagree on the field names.** The error
reports `date_naiss`, `date_sinistre` and `path_files`; the endpoint actually
reads `dateNaiss`, `dateSinistre` and… `pieces`. Sending exactly what the error
asks for keeps returning the same error, which reads as a client bug and is not.

| Reported as     | Actually expected             |
| --------------- | ----------------------------- |
| `date_naiss`    | `dateNaiss` ("DD/MM/YYYY")    |
| `date_sinistre` | `dateSinistre` ("DD/MM/YYYY") |
| `path_files`    | `pieces` (the file part)      |

**Trap 2 — `produit` must match a referential no mobile endpoint exposes.** With
every other field accepted, the call fails:

```
500 … "Une erreur est survenue lors de la création de la demande.",
    "details": "Attempt to read property \"produit_sinistre_id\" on null"
```

Ten candidate values were tried — `SIED`, `EPVD`, `ACED`, `SCED`, the produit id,
the type-contract code, the long and short labels. All resolve to `null`. There
is no `GET /produits-sinistre` (or equivalent) on the mobile surface, so the
value cannot be derived.

**Frontend state.** **Not wired.** The screen still navigates to its success page
without filing anything — which is worse than an error, because the customer
believes their claim was submitted.

**Needed.**

1. The referential behind `produit` — an endpoint, or the list of accepted values.
2. Align the validator's field names with the ones the endpoint reads.
3. Declare the parts in the spec (request P2.6, shared with `/refunds`).

---

## 24. 🟠 Santé renewal and adjonction quotes refuse every payload

`POST /avenants/{contratId}/renouvellements/sante/devis` answers
`409 "problem devis Renouvellement Sante"` — a single opaque message, with no
field named and no reason given.

Probed on 2026-09-16 against six Santé contracts of the test account
(individuel and famille, `ENCAISSE` and `NON PAYE`), with `modifyDevis` true and
false, `typecontratid` 530 and 531, `formule` as a label and as an id, with and
without `membres`. Every combination returns the same 409.

It may well be a business precondition — a renewal window, a contract state —
but nothing in the response says so, and no documentation covers it.

**Frontend state.** The renewal path still prices from a fixture
(`MOCK.devis.sante.summaryPricing`), which is why two of the three remaining
mock reads in the app are here.

**Needed.** Either a message naming what is wrong, or the preconditions in
writing (which contract states are renewable, and from when).

---

## 25. 🟡 `POST /personnes/assure` takes a profession label, not an id

`GET /professions` is a proper referential: 354 rows, unique ids, unique labels,
none empty (verified live 2026-09-16). The souscription payload, however, carries
`profession` as a free **string**, so the app now sends the label picked from
that referential.

**Frontend state.** The traveller forms used a free-text input; a typo produced a
value the backend could not match to anything. They are now a searchable picker
fed by `/professions`, so only referential values are sent. The rows arrive in
insertion order and are sorted client-side.

**Needed** (quality, not blocking): accept `professionId` on
`POST /personnes/assure`, which would remove the label round-trip and any
ambiguity if two labels are ever normalised differently. Sorting the referential
server-side would also let the client drop its own sort.

---

## 26. 🟡 `POST /members/validation` — the `declare` flag is not read

Wired on 2026-09-16: the "Compléter profil santé" wizard used to clear its draft
and jump to its success screen **without sending anything**, so five steps of
input reached nobody. It now persists the RIB (`PUT /members`) and files the
declaration.

The endpoint works, and answers
`200 { message: "Merci d'avoir valider vos informations. Prière de télécharger
votre bulletin d'adhésion et de le charger après l'avoir signé." }`. It stamps
`dateBulletin` on the record; `validation` stays `0`, presumably until the
signed bulletin comes back.

**Two points to confirm:**

- `declare: false` answers the **same success message** and has the **same
  effect** as `declare: true` (both verified live). Either the flag means
  something and should be honoured, or it should leave the contract.
- The message tells the user to download their bulletin d'adhésion — but
  `GET /members/bulletinAdhesion` answers `500` and hangs past 12 s (§P3.10),
  and `POST /members/addPieceJointe` cannot be called at all (§22). **The step
  the success message asks for is, end to end, impossible today.**

---

## 27. 🔴 The Santé avenant endpoints refuse every payload

Two flows with complete UI in the app — renewal (formula → members → personalize
→ promo → consent → payment) and adjonction (form → payment → success) — have
nothing to call.

**Renewal** — `POST /avenants/{contratId}/renouvellements/sante/devis` answers
`409 "problem devis Renouvellement Sante"` to every payload (see §24).

**Adjonction** — `POST /avenants/{contratId}/adjonctions/sante/devis` is worse:
it **crashes on its own error path**. The upstream service answers 409, and the
gateway cannot deserialize it:

```
500 { "status": false, "error": 500, "devis": null,
      "errorMessage": "Cannot deserialize value of type `int` from String \"409 :\"
        … (through reference chain: DevisAdjonctionSanteResponseDTO[\"error\"])" }
```

So `DevisAdjonctionSanteResponseDTO.error` is typed `int` while the upstream
sends the string `"409 :"`. The real business rejection is hidden behind a
serialization bug in the gateway's own error mapping.

**Needed.** Fix the error DTO so the upstream status surfaces, then tell us what
the 409 actually means — both quote endpoints are refusing for a reason nothing
in the response states.

---

## 28. ⬜ Two endpoints exist with no screen behind them

Neither is wiring; both would be new features. Recorded so the surface is not
forgotten.

| Endpoint                                            | Contract                                            | Note                                                                                                                          |
| --------------------------------------------------- | --------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------- |
| `POST /refunds/{refundId}/createRefundCancellation` | the **whole** `AddRemboursementDto` multipart again | Cancelling a claim by resending it in full is an odd contract — a reason, or nothing, would be expected. No screen offers it. |
| `POST /voyages/{contratId}/report`                  | `{ dateDebut, modifyDevis }`                        | Postponing a trip. Simple contract, no screen.                                                                                |

---

## 29. 🔴 `confirmPayment` denies a payment the contract says was made

**The card leg finally ran end to end on 2026-09-16**, with the CIB test cards
the client sent on 09/09 and the `khaddoucheTest@amana.dz` account:

| Step                                                      | Result                                                                                                             |
| --------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------ |
| `POST /modePaiement/CIB` (contrat 11872, quittance 12935) | `200`, **real** `formUrl` on `test.satim.dz` + `orderId`                                                           |
| Satim page                                                | loads, 3 703,08 DZD, merchant "Sofiane Belghache / chic-dorient.com"                                               |
| Card 6280581110007215 · 01/2027 · CVV 373                 | accepted                                                                                                           |
| 3-D Secure password `123456`                              | accepted                                                                                                           |
| Redirect                                                  | `https://site-test.amana.dz/Confirmation?lang=fr&orderId=…`                                                        |
| `GET /contrats/11872`                                     | **`ENCAISSE`**, `codeReference` `DEVIS 11872` → **`E-AVA 20260000817`**, encaissement row with `modepaiementid: 2` |
| `GET /modePaiement/confirmPayment/{orderId}`              | **`{"valid": false, … every field null, "amount": 0}`**                                                            |

So the money moved, the contract was issued — and **the endpoint the app asks
answers "not valid"**. Called twice, several minutes apart: same answer.

**Impact.** Left as-is the app shows a failure screen to a customer who has just
paid, and invites them to pay again. This is the worst failure mode in the whole
funnel.

**Frontend workaround (shipped).** `useConfirmCardPayment` now re-reads
`GET /contrats/{contratId}` whenever the gateway denies, and treats
`ENCAISSE` / `ACTIF` / `PAYE` as settled. Same principle as the V8b guard on
souscription: the contract is what bills, so the contract decides.

**Needed.** Make `confirmPayment` report a settled payment as valid — it is the
only endpoint that can also carry the gateway's own detail (amount, response
code), which the fallback cannot.

---

## 30. 🟠 The payment page does not return to the app

`useCardPaymentFlow` opens the Satim page with
`WebBrowser.openAuthSessionAsync(formUrl, Linking.createURL("payment-return"))`,
which expects the page to redirect to the app's deep link.

Satim redirects to **`https://site-test.amana.dz/Confirmation?lang=fr&orderId=…`**
— a web page (verified 2026-09-16). The in-app browser therefore lands on the
Amana web confirmation and **stays there**: the session ends only when the
customer closes it by hand, and the app has no signal that the payment finished.

**Needed.** Configure the merchant's `returnUrl` so it can carry the mobile deep
link (or accept one per transaction), so the browser closes itself and the app
knows immediately. Until then the app relies on the contract re-read of §29.

> Side note: the merchant on the payment page is still **"Sofiane Belghache /
> chic-dorient.com"** — the third-party test instance flagged on 2026-07-26. It
> works with the supplied cards, but a customer-facing page naming another
> company cannot ship.

---

## 31. 🔴 "Compléter mon profil santé" — three of the five steps cannot be called

The backend described this rubric on **2026-08-05** as deliberately one API per
step, rather than a single call, so that each stage is persisted before the
bulletin is generated. That design is sound. Checked step by step against the
live API on 2026-09-16, here is what actually answers:

| Étape (message du 05/08)           | Endpoint annoncé                | Réalité                                                                           |
| ---------------------------------- | ------------------------------- | --------------------------------------------------------------------------------- |
| Infos personnelles + état de santé | `PUT /members`                  | ✅ **branché** — mais le contrat réel diffère du spec sur trois points (§7)       |
| Personnes à charge                 | `POST /ayantDroits`             | 🔴 **`500 INTERNAL_SERVER_ERROR` nu**, sans le moindre détail                     |
| Bénéficiaires en cas de décès      | `POST /beneficiaires`           | ⚠️ **ce chemin répond 404** — le bon est `/beneficiaries`, et il fonctionne (§16) |
| Téléchargement du bulletin         | `GET /members/bulletinAdhesion` | 🔴 `500`, puis plus de réponse au-delà de 12 s (P3.10)                            |
| Pièces justificatives              | « en cours d'amélioration »     | 🔴 toujours indécouvrable (§22)                                                   |

**`POST /ayantDroits` is the new blocker.** It refuses JSON
("Content-Type 'application/json' is not supported", so multipart it is), then
answers a **bare 500 with no message at all** whatever parts are sent — name,
first name, birth date in three formats, sex, relationship, with and without a
photo. Nothing is created. It is less diagnosable than `addPieceJointe` (§22),
which at least says "The given data was invalid".

**Frontend state.** The wizard now honours the two steps that work: it persists
the identity and questionnaire (`PUT /members`), creates the beneficiaries
(`POST /beneficiaries`) and files the declaration (`POST /members/validation`).
**The dependants it collects stay in the local draft** — there is nothing to
send them to.

**Needed**, in order:

1. The parts `POST /ayantDroits` expects — one working `curl` would do — or a
   validator that names the missing fields.
2. Correct the path given on 05/08: `/beneficiaires` does not exist.
3. `GET /members/bulletinAdhesion`, which the validation step's own success
   message tells the customer to use.

---

## Verification

```bash
BASE="https://gateway.amana.dz/MOBILE/api/v1"
JWT=$(curl -s -X POST "$BASE/auth/login" -H 'Content-Type: application/json' \
  -d '{"username":"test-chahinaz@amana.dz","password":"<mot de passe>"}' \
  | python3 -c 'import sys,json;print(json.load(sys.stdin)["jwt"])')

# Every read this branch wired, with the exact params the hooks send — all 200:
for u in "/a-propos-amana/sos" "/documents" "/notifications?page=0&size=50" \
         "/actes/groups" "/garanties/EPVD" "/ayantDroits?page=0" "/beneficiaries?page=1"; do
  printf "%-36s " "$u"
  curl -s -o /dev/null -w "%{http_code}\n" "$BASE$u" -H "Authorization: Bearer $JWT"
done
```

Write endpoints **were** exercised on 2026-09-16 — see §20 for what that left
behind. The three destructive ones (dependant removal, contract cancellation,
beneficiary deletion) were probed with non-existent identifiers only, so their
happy path is still unverified and must be run once on a disposable account
before release.

---

## 32. 🔴 `GET /documents` answers 500 for a subscriber who is not a health member

Found on 2026-09-16 by running the E2E flows, not by reading the spec.

```bash
curl -s "$BASE/documents" -H "Authorization: Bearer $JWT_khaddouche"
# 500 {"message":"Error fetching data: 404 Not Found: \"{\"status\":404,
#      \"message\":\"cet adherent n'existe pas\",
#      \"path\":\"api/bulletinV2DocumentUtil/1\"}\""}
```

The same call with a health member (`test-chahinaz`) returns `200` and the
list of regulatory documents.

**Why this is wrong.** `/documents` serves a catalogue of _public_ regulatory
texts — CSA rulings, CTRF instructions. It has nothing to do with health
membership, yet it is routed through `bulletinV2DocumentUtil/{adherentId}`
and dies when that lookup misses. Every subscriber who never took a health
policy — the majority — gets a dead screen where the law requires a document
to be available.

**Expected.** Serve the catalogue to any authenticated subscriber. If a
member-specific section genuinely exists, return the common part and omit the
rest rather than failing the whole call. And return the upstream `404` as a
`404`, not wrapped in a `500`.

**Related but distinct** from P0-H: that one asks for a downloadable URL on
documents that _do_ come back. This one is the list itself failing.

---

## 33. 🔴 `khaddoucheTest` has no subscriber record, and nothing says so

**Correction of an earlier entry.** This section first claimed that `/auth/me`
never returns the subscriber's own identity. That was wrong, and the mistake
came from testing a single account.

`test-chahinaz` returns a complete record: `personneId`, a `souscripteur` block
with `datenaissance`, plus `adresse`, `wilaya`, `email`, `tel`.
`khaddoucheTest` returns **none of it** — no `personneId`, no `souscripteur`,
no address, no wilaya, no email. Only `nom`, `prenom` and a free-text
`adresse_user`.

```bash
curl -s "$BASE/auth/me" -H "Authorization: Bearer $JWT_khaddouche" | jq 'keys'
# ["adherentSanteId","adresse_user","alternProfileImage","checkuser",
#  "is_activited","login","nbr_tentatif","nom","prenom","userId","userSanteId","username"]
```

**Two consequences, both measured on 2026-09-16.**

1. **Subscription is impossible with that account.** `POST /souscriptions`
   needs `personneId`; without it every contract creation fails at the consent
   step. The customer fills in the whole funnel — quote, insured, consents —
   and is told only "Paiement non abouti", which is not what happened: nothing
   was ever created, and no payment was attempted.

2. **"Yes, I am the insured" prefills almost nothing**, because there is no
   record to read from.

**Expected, in order.** Say why: an account without a subscriber record should
be told so before it starts a funnel, not after it has filled one in — a `409`
naming the missing record would be enough for the app to explain it. Then
provision the record, or say explicitly that such accounts exist in production
and what the app should do with them.

**For the QA campaign**: the subscription parcours run on `test-chahinaz`.
`khaddoucheTest` stays the account for navigation and destructive operations.
