# Cahier de test back — M1 Répertoire clients (ticket ERP-60 / #478)

Mapping **toutes les RG (§ 7) → test(s) PHPUnit**, à jour après ERP-60.

Légende source : `ERP-55` `ERP-56` `ERP-57` `ERP-58` = tests écrits par les wagons
précédents ; **`ERP-60`** = tests ajoutés par ce ticket (stratégie « combler les
trous, zéro duplication »).

## Stratégie

ERP-60 n'écrit QUE les tests des RG non déjà couvertes par la stack, et mappe ici
l'intégralité des RG (existantes + nouvelles + déléguées). Les tests dépendants
des **rôles métier** (matrice RBAC bureau/compta/commerciale/usine) sont
**délégués à ERP-74 (#493)** : ces rôles n'existent qu'après le
merge de la stack.

## Mapping RG → test

| RG | Intitulé | Test(s) | Source |
|----|----------|---------|--------|
| ~~RG-1.01~~ | _(supprimée V1 — refonte-contact)_ contact inline retiré du Client ; complétude couverte par RG-1.05 / RG-1.14 (`ClientContact`) | — | refonte-contact |
| ~~RG-1.02~~ | _(supprimée du Client V1)_ téléphones inline retirés du Client (testés sur `ClientContact`) | — | refonte-contact |
| RG-1.03 | distributor/broker exclusifs + type catégorie | `ClientApiTest::testPostWithDistributorAndBrokerReturns422` ; `::testPostDistributorReferencingNonDistributorReturns422` ; `::testPostValidDistributorReturns201` ; `ClientProcessorTest` (unit) | ERP-55 |
| ~~RG-1.04~~ | _(SUPPRIMÉE)_ Onglet Information désormais facultatif pour tous les rôles (validation de complétude retirée) | — | ERP-55 / ERP-74 |
| RG-1.05 | Contact : prénom OU nom → 422 (CHECK) | `ClientSubResourceApiTest::testPostContactWithoutNameReturns422` | ERP-57 |
| RG-1.06/07/08 | Adresse prospect exclusive de livraison/facturation → 422 (Assert\Callback + CHECK filet) | `ClientAddressTest::testProspectAddressCannotBeDelivery` ; `::testProspectAddressCannotBeBilling` | ERP-60 / **ERP-76** |
| RG-1.09 | Code postal `^[0-9]{4,5}$` → 422 | `ClientSubResourceApiTest::testPostAddressWithInvalidPostalCodeReturns422` | ERP-57 |
| RG-1.10 | ≥ 1 site sur adresse → 422 | `ClientSubResourceApiTest::testPostAddressWithoutSiteReturns422` | ERP-57 |
| RG-1.11 | billingEmail obligatoire ssi isBilling → 422 (Assert\Callback + CHECK filet) | `ClientAddressTest::testBillingAddressRequiresBillingEmail` ; `::testNonBillingAddressRejectsBillingEmail` | ERP-60 / **ERP-76** |
| RG-1.12 | Virement → banque obligatoire → 422 | `ClientProcessorTest::testVirementWithoutBankIsUnprocessable` ; `::testVirementWithBankPasses` (unit) | ERP-55 |
| RG-1.13 | LCR → ≥ 1 RIB ; DELETE dernier RIB en LCR → 409 | `ClientProcessorTest::testLcrWithoutRibIsUnprocessable` / `::testLcrWithRibPasses` (unit) ; `ClientSubResourceApiTest::testDeleteLastRibUnderLcrReturns409` / `::testDeleteRibNonLcrReturns204` | ERP-55 / ERP-57 |
| RG-1.14 | ≥ 1 bloc Contact pour finaliser l'onglet | **Front-driven (pas de state machine back).** Back voisin : `ClientSubResourceApiTest::testDeleteLastContactReturns409` | ERP-57 |
| RG-1.15 | ~~Unicité SIREN~~ supprimée (Q4) — SIREN partageable | `ClientUniquenessTest::testDuplicateSirenIsAllowed` ; `ClientMigrationTest::testNoSirenOrEmailUniqueIndex` | **ERP-60** |
| RG-1.16 | companyName unique (case-insensitive) parmi actifs → 409 | `ClientApiTest::testPostDuplicateCompanyNameReturns409` ; `ClientMigrationTest::testCompanyNameActivePartialIndexExistsExactlyOnce` | ERP-55 / **ERP-60** |
| RG-1.17 | ~~Unicité email~~ supprimée (Q4) — email partageable | `ClientUniquenessTest::testDuplicateEmailIsAllowed` ; `ClientMigrationTest::testNoSirenOrEmailUniqueIndex` | **ERP-60** |
| RG-1.18 | companyName upper-cased serveur | `ClientApiTest::testPostNormalizesTextFields` ; `ClientFieldNormalizerTest::testCompanyNameIsUppercased` (unit) | ERP-55 |
| RG-1.19 | firstName/lastName capitalize serveur | `ClientApiTest::testPostNormalizesTextFields` ; `ClientFieldNormalizerTest::testPersonNameIsTitleCased` (unit) ; `ClientSubResourceApiTest::testPostContactNormalizesFields` | ERP-55 / ERP-57 |
| RG-1.20 | Téléphones chiffres-seuls serveur | `ClientApiTest::testPostNormalizesTextFields` ; `ClientFieldNormalizerTest::testPhoneKeepsOnlyDigits` (unit) ; `ClientFormulaireMainTest::testPostPersistsSecondaryPhoneNormalized` (secondary) | ERP-55 / **ERP-60** |
| RG-1.21 | email lowercase serveur | `ClientApiTest::testPostNormalizesTextFields` ; `ClientFieldNormalizerTest::testEmailIsLowercased` (unit) ; `ClientSubResourceApiTest::testPostContactNormalizesFields` / `::testPostAddressNormalizesBillingEmail` | ERP-55 / ERP-57 |
| RG-1.22 | Archive : permission `archive` + archivedAt + aucun autre champ | `ClientApiTest::testPatchArchiveSetsArchivedAtThenRestore` ; `::testPatchArchiveWithOtherFieldReturns422` ; `ClientProcessorTest` (unit, gating archive) | ERP-55 |
| RG-1.23 | Restauration : archivedAt=null ; **409 si conflit d'unicité** | `ClientApiTest::testPatchArchiveSetsArchivedAtThenRestore` (cas nominal) ; **`ClientArchiveTest::testRestoreConflictReturns409`** (409 restauration, gap P1) | ERP-55 / **ERP-60** |
| RG-1.24 | Liste exclut les archivés par défaut | `ClientApiTest::testListSortedByCompanyNameAscAndExcludesArchived` | ERP-55 |
| RG-1.25 | `?includeArchived=true` inclut les archivés | `ClientApiTest::testListIncludeArchivedReturnsArchived` | ERP-55 |
| RG-1.26 | Tri par défaut companyName ASC | `ClientApiTest::testListSortedByCompanyNameAscAndExcludesArchived` | ERP-55 |
| RG-1.27 | Timestampable/Blamable : created* figés, updated* mis à jour | `ClientAuditTest::testCreatedFrozenAndUpdatedByReflectsModifier` | **ERP-60** |
| RG-1.28 | PATCH multi-groupes sans permission → 403 strict (tout le payload) | `ClientProcessorTest::testStrictMixWithAccountingFieldIsForbidden` / `::testAccountingFieldWithoutPermissionIsForbidden` (unit) ; **`ClientPatchStrictTest::testMixedGroupsPatchWithoutAccountingPermissionIsForbidden`** (fonctionnel) | ERP-55 / **ERP-60** |
| RG-1.29 | Catégorie d'adresse limitée aux types SECTEUR/AUTRE | **Filtrage LECTURE = front-driven** (SearchFilter `GET /api/categories?categoryType.code[]=…`). **Validation ÉCRITURE** : `ClientAddress::validateCategoryTypes` (Assert\Callback) rejette une catégorie DISTRIBUTEUR/COURTIER en 422 (violation `categories`). Tests : `ClientAddressTest::testAddressRejectsDistributorCategory` / `::testAddressRejectsBrokerCategory` / `::testAddressAcceptsSectorCategory` / `::testAddressAcceptsOtherCategory` | **ERP-76** |

## Couvertures transverses

| Sujet | Test(s) | Source |
|-------|---------|--------|
| Audit iban/bic présents dans le diff (pas d'`#[AuditIgnore]`) | `ClientAuditTest::testRibCreateAuditIncludesIbanAndBic` | **ERP-60** |
| Sécurité générique : 401 anonyme + 403 sans `commercial.clients.view` | `ClientSecurityTest` (collection + détail) ; `ClientExportControllerTest::testForbiddenWithoutClientsViewPermission` / `::testUnauthorizedWhenAnonymous` | **ERP-60** / ERP-58 |
| Migration : index partiel unique présent (1 seul), pas de siren/email unique | `ClientMigrationTest` | **ERP-60** |
| Référentiels comptables read-only (405 écriture, 401/403) | `ReferentialApiTest` | ERP-56 |
| Export XLSX (colonnes accounting selon permission, 401/403) | `ClientExportControllerTest` | ERP-58 |

## Délégué à ERP-74 (#493) — NE PAS faire dans ERP-60

- **Matrice RBAC différenciée** par rôle métier (Bureau / Compta / Commerciale /
  Usine) : 200/403 par verbe et par onglet selon le rôle.
- ~~**RG-1.04 fonctionnel**~~ : _(SUPPRIMÉE)_ l'onglet Information est désormais facultatif pour tous les rôles.
- Raison : ces rôles métier ne sont seedés qu'après le merge de la stack M1.

## Gaps & suivi

- ~~**RG-1.29 (validation écriture)**~~ — **résolu en ERP-76**. La validation
  d'écriture refuse désormais une catégorie de type `DISTRIBUTEUR`/`COURTIER` sur
  une `ClientAddress` (→ 422, violation `categories`) via l'Assert\Callback
  `ClientAddress::validateCategoryTypes`. Le filtrage de lecture reste
  front-driven (SearchFilter). Couvert par `ClientAddressTest`.
- ~~**Violations CHECK → statut HTTP**~~ — **résolu en ERP-76**. Les règles
  d'adresse RG-1.06/07/08/11 sont désormais rejetées en **422** par des
  Assert\Callback applicatifs (`validateProspectExclusivity` /
  `validateBillingEmailPresence`) qui s'exécutent AVANT la base. Les CHECK
  Postgres (`chk_client_address_prospect_exclusive` /
  `chk_client_address_billing_email`) restent en filet de sécurité. Les tests
  `ClientAddressTest` assertent maintenant le 422 explicite (et non plus ≥ 400).
