test(technique) : couvrir RG-3.x PHPUnit + capturer le contrat JSON (ERP-139) (#100)
Auto Tag Develop / tag (push) Successful in 11s

Ticket Lesstime #139 (M3 — Répertoire prestataires, position 1.9). DoD back avant le front : suite PHPUnit consolidée sur la matrice § 8.1 + captures JSON réelles dans la spec § 4.0.bis.

## Contenu
- **Fix réfs comptables** : `provider:read:accounting` ajouté sur `TvaMode`/`PaymentDelay`/`PaymentType`/`Bank` — sans ça elles sortaient en IRI nu dans le détail prestataire (réplique du fix ERP-92 du M2, piège #1 § 4.0.bis).
- **`ProviderSerializationContractTest`** (13 tests) : gating RIB/scalaires par omission, réfs compta en objet `{id,code,label}`, `isArchived`, embed categories/sites liste+détail, sous-collections, enveloppe AP4 ; `testDodReferenceJsonShape` dumpe le JSON réel (`PROVIDER_DOD_DUMP=1`).
- **`ProviderAuditTest`** (5 tests) : create/update/archive (`technique.Provider`), iban/bic dans le diff (`technique.ProviderRib`, pas dAuditIgnore), trace M2M `sites`.
- **`ProviderListTest`** étendu : `?pagination=false`, anti-N+1, filtre `?typeCode=PRESTATAIRE`.
- **`ProviderRbacGatingTest`** étendu : restauration en conflit de nom → 409 (RG-3.14).
- **`ProviderFixtures`** (§ 8.4) : démo idempotente (complet VIREMENT+banque+RIB, LCR+RIB, CHEQUE multi-cat, minimal, archivé) répartie sur sites 86/17/82 ; skip en env `test`.
- Helper `seedCompleteProvider` ; spec § 4.0.bis : gabarits remplacés par les captures réelles (liste + détail avec/sans accounting.view).

## Vérifications
- `make php-cs-fixer-allow-risky` → 0 fichier
- `make test` → OK, 677 tests, 3328 assertions (garde-fous globaux verts)

## Notes
- MR stackée sur ERP-138 (base = sa branche).
- Fixtures démo exercées en dev via `make fixtures` (autowiring vérifié).

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #100
This commit was merged in pull request #100.
This commit is contained in:
2026-06-12 14:44:43 +00:00
parent d5462bcf42
commit 3fe0f676f6
41 changed files with 5725 additions and 100 deletions
+138 -52
View File
@@ -176,7 +176,7 @@ Anti-N+1 (le code fera foi) : le `DoctrineProviderRepository` ne fetch-joine PAS
- **Filtre LISTE** (`ProviderProvider` ou query extension dédiée `ProviderSiteScopeExtension`) : si l'user **n'a pas** `sites.bypass_scope` ET que `CurrentSiteProvider::get()` retourne un site → ne renvoyer que les prestataires dont `provider.sites` **contient** le `currentSite` (jointure `provider_site` + `WHERE site = :currentSite`). Si l'user a `bypass_scope` (Admin, profils consolidation) → aucun filtre (tous sites). Si `currentSite = null` (mode dégradé / module Sites off) → aligné `site-aware.md § 5` (no-op lecture, à documenter).
- **Filtre DÉTAIL** (`Get`) : un user sans `bypass_scope` qui demande un prestataire **hors de son site courant****404** (cohérence : ne pas révéler l'existence d'une ligne hors périmètre).
- **Écriture (décision Matthieu, 11/06)** : un user **sans** `bypass_scope` ne peut attacher **que les sites dont il dispose** (ses `user_site`) — sur le formulaire principal (`provider.sites`, RG-3.03) **comme** sur chaque adresse (`provider_address.sites`, RG-3.05). Tout site hors de ses `user_site` dans le payload → **422** sur `sites`. Un user `bypass_scope` (Admin) peut attacher n'importe quel site. Garde porté par le `ProviderProcessor` (POST + PATCH + sous-ressource adresses).
- **Cohérence sous-ressources** (`/providers/{id}/...`) : le détail étant déjà gardé en 404 hors périmètre, les sous-ressources héritent du garde-fou parent (cf. `site-aware.md § 6.1`).
- **Cohérence sous-ressources** (Contacts / Adresses / RIB) : le cloisonnement du parent **n'est PAS hérité automatiquement** — les opérations `Get` / `Patch` / `Delete` des sous-ressources passent par le provider Doctrine par défaut (et `SiteScopedQueryExtension` ne filtre que les `SiteAwareInterface`, ce que ces entités ne sont pas). Le garde-fou est donc posé **explicitement** : (a) en lecture/édition/suppression via le provider décoré `ProviderSubResourceItemProvider` (un parent hors périmètre → **404**) ; (b) en création (`POST /providers/{id}/...`) via `ProviderSiteScopeChecker::assertInScope` dans chaque processor (parent hors périmètre → **404**). La décision de scope est centralisée dans `ProviderSiteScopeChecker` (source unique partagée avec `ProviderProvider`). ⚠️ Ne pas retirer ces gardes en les croyant redondants : sans eux, un user cloisonné peut lire l'IBAN/BIC d'un RIB d'un autre site.
> **Conséquence RBAC** : la colonne « Consultation » du docx (« Tout » vs « son site uniquement ») se réalise **par `sites.bypass_scope`**, pas par le code de rôle. Décision d'attribution par défaut (à acter au ticket RBAC) : `bypass_scope` aux profils Admin (auto) + Bureau + Compta + Commerciale (ils voient « Tout » d'après le docx) ; **Usine ne l'a pas** → cloisonné à son site. Si MALIO préfère que Bureau/Commerciale soient aussi cloisonnés, il suffit de ne pas leur donner `bypass_scope` — **aucun code à changer** (c'est l'intérêt de piloter par user/permission et non par rôle).
@@ -624,67 +624,153 @@ Même pattern que les jumelles `Supplier*` (`#[Auditable]`, `TimestampableBlamab
> 1. Réfs comptables (`tvaMode`/`paymentDelay`/`paymentType`/`bank`) : doivent sortir en **objet `{id, code, label}`**, pas en IRI nu → vérifier que les entités partagées portent bien le groupe `provider:read:accounting` (sinon les annoter, comme le fix ERP-92 l'a fait pour `supplier:read:accounting`).
> 2. Gating compta par **omission de clé** : pour un user sans `accounting.view`, les clés `siren`/`tvaMode`/`ribs`/… sont **absentes** (pas `null`).
`GET /api/providers` (liste, ADMIN — un membre, forme attendue) :
> **✅ Capturé sur l'API réelle (ERP-139)** via `ProviderSerializationContractTest::testDodReferenceJsonShape` (`PROVIDER_DOD_DUMP=1`). Les `id`/`companyName`/noms de catégorie ci-dessous proviennent du prestataire seedé par le test (`seedCompleteProvider`) ; la **forme** (clés, embed, gating) est le contrat réel à respecter côté front.
`GET /api/providers` (liste, ADMIN avec `accounting.view` — un membre, capture réelle) :
```json
{
"@context": "/api/contexts/Provider",
"@id": "/api/providers",
"@type": "Collection",
"totalItems": 1,
"member": [
{
"@id": "/api/providers/1", "@type": "Provider", "id": 1,
"companyName": "MAINTENANCE PRO SAS",
"categories": [
{"@type": "Category", "@id": "/api/categories/300", "id": 300, "name": "Maintenance industrielle", "code": "MAINTENANCE",
"categoryType": {"@id": "/api/category_types/3", "@type": "CategoryType", "id": 3, "code": "PRESTATAIRE", "label": "Prestataire"}}
],
"sites": [
{"@type": "Site", "@id": "/api/sites/87", "id": 87, "name": "Chatellerault", "postalCode": "86100", "city": "Châtellerault", "color": "#056CF2"}
],
"siren": "987654321", "accountNumber": "P0001",
"tvaMode": {"@id": "/api/tva_modes/30", "@type": "TvaMode", "id": 30, "code": "FRANCE_VENTES", "label": "France (ventes)"},
"paymentType": {"@id": "/api/payment_types/14", "@type": "PaymentType", "id": 14, "code": "LCR", "label": "LCR"},
"ribs": [
{"@id": "/api/provider_ribs/1", "@type": "ProviderRib", "id": 1, "label": "Compte principal", "bic": "BNPAFRPPXXX", "iban": "FR1420041010050500013M02606"}
],
"updatedAt": "2026-06-11T10:00:00+02:00",
"isArchived": false
}
],
"view": {"@id": "/api/providers", "@type": "PartialCollectionView"}
"@context": "/api/contexts/Provider",
"@id": "/api/providers",
"@type": "Collection",
"totalItems": 1,
"member": [
{
"@id": "/api/providers/572",
"@type": "Provider",
"id": 572,
"companyName": "DOD21AADC 0E3CCE",
"categories": [
{
"@type": "Category",
"@id": "/api/categories/3006",
"id": 3006,
"name": "test_prov_cat_nettoyage",
"code": "NETTOYAGE",
"categoryTypes": [
{"@id": "/api/category_types/586", "@type": "CategoryType", "id": 586, "code": "PRESTATAIRE", "label": "Prestataire"}
],
"createdAt": "2026-06-12T15:17:29+02:00",
"updatedAt": "2026-06-12T15:17:29+02:00"
}
],
"sites": [
{"@type": "Site", "@id": "/api/sites/28", "id": 28, "name": "Chatellerault", "street": "14 All. d'Argenson", "postalCode": "86100", "city": "Châtellerault", "color": "#056CF2", "createdAt": "2026-06-12T10:51:22+02:00", "updatedAt": "2026-06-12T10:51:22+02:00", "fullAddress": "14 All. d'Argenson\n86100 Châtellerault"},
{"@type": "Site", "@id": "/api/sites/29", "id": 29, "name": "Saint-Jean", "street": "Z i", "postalCode": "17400", "city": "Fontenet", "color": "#F3CB00", "createdAt": "2026-06-12T10:51:22+02:00", "updatedAt": "2026-06-12T10:51:22+02:00", "fullAddress": "Z i\n17400 Fontenet"}
],
"siren": "987654321",
"accountNumber": "P0001",
"tvaMode": {"@id": "/api/tva_modes/13", "@type": "TvaMode", "id": 13, "code": "FRANCE_VENTES", "label": "France (ventes)"},
"nTva": "FR00987654321",
"paymentDelay": {"@id": "/api/payment_delays/8", "@type": "PaymentDelay", "id": 8, "code": "J30", "label": "30 jours"},
"paymentType": {"@id": "/api/payment_types/10", "@type": "PaymentType", "id": 10, "code": "LCR", "label": "LCR"},
"ribs": [
{"@id": "/api/provider_ribs/60", "@type": "ProviderRib", "id": 60, "label": "Compte principal", "bic": "BNPAFRPPXXX", "iban": "FR1420041010050500013M02606", "createdAt": "2026-06-12T15:17:29+02:00", "updatedAt": "2026-06-12T15:17:29+02:00"}
],
"createdAt": "2026-06-12T15:17:29+02:00",
"updatedAt": "2026-06-12T15:17:29+02:00",
"isArchived": false
}
],
"view": {"@id": "/api/providers?search=DoD21aadc", "@type": "PartialCollectionView"}
}
```
> Les prestataires archivés sont **exclus** du `totalItems` (RG-3.16). Pour la **Commerciale** (sans `accounting.view`), `siren`/`tvaMode`/`paymentType`/`ribs` **disparaissent** de chaque membre.
> Les `sites[]` de la liste sont la **relation directe** `provider.sites` (formulaire principal — RG-3.03), objet `Site` entier (pas un IRI nu). Les catégories embarquent `code` + `name`. Les prestataires archivés sont **exclus** du `totalItems` (RG-3.16). Pour un profil **sans `accounting.view`** (ex. Commerciale), `siren`/`accountNumber`/`tvaMode`/`nTva`/`paymentDelay`/`paymentType`/`bank`/`ribs` **disparaissent** de chaque membre (gating par omission — cf. détail restreint ci-dessous).
`GET /api/providers/{id}` (détail — user avec `accounting.view`, forme attendue) :
`GET /api/providers/{id}` (détail — user **avec** `accounting.view`, capture réelle) :
```json
{
"@id": "/api/providers/1", "@type": "Provider", "id": 1,
"companyName": "MAINTENANCE PRO SAS",
"categories": [{"@type": "Category", "@id": "/api/categories/300", "id": 300, "name": "Maintenance industrielle", "code": "MAINTENANCE"}],
"sites": [{"@type": "Site", "@id": "/api/sites/87", "id": 87, "name": "Chatellerault", "postalCode": "86100", "city": "Châtellerault", "color": "#056CF2"}],
"siren": "987654321", "accountNumber": "P0001",
"tvaMode": {"@id": "/api/tva_modes/30", "@type": "TvaMode", "id": 30, "code": "FRANCE_VENTES", "label": "France (ventes)"},
"nTva": "FR00987654321",
"paymentDelay": {"@id": "/api/payment_delays/11", "@type": "PaymentDelay", "id": 11, "code": "J30", "label": "30 jours"},
"paymentType": {"@id": "/api/payment_types/14", "@type": "PaymentType", "id": 14, "code": "LCR", "label": "LCR"},
"contacts": [
{"@id": "/api/provider_contacts/1", "@type": "ProviderContact", "id": 1, "firstName": "Marie", "lastName": "Martin", "jobTitle": "Responsable", "phonePrimary": "0612345678", "email": "marie.martin@seed.test"}
],
"addresses": [
{"@id": "/api/provider_addresses/1", "@type": "ProviderAddress", "id": 1, "country": "France", "postalCode": "86000", "city": "Poitiers", "street": "12 rue des Acacias",
"sites": [{"@type": "Site", "@id": "/api/sites/87", "id": 87, "name": "Chatellerault", "postalCode": "86100", "city": "Châtellerault", "color": "#056CF2"}],
"contacts": [{"@id": "/api/provider_contacts/1", "@type": "ProviderContact", "id": 1, "firstName": "Marie", "lastName": "Martin"}],
"categories": [{"@type": "Category", "@id": "/api/categories/300", "id": 300, "name": "Maintenance industrielle", "code": "MAINTENANCE"}]}
],
"ribs": [{"@id": "/api/provider_ribs/1", "@type": "ProviderRib", "id": 1, "label": "Compte principal", "bic": "BNPAFRPPXXX", "iban": "FR1420041010050500013M02606"}],
"isArchived": false
"@context": "/api/contexts/Provider",
"@id": "/api/providers/572",
"@type": "Provider",
"id": 572,
"companyName": "DOD21AADC 0E3CCE",
"categories": [
{"@type": "Category", "@id": "/api/categories/3006", "id": 3006, "name": "test_prov_cat_nettoyage", "code": "NETTOYAGE", "categoryTypes": [{"@id": "/api/category_types/586", "@type": "CategoryType", "id": 586, "code": "PRESTATAIRE", "label": "Prestataire"}], "createdAt": "2026-06-12T15:17:29+02:00", "updatedAt": "2026-06-12T15:17:29+02:00"}
],
"sites": [
{"@type": "Site", "@id": "/api/sites/28", "id": 28, "name": "Chatellerault", "street": "14 All. d'Argenson", "postalCode": "86100", "city": "Châtellerault", "color": "#056CF2", "createdAt": "2026-06-12T10:51:22+02:00", "updatedAt": "2026-06-12T10:51:22+02:00", "fullAddress": "14 All. d'Argenson\n86100 Châtellerault"},
{"@type": "Site", "@id": "/api/sites/29", "id": 29, "name": "Saint-Jean", "street": "Z i", "postalCode": "17400", "city": "Fontenet", "color": "#F3CB00", "createdAt": "2026-06-12T10:51:22+02:00", "updatedAt": "2026-06-12T10:51:22+02:00", "fullAddress": "Z i\n17400 Fontenet"}
],
"siren": "987654321",
"accountNumber": "P0001",
"tvaMode": {"@id": "/api/tva_modes/13", "@type": "TvaMode", "id": 13, "code": "FRANCE_VENTES", "label": "France (ventes)"},
"nTva": "FR00987654321",
"paymentDelay": {"@id": "/api/payment_delays/8", "@type": "PaymentDelay", "id": 8, "code": "J30", "label": "30 jours"},
"paymentType": {"@id": "/api/payment_types/10", "@type": "PaymentType", "id": 10, "code": "LCR", "label": "LCR"},
"contacts": [
{"@id": "/api/provider_contacts/50", "@type": "ProviderContact", "id": 50, "firstName": "Marie", "lastName": "Martin", "jobTitle": "Responsable", "phonePrimary": "0612345678", "email": "marie.martin@seed.test", "createdAt": "2026-06-12T15:17:29+02:00", "updatedAt": "2026-06-12T15:17:29+02:00"}
],
"addresses": [
{
"@id": "/api/provider_addresses/35", "@type": "ProviderAddress", "id": 35,
"country": "France", "postalCode": "86000", "city": "Poitiers", "street": "12 rue des Acacias",
"sites": [
{"@type": "Site", "@id": "/api/sites/28", "id": 28, "name": "Chatellerault", "street": "14 All. d'Argenson", "postalCode": "86100", "city": "Châtellerault", "color": "#056CF2", "createdAt": "2026-06-12T10:51:22+02:00", "updatedAt": "2026-06-12T10:51:22+02:00", "fullAddress": "14 All. d'Argenson\n86100 Châtellerault"},
{"@type": "Site", "@id": "/api/sites/29", "id": 29, "name": "Saint-Jean", "street": "Z i", "postalCode": "17400", "city": "Fontenet", "color": "#F3CB00", "createdAt": "2026-06-12T10:51:22+02:00", "updatedAt": "2026-06-12T10:51:22+02:00", "fullAddress": "Z i\n17400 Fontenet"}
],
"contacts": [
{"@id": "/api/provider_contacts/50", "@type": "ProviderContact", "id": 50, "firstName": "Marie", "lastName": "Martin", "jobTitle": "Responsable", "phonePrimary": "0612345678", "email": "marie.martin@seed.test", "createdAt": "2026-06-12T15:17:29+02:00", "updatedAt": "2026-06-12T15:17:29+02:00"}
],
"categories": [
{"@type": "Category", "@id": "/api/categories/3006", "id": 3006, "name": "test_prov_cat_nettoyage", "code": "NETTOYAGE", "categoryTypes": [{"@id": "/api/category_types/586", "@type": "CategoryType", "id": 586, "code": "PRESTATAIRE", "label": "Prestataire"}], "createdAt": "2026-06-12T15:17:29+02:00", "updatedAt": "2026-06-12T15:17:29+02:00"}
],
"createdAt": "2026-06-12T15:17:29+02:00", "updatedAt": "2026-06-12T15:17:29+02:00"
}
],
"ribs": [
{"@id": "/api/provider_ribs/60", "@type": "ProviderRib", "id": 60, "label": "Compte principal", "bic": "BNPAFRPPXXX", "iban": "FR1420041010050500013M02606", "createdAt": "2026-06-12T15:17:29+02:00", "updatedAt": "2026-06-12T15:17:29+02:00"}
],
"createdAt": "2026-06-12T15:17:29+02:00",
"updatedAt": "2026-06-12T15:17:29+02:00",
"isArchived": false
}
```
> Pour un user **sans** `accounting.view` (ex. Commerciale) : les clés `siren`, `accountNumber`, `tvaMode`, `nTva`, `paymentDelay`, `paymentType`, `bank`, `ribs` **sont absentes** (gating par omission — à confirmer par test).
`GET /api/providers/{id}` (même prestataire, user **sans** `accounting.view` — capture réelle) :
```json
{
"@context": "/api/contexts/Provider",
"@id": "/api/providers/572",
"@type": "Provider",
"id": 572,
"companyName": "DOD21AADC 0E3CCE",
"categories": [
{"@type": "Category", "@id": "/api/categories/3006", "id": 3006, "name": "test_prov_cat_nettoyage", "code": "NETTOYAGE", "categoryTypes": [{"@id": "/api/category_types/586", "@type": "CategoryType", "id": 586, "code": "PRESTATAIRE", "label": "Prestataire"}], "createdAt": "2026-06-12T15:17:29+02:00", "updatedAt": "2026-06-12T15:17:29+02:00"}
],
"sites": [
{"@type": "Site", "@id": "/api/sites/28", "id": 28, "name": "Chatellerault", "street": "14 All. d'Argenson", "postalCode": "86100", "city": "Châtellerault", "color": "#056CF2", "createdAt": "2026-06-12T10:51:22+02:00", "updatedAt": "2026-06-12T10:51:22+02:00", "fullAddress": "14 All. d'Argenson\n86100 Châtellerault"},
{"@type": "Site", "@id": "/api/sites/29", "id": 29, "name": "Saint-Jean", "street": "Z i", "postalCode": "17400", "city": "Fontenet", "color": "#F3CB00", "createdAt": "2026-06-12T10:51:22+02:00", "updatedAt": "2026-06-12T10:51:22+02:00", "fullAddress": "Z i\n17400 Fontenet"}
],
"contacts": [
{"@id": "/api/provider_contacts/50", "@type": "ProviderContact", "id": 50, "firstName": "Marie", "lastName": "Martin", "jobTitle": "Responsable", "phonePrimary": "0612345678", "email": "marie.martin@seed.test", "createdAt": "2026-06-12T15:17:29+02:00", "updatedAt": "2026-06-12T15:17:29+02:00"}
],
"addresses": [
{
"@id": "/api/provider_addresses/35", "@type": "ProviderAddress", "id": 35,
"country": "France", "postalCode": "86000", "city": "Poitiers", "street": "12 rue des Acacias",
"sites": [
{"@type": "Site", "@id": "/api/sites/28", "id": 28, "name": "Chatellerault", "street": "14 All. d'Argenson", "postalCode": "86100", "city": "Châtellerault", "color": "#056CF2", "createdAt": "2026-06-12T10:51:22+02:00", "updatedAt": "2026-06-12T10:51:22+02:00", "fullAddress": "14 All. d'Argenson\n86100 Châtellerault"},
{"@type": "Site", "@id": "/api/sites/29", "id": 29, "name": "Saint-Jean", "street": "Z i", "postalCode": "17400", "city": "Fontenet", "color": "#F3CB00", "createdAt": "2026-06-12T10:51:22+02:00", "updatedAt": "2026-06-12T10:51:22+02:00", "fullAddress": "Z i\n17400 Fontenet"}
],
"contacts": [
{"@id": "/api/provider_contacts/50", "@type": "ProviderContact", "id": 50, "firstName": "Marie", "lastName": "Martin", "jobTitle": "Responsable", "phonePrimary": "0612345678", "email": "marie.martin@seed.test", "createdAt": "2026-06-12T15:17:29+02:00", "updatedAt": "2026-06-12T15:17:29+02:00"}
],
"categories": [
{"@type": "Category", "@id": "/api/categories/3006", "id": 3006, "name": "test_prov_cat_nettoyage", "code": "NETTOYAGE", "categoryTypes": [{"@id": "/api/category_types/586", "@type": "CategoryType", "id": 586, "code": "PRESTATAIRE", "label": "Prestataire"}], "createdAt": "2026-06-12T15:17:29+02:00", "updatedAt": "2026-06-12T15:17:29+02:00"}
],
"createdAt": "2026-06-12T15:17:29+02:00", "updatedAt": "2026-06-12T15:17:29+02:00"
}
],
"createdAt": "2026-06-12T15:17:29+02:00",
"updatedAt": "2026-06-12T15:17:29+02:00",
"isArchived": false
}
```
> **Gating par omission confirmé sur le JSON réel** : pour un user **sans** `accounting.view`, les clés `siren`, `accountNumber`, `tvaMode`, `nTva`, `paymentDelay`, `paymentType`, `bank` **et `ribs`** sont **absentes** (pas `null`). `isArchived`, `contacts[]`, `addresses[]` (avec `sites[]`/`contacts[]`/`categories[]`) restent exposés. Vérifié par `ProviderSerializationContractTest::testRibsAbsentForUserWithoutAccountingView` + `testAccountingScalarsGatedByOmission`.
>
> **Réfs comptables = objets embarqués `{id, code, label}`** (pas IRI nu) : le fix ERP-139 a ajouté `provider:read:accounting` sur `TvaMode`/`PaymentDelay`/`PaymentType`/`Bank` (réplique du fix ERP-92 du M2). Vérifié par `testAccountingReferentialsEmbedIdCodeLabel`.
### 4.1 `GET /api/providers` — Liste
@@ -923,7 +1009,7 @@ Cf. § 2.9 (matrice détaillée — identique à la matrice M2 transposée sur `
- [x] 3 maillons de sérialisation documentés pour chaque champ liste + détail (§ 4.0)
- [x] Décision embed vs GetCollection explicite et câblée (embed détail + sous-ressources write — § 3.3 / § 3.4 / § 4.5), **pas de POST-only**
- [ ] **Réponses JSON RÉELLES** à capturer (§ 4.0.bis) — gabarit posé, capture à faire au 1er ticket back (DoD avant front)
- [x] **Réponses JSON RÉELLES** capturées (§ 4.0.bis) — liste + détail (avec/sans `accounting.view`) collés depuis `ProviderSerializationContractTest` (ERP-139)
- [x] Matrice RBAC rôle × onglet + mode strict PATCH (§ 2.9 / RG-3.15)
- [x] Pagination (n°13), COMMENT ON COLUMN (n°12), Timestampable/Blamable, Audit + i18n, routes à plat : rappelés
- [x] Réutilisations M1/M2 identifiées (référentiels compta partagés, taxonomie code/type, filtre `?typeCode=`, `usePaginatedList`, blocs, archive, normalisation, `useAddressAutocomplete`)