Commit Graph

3 Commits

Author SHA1 Message Date
tristan 222338e5a4 fix(commercial) : validation onglet compta LCR + controle croise BIC/IBAN (ERP-118) (#78)
Auto Tag Develop / tag (push) Successful in 7s
## ERP-118 — Validation onglet Comptabilité (LCR / RIB)

### 1. Fix — 422 « Au moins un RIB est obligatoire pour le type de règlement LCR »

L'onglet Comptabilité envoyait le `PATCH /clients/{id}` des scalaires (`paymentType=LCR`) **avant** le `POST /clients/{id}/ribs`. Or le back valide RG-1.13 (LCR ⟹ ≥1 RIB persisté) sur ce PATCH, en lisant les RIB en base — vides à ce stade. Résultat : 422, et le `return` empêchait la création des RIB. Premier passage en LCR impossible (deadlock).

**Correctif :** inverser l'ordre — RIB d'abord, puis PATCH des scalaires.
- `new.vue` : `POST/PATCH RIB` → `PATCH scalaires`.
- `[id]/edit.vue` : ordre universel `CREATE/UPDATE RIB` → `PATCH scalaires` → `DELETE RIB retirés` (suppressions après le PATCH : le guard back n'autorise la suppression du dernier RIB qu'une fois quitté LCR). Corrige au passage un 409 latent sur le swap du dernier RIB en LCR.

### 2. Feat — contrôle croisé pays BIC/IBAN

`Assert\Bic(ibanPropertyPath: 'iban')` sur `ClientRib` et `SupplierRib` : le pays du BIC (positions 5-6) doit correspondre au pays de l'IBAN (positions 1-2). Un BIC et un IBAN valides isolément mais de pays différents → 422, violation portée par le champ `bic` avec message FR (`ibanMessage`), mappée inline côté front. Aucune modif front nécessaire.

### Tests

- Tests fonctionnels du mismatch (BIC DE + IBAN FR → 422 sur `propertyPath=bic`, message FR) côté client et fournisseur.
- Suite back complète au vert (garde-fou `EntityConstraintsHaveFrenchMessageTest` inclus), suite front Vitest au vert.

### Points d'attention

- **Durcissement de RG** (cross-check BIC/IBAN) hors spec initiale : des RIB existants avec BIC/IBAN de pays différents deviendraient non modifiables sans correction.
- L'orchestration de submit n'est pas couverte par un test unitaire (pas d'infra de test composant sur ces écrans) — vérification golden path recommandée.

Reviewed-on: #78
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-09 08:44:12 +00:00
matthieu 145d4362db feat(commercial) : sous-ressources M2 fournisseurs (contacts/adresses/ribs) (ERP-88) (#67)
Auto Tag Develop / tag (push) Successful in 7s
## ERP-88 — Sous-ressources M2 (contacts / adresses / ribs)

Étape 4/7 du pipeline M2. Dépend de #86 (entités) et #87 (Provider/Processor). Bloque #92.

### Contenu
Opérations API Platform + Processors d'écriture des sous-collections du fournisseur (POST/PATCH/DELETE + GET unitaire).

**SupplierContactProcessor**
- Rattachement au fournisseur parent (404 si absent).
- Normalisation serveur RG-2.12 (Title Case nom/prénom, téléphones chiffres seuls, email lowercase).
- RG-2.04 : firstName **ou** lastName obligatoire (422 sur `firstName`).
- DELETE libre (RG-2.13 front-driven : collection peut rester vide côté back).

**SupplierAddressProcessor**
- Rattachement au fournisseur parent.
- RG-2.05 (CP `^[0-9]{4,5}$`), RG-2.06 (≥1 site), RG-2.09 (type d'adresse) portées par les contraintes d'entité (ERP-86).
- RG-2.10 (catégorie de type FOURNISSEUR) ajoutée via `Assert\Callback validateCategoryType` (propertyPath=`categories`).

**SupplierRibProcessor**
- Rattachement au fournisseur parent.
- RG-2.08 : refus du DELETE du dernier RIB quand `paymentType.code = LCR` → **409**.

### Security différenciée
| Sous-ressource | Écriture | Lecture |
|---|---|---|
| contacts / adresses | `commercial.suppliers.manage` | `commercial.suppliers.view` |
| ribs | `commercial.suppliers.accounting.manage` | `commercial.suppliers.accounting.view` |

POST en `read:false` (parent rattaché manuellement) — parade NonUniqueResult héritée du M1. Messages FR (ERP-107) + `violations[].propertyPath` aligné (ERP-101).

### Vérifications
- `make php-cs-fixer-allow-risky` : 0 fichier à corriger
- `make test` : 483 tests OK
- `debug:router` : 12 routes générées (4 par sous-ressource)

### Hors périmètre (tickets suivants)
- Déclaration RBAC `commercial.suppliers.*` dans `CommercialModule` (#7) — sans elle, l'accès reste 403.
- Tests fonctionnels de la matrice RG (#8) — dépendent du RBAC + fixtures Supplier.

### Notes de review (non bloquantes, alignées M1)
- `position` des sous-collections non exposé à l'API (décision ERP-86, géré serveur).
- M2M `SupplierAddress.contacts` non vérifié same-supplier — comportement identique au M1 (ClientAddress), à traiter globalement si besoin.

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #67
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
2026-06-08 07:31:48 +00:00
matthieu 6a01067746 feat(commercial) : entités + repositories M2 fournisseurs (ERP-86) (#65)
Auto Tag Develop / tag (push) Successful in 7s
## ERP-86 — Entités + Repositories M2 Fournisseurs (étape 2/7)

PR **empilée sur ERP-85** (#64) : ne contient que le commit ERP-86. À merger après #64 (la base rebascule automatiquement au fil des merges de la chaîne #63#64 → develop).

Dépend de #64 (migration BDD). Bloque #87 (Provider + Processor) et suivants.

### Contenu

4 entités jumelles du M1 `Client*`, mapping ORM aligné **exactement** sur la migration ERP-85 (noms, types, longueurs, FK, M2M, index), **sans contact inline** (ERP-106) :

- **`Supplier`** — `#[Auditable]` + Timestampable/Blamable. Formulaire principal, onglet Information (+ `volumeForecast`, spécifique fournisseur), onglet Comptabilité (FK référentiels M1 partagés), archivage (`isArchived`/`archivedAt`), soft-delete préparé. Catégories M2M via `CategoryInterface` (règle n°1, pas d'import inter-module). Pas de `distributor`/`broker`.
- **`SupplierContact`** — onglet Contacts (RG-2.04 : `firstName` OU `lastName`).
- **`SupplierAddress`** — enum `addressType` (`PROSPECT`/`DEPART`/`RENDU` via `Assert\Choice`), `bennes`, `triageProvider` ; M2M sites/contacts/categories.
- **`SupplierRib`** — RIB, embed gaté comptable.
- **Repositories** : interfaces `Domain/Repository/` + impls `Infrastructure/Doctrine/`.

### Points clés

- **Contrat de sérialisation (RETEX M1, 3 maillons posés sur l'entité)** : read-groups sur les propriétés ; getters `isArchived()` / `isTriageProvider()` avec `#[Groups]` + `#[SerializedName('isX')]` (parade piège booléen n°3) ; embed `contacts`/`addresses` (`supplier:item:read`) et `ribs` (`supplier:read:accounting`). `getSites()` agrège/dédoublonne les `Site` des adresses (`name`/`postalCode`, pas de `code`).
- **Fetch-joins anti-N+1** dans le **repository de liste** : `hydrateListCollections()` en 2 passes (`categories`, puis `addresses.sites`) — évite le produit cartésien (pattern ERP-100). Filtres : recherche `companyName` + contacts liés (D1), `categoryCode`, `siteId`, archivage.
- **Pas d'`#[ApiResource]`** : Provider/Processor (gating accounting, archivage, mode strict) sont au ticket **ERP-87**. L'ajouter ici référencerait des classes inexistantes → boot/tests cassés. Les groupes de lecture/écriture sont déjà en place ; le `normalizationContext` viendra avec #87.
- **Validation FR (ERP-107)** : messages FR sur toutes les contraintes ; `Assert\Length(max)` calé sur les colonnes. Garde-fou `EntityConstraintsHaveFrenchMessageTest` étendu : `Assert\Choice` ajouté au mapping ; `addressType` et `postalCode` whitelistés du miroir Length (déjà bornés par Choice / Regex).
- Clés i18n `audit.entity.commercial_supplier*` ajoutées (garde-fou `AuditableEntitiesHaveI18nLabelTest`).

### Vérifications

- `make test` : **483/483 OK** (1965 assertions).
- `make php-cs-fixer-allow-risky` : 0 correction.
- `doctrine:schema:validate` : mapping correct (bruit d'index FK cosmétique identique au M1 `client`).

---------

Co-authored-by: admin malio <malio@yuno.malio.fr>
Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #65
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
2026-06-08 07:18:30 +00:00