Commit Graph

91 Commits

Author SHA1 Message Date
tristan 391f383c4b fix : brouillon à contrepartie incomplète enregistrable sans erreur 500 (ERP-193)
Un brouillon dont le type de contrepartie est choisi sans son champ associé
(client/fournisseur null, ou libellé « Autre » vide) violait chk_wt_*_branch
et levait une 500 : le callback de cohérence RG-5.03 ne joue qu'au groupe
finalize, laissant passer l'incohérence à l'enregistrement du brouillon.

- back : WeighingTicketProcessor retire la contrepartie entière quand le champ
  de branche est absent (clearCounterparty) au lieu de persister un état
  incohérent. N'affecte que le brouillon (à la validation, le callback finalize
  lève déjà une 422 avant le processor).
- front : buildDraftPayload n'émet le type que si son champ associé est rempli ;
  la validation continue d'envoyer toujours le type pour la 422 métier.
- tests : 2 cas back (CLIENT sans client, AUTRE libellé vide) + 2 cas front.
2026-06-24 16:12:40 +02:00
tristan 9e2206a7d6 fix : DSD saisi conservé en pesée manuelle (ERP-193)
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 2m12s
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 3m45s
En pesée manuelle, le serveur incrémentait automatiquement le DSD et ignorait la
saisie de l'opérateur. Désormais l'opérateur saisit le poids ET le DSD (le numéro
du pont réellement utilisé), conservés tels quels — plus d'auto-incrément. Le
champ « Numéro de pesée » séparé (manualNumber) est supprimé : pour le client
c'est la même chose que le DSD. Pas de contrainte d'unicité sur le DSD (doublons
autorisés). Colonnes empty_manual_number/full_manual_number droppées.
2026-06-24 15:33:12 +02:00
tristan 31678cb716 feat(back) : export tickets de pesée — colonnes Fournisseur/Client/Autre + Statut (ERP-193)
Remplace les colonnes « Type contrepartie » + « Contrepartie » par 3 colonnes
mutuellement exclusives Fournisseur / Client / Autre (miroir de la liste), et
ajoute une colonne Statut (« En attente » / « Terminée »).
2026-06-24 15:15:25 +02:00
tristan 819ac5e608 feat : cycle de vie brouillon/validé du ticket de pesée (ERP-193)
Une pesée (bascule ou manuelle) s'enregistre désormais dès la validation de sa
modale, sans exiger la contrepartie ni l'immatriculation : le ticket naît
« brouillon » (status DRAFT, sans numéro). Le bouton « Valider » finalise quand
les 3 champs du haut (contrepartie + champ associé + immatriculation) ET les 2
pesées sont renseignés : attribution du numéro {siteCode}-TP-{NNNN} et passage
en VALIDATED, puis ouverture du bon de pesée PDF.

Back : counterparty_type/immatriculation/number nullables + colonne status
(migration racine), contraintes strictes déplacées en groupe de validation
finalize, opération PATCH /weighing_tickets/{id}/validate, numéro attribué à la
validation. Front : 4 champs en haut hors blocs, persistance immédiate des
pesées, écrans Ajouter/Modifier refondus, colonne Statut dans la liste, form à
plat pleine largeur. Tests back (lifecycle brouillon/validate) + front à jour.
2026-06-24 15:13:12 +02:00
tristan 36149dd521 feat(back) : bon de pesée PDF via template Twig (ERP-192)
Endpoint API Platform GET /api/weighing_tickets/{id}/print.pdf (provider
renvoyant un binaire, pas de controller) sécurisé par
logistique.weighing_tickets.view. Rendu d'un template Twig hydraté avec le
ticket converti en PDF via Dompdf. Reproduit le modèle fourni : en-tête fixe
(logo + identité société, indépendant du site), pesées à vide/plein avec le
numéro de pesée affiché comme un DSD, poids net = plein − vide.
2026-06-24 10:01:49 +02:00
tristan a650fe8132 fix(back) : conserver le DSD de la pesée au lieu de le réallouer au save (ERP-189)
Le DSD est l'index de pesée du pont, déjà verrouillé (FOR UPDATE) à l'émission
par l'endpoint de pesée. Le WeighingTicketProcessor ne le réalloue plus à la
création : il conserve la valeur reçue, n'allouant qu'en filet de sécurité si
elle est absente. Supprime le double comptage AUTO, aligne le DSD prévisionnel
sur le DSD enregistré, et reste compatible avec le futur driver matériel
(le DSD viendra du pont et sera persisté à l'identique).
2026-06-23 17:46:43 +02:00
tristan 68e7205793 fix(back) : 422 de validation mappables + poids obligatoire sur le ticket de pesée (ERP-189)
- collectDenormalizationErrors sur Post/Patch : les erreurs de dénormalisation
  (date/type/IRI) reviennent en 422 avec propertyPath (et non 400 opaque), donc
  mappables inline côté front (miroir M1 Client).
- NotBlank sur emptyWeight : le poids à vide est obligatoire à la création, sa
  violation est renvoyée avec counterpartyType / immatriculation d'un seul coup.
2026-06-23 14:03:32 +02:00
tristan 5e15c1f69f fix : retours métier ERP-193 (4 répertoires) (#139)
Auto Tag Develop / tag (push) Successful in 11s
Lot de retours métier **ERP-193** (« Fix tous les retours starseed »), transverse aux 4 répertoires (clients, fournisseurs, prestataires, transporteurs).

## Contenu

- **Pagination** : défaut à 25 items/page sur les 4 répertoires.
- **Libellé** : colonne « Dernière activité » → « Dernière modification ».
- **Consultation** : masquage des onglets vides (coquilles « à venir » + onglets de données sans donnée).
- **Chiffre d'affaires** : plafonné à 999 999 999 999,99 (clamp front + `Assert\LessThanOrEqual` back).
- **Date de création** : interdiction des dates futures (`:max` MalioDate + `Assert\LessThanOrEqual('today')` back).
- **Caractères spéciaux** : blocage des caractères parasites (`²³§~#|…`) dans les champs texte via une allow-list par profil (nom de personne / texte libre / adresse / code alphanumérique) — filtrage front à la frappe + `Assert\Regex` back autoritaire. Email/IBAN/BIC/TVA conservent leurs validateurs de format.
- **UI** : champs en consultation et onglets validés grisés (`readonly` → `disabled`).
- **UI** : boutons « Archiver » en rouge (variant `danger`).

## Tests

- Back : nouveaux tests RG (plafond CA, dates futures, caractères spéciaux) + garde-fou contraintes — suite complète verte (813 tests).
- Front : nouveaux tests unitaires (sanitizers, helpers date/montant) — 615 tests verts, eslint clean.

---------

Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Reviewed-on: #139
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-22 09:40:40 +00:00
matthieu 36e947fd8e test(logistique) : tests PHPUnit RG-5.01→5.10 + capture contrat JSON (ERP-187) (#137)
Auto Tag Develop / tag (push) Successful in 8s
## ERP-187 (1.7) — Tests PHPUnit RG-5.01→5.10 + capture contrat JSON

Couvre les règles de gestion du M5 (tickets de pesée) par des tests PHPUnit et capture la **réponse JSON réelle** (DoD § 4.0.bis) collée dans `spec-back.md` avant les écrans front.

### Tests unitaires (Processor / Normalizer / Callback — sans BDD ni HTTP)
- **NetWeightTest** (RG-5.05) : net = plein − vide, `null` si une pesée manque, recalcul au PATCH.
- **CounterpartyValidationTest** (RG-5.03) : présence du champ requis par branche (propertyPath `client`/`supplier`/`otherLabel`) + exclusivité (null-ification hors-branche).
- **ImmatriculationNormalizationTest** (RG-5.01/5.10) : masque `XX-000-XX`, « Tout format », mapping 422 sur `immatriculation`.

### Tests fonctionnels (API réelle)
- **WeighingTicketNumberingTest** (RG-5.02/5.09) : format `{siteCode}-TP-{NNNN}`, séquence par site, isolation inter-sites, immuabilité numéro/site au PATCH.
- **WeighingTicketSerializationContractTest** (DoD § 4.0.bis) : 4 pièges verts (client embarqué, `plateFreeFormat` présent, `number` formaté, `netWeight` = full − empty) + dump JSON via `WEIGHING_TICKET_DOD_DUMP`.
- **WeighingTicketRBACMatrixTest** (§ 5.2) : admin/bureau/usine OK, compta/commerciale 403, anonyme 401.

> DSD / stub pont bascule / endpoint pesée déjà couverts (ERP-184/185).

### DoD
- `spec-back.md § 4.0.bis` : **JSON réel** (liste + détail) collé, 4 pièges marqués  — feu vert front.

### Vérifications
- `make test` complet **vert** : 848 tests, 6302 assertions (0 échec ; deprecations/notices PHPUnit seuls).
- `make php-cs-fixer-allow-risky` : 0 correction.

Empilée sur ERP-186 (stack M5).

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #137
2026-06-18 13:33:39 +00:00
Matthieu 02a22597b3 feat(logistique) : export XLSX des tickets de pesée (ERP-186)
Endpoint GET /api/weighing_tickets/export.xlsx — controller custom (priority: 1)
calque sur les exports M2/M3/M4, delegue la generation au SpreadsheetExporter
partage. Rejoue la selection du WeighingTicketProvider (recherche ?search, tri
?order[displayDate], cloisonnement par site courant) SANS pagination : export
complet de la liste (§ 4.5).

Colonnes : Numero, Type contrepartie, Contrepartie (nom Client/Fournisseur/
Autre), Date, Immatriculation, Poids vide, Poids plein, Poids net, DSD vide,
DSD plein. Securite logistique.weighing_tickets.view.

Tests fonctionnels : 200 + en-tetes/Content-Disposition, mapping des colonnes
avec net = plein - vide (RG-5.05), cloisonnement par site (non-admin), 403, 401.
2026-06-18 14:37:16 +02:00
Matthieu 76e7a59ba7 feat(logistique) : WeighingTicketProvider + Processor — numérotation, contrepartie, net, normalisation (ERP-185)
Logique métier d'écriture et de lecture du ticket de pesée (M5).

Processor (POST/PATCH) :
- résolution du site courant (CurrentSiteProvider) + attribution du numéro
  {siteCode}-TP-{NNNN} à la création, immuables ensuite (RG-5.02 / RG-5.09) ;
- exclusivité de la contrepartie CLIENT/FOURNISSEUR/AUTRE — null-ification des
  champs hors-branche (RG-5.03, garde-fou CHECK Postgres) ;
- normalisation immatriculation trim/UPPER + masque XX-000-XX hors « Tout
  format », 422 inline sur le champ si invalide (RG-5.01 / RG-5.10) ;
- DSD autoritaire pour les pesées AUTO via DsdAllocator (verrou), MANUEL conservé
  (RG-5.04) ;
- poids net = plein − vide recalculé (RG-5.05).

Provider (GET) : liste paginée (Paginator ORM, règle n°13), recherche ?search=,
tri ?order[displayDate], cloisonnement par site courant appliqué dans le provider
(le SiteScopedQueryExtension ne traverse pas un provider custom), fetch-join
client/supplier/site anti-N+1, 404 hors périmètre / soft-delete.

Ajouts : WeighingTicketNumberAllocator (compteur weighing_ticket_counter,
SELECT FOR UPDATE), WeighingTicketFieldNormalizer, InvalidImmatriculationException
+ alias DI.

make test vert (811), Architecture vert (CollectionsArePaginatedTest).
2026-06-18 14:37:16 +02:00
Matthieu e88bb059e6 feat(logistique) : pesée pont bascule stub + allocateur DSD + endpoint (ERP-184)
- WeighbridgeReaderInterface (contrat) + RandomWeighbridgeReader (stub,
  poids aléatoire ∈ [10000,50000] kg, RG-5.06) + WeighbridgeUnavailableException
- DsdAllocator : compteur DSD par site (weighbridge_dsd_counter) incrémenté
  sous verrou ligne SELECT ... FOR UPDATE (RG-5.04, § 2.7)
- endpoint POST /api/weighbridge_readings : ressource virtuelle
  WeighbridgeReadingResource + WeighbridgeReadingProcessor (pas de controller)
  - AUTO -> {weight, dsd, mode} ; MANUAL -> {weight, dsd, manualNumber, mode}
  - WeighbridgeUnavailableException -> HTTP 503 explicite (RG-5.06)
  - site courant via CurrentSiteProviderInterface (contrat Sites)
  - is_granted('logistique.weighing_tickets.manage')
- dsd renvoyé prévisionnel : attribution autoritaire refaite à la création
  du ticket (ERP-185)
- tests : WeighbridgeReaderStubTest, DsdAllocatorTest, processor (503/400),
  WeighbridgeReadingApiTest (RBAC + AUTO/MANUAL + 422)
2026-06-18 14:37:16 +02:00
Matthieu 312c119c06 feat(logistique) : entité WeighingTicket + dette site.code (ERP-183)
Entité WeighingTicket
- Entité métier complète (#[Auditable], TimestampableBlamableTrait, relations
  ORM Client/Supplier/Site) + contrat de sérialisation à 3 maillons
  (weighing_ticket:read / :item:read + contextes par opération).
- Getters calculés displayDate et plateFreeFormat (#[SerializedName]),
  sécurité view/manage, pas de Delete/archive.
- Validation #[Assert\*] messages FR + #[Assert\Callback] RG-5.03 (->atPath()),
  libellé i18n audit.entity.logistique_weighingticket.
- Repository : interface Domain + DoctrineWeighingTicketRepository
  (recherche + tri number DESC, deletedAt IS NULL).

Dette site.code
- Site.code mappé VARCHAR(8) (groupes read/write), dérivation auto au
  PrePersist (2 premiers chiffres du CP), UniqueConstraint uq_site_code.
- Migration Version20260617160000 : ALTER COLUMN code SET NOT NULL + COMMENT.
- Fixtures (codes 86/17/82) et SiteApiTest ajustés.

Câblage
- doctrine.yaml : mapping ORM du module Logistique (absent du scaffold ERP-181).
- ColumnCommentsCatalog : site.code + table weighing_ticket.

Specs M5 versionnées (spec-back / spec-front / prompts).
2026-06-18 14:37:16 +02:00
Matthieu 8491f55072 feat(logistique) : migration schéma M5 tickets de pesée (ERP-182)
Crée le schéma BDD du module Logistique (M5) au namespace racine
DoctrineMigrations (FK cross-module user/client/supplier/site, règle n°11) :

- site.code VARCHAR(8) (préfixe de numérotation {siteCode}-TP, RG-5.02) +
  backfill depuis le code postal + index unique uq_site_code. Colonne NULLABLE
  à ce ticket (l'entité Site ne mappe pas encore code) ; mapping ORM,
  peuplement et SET NOT NULL portés par le ticket entité.
- weighing_ticket_counter / weighbridge_dsd_counter : compteurs par site
  (numéro RG-5.02 / DSD pont RG-5.04), gérés en DBAL brut FOR UPDATE, hors ORM
  → exclus du schema_filter (sinon schema:update les droppe) + catalogués.
- weighing_ticket : table principale (contrepartie Client/Fournisseur/Autre
  avec CHECK 3 branches RG-5.03, immatriculation partagée, pesées vide/plein
  en colonnes plates, net_weight dérivé, soft-delete + Timestampable/Blamable).
  Index unique (site_id, number) + index FK. ON DELETE site/client/supplier =
  RESTRICT, created_by/updated_by = SET NULL.

COMMENT ON COLUMN sur chaque colonne créée (règle n°12). make test +
ColumnsHaveSqlCommentTest verts, db-reset OK.
2026-06-18 14:36:05 +02:00
Matthieu c63a5f971f feat(logistique) : scaffold module + socle RBAC tickets de pesée (ERP-181)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 3m12s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m44s
Nouveau module Logistique (M5), sans entité ni migration (ticket 1.2) :
- LogistiqueModule (ID logistique, permissions weighing_tickets.view/manage)
  enregistré dans config/modules.php
- layer front frontend/modules/logistique (auto-détecté)
- sidebar : section Logistique + item /weighing-tickets (gate ...view)
  + clés i18n sidebar.logistique.*
- 3 miroirs RBAC alignés : sidebar.php, personas.ts (user-full),
  SeedE2ECommand (user-full)
- matrice métier RbacSeeder : Bureau + Usine = view/manage ;
  Compta + Commerciale = aucun accès (spec § 5.2)
2026-06-18 14:36:05 +02:00
tristan d304b74289 refactor(transport) : supprime les reliquats multi-adresses — colonne position, dead code front, docblocks 1:n (ERP-172)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 3m20s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m39s
2026-06-18 10:38:25 +02:00
tristan e76bd1dd63 feat(transport) : adresse unique par transporteur (OneToOne back + un seul bloc front) (ERP-172) 2026-06-17 17:32:29 +02:00
tristan 498cef8cc0 fix(transport) : embarque le nom de la décharge dans le détail carrier (consultation/modif) (ERP-171)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 3m15s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m50s
2026-06-17 16:08:08 +02:00
tristan f29266e5e8 fix(transport) : contact transporteur valide si prénom OU nom (alignement M1/M2/M3) (ERP-168)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 3m24s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m35s
2026-06-17 16:07:51 +02:00
Matthieu 6dab7cfd17 style(transport) : conformite php-cs-fixer (lint CI projet entier)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 3m10s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m38s
2 nits cs preexistants masques par le cache local (.php-cs-fixer.cache) et
revele par la CI (check projet entier, sans cache) : QualimatCarrierSearchProvider
et CarrierFixtures. Sans incidence fonctionnelle.
2026-06-16 15:36:42 +02:00
Matthieu 18c88156e5 test(transport) : couverture RG-4.01→4.14 + contrat + fixtures (ERP-163)
- CarrierListTest : anti-N+1 liste (fetch-join qualimat), tri name ASC,
  echappatoire ?pagination=false (regle n°13)
- CarrierAuditTest : POST/PATCH/archive -> audit_log entity_type='transport.Carrier'
- CarrierAddressApiTest : CP/ville incoherents acceptes (RG-4.06, pas de
  controle de coherence serveur)
- CarrierFixtures : fixtures dev completes et idempotentes (QUALIMAT validite
  passee, AUTRE+decharge, affrete, LIOT, complet prix CLIENT+FOURNISSEUR,
  archive) ; env-gated dev uniquement
- spec-back § 4.0.bis : JSON reel capture (liste + detail) via CarrierSerializationContractTest
2026-06-16 15:13:11 +02:00
Matthieu c0fa00c9c5 feat(transport) : filtre archivedOnly sur l'export repertoire (coherence liste)
L'export XLSX du repertoire reflete la vue liste : il propage desormais
?archivedOnly comme CarrierProvider (sinon l'export divergerait de l'ecran
quand le toggle « Voir les archives » est actif).
2026-06-16 15:13:11 +02:00
Matthieu e688fe7e0b feat(transport) : export XLSX répertoire + prix transporteur (ERP-162)
GET /api/carriers/export.xlsx (mêmes filtres que la liste : includeArchived,
search, certificationType) et GET /api/carriers/{id}/prices/export.xlsx (tableau
Prix regroupé Benne / Fond Mouvant). Controllers Symfony custom avec
#[Route(priority: 1)] pour éviter le conflit API Platform {id}, génération
déléguée au service Shared SpreadsheetExporterInterface.
2026-06-16 15:13:10 +02:00
Matthieu 7d2812cea6 feat(transport) : sous-ressource prix transporteur (ERP-161)
POST /api/carriers/{id}/prices + PATCH/DELETE /api/carrier_prices/{id}
(security transport.carriers.manage) via CarrierPriceProcessor.

RG-4.09->4.11 : coherence de branche CLIENT/FOURNISSEUR (champs requis +
appartenance de l'adresse de livraison au client / de l'adresse d'appro au
fournisseur, sinon 422), nettoyage de la branche opposee (CHECK BDD). Champs
communs obligatoires via Assert\NotBlank + Assert\Choice.

Les contrats Shared ClientAddressInterface / SupplierAddressInterface exposent
desormais getClient() / getSupplier() (canal cross-module, regle n°1) pour la
verification d'appartenance. Colonnes enum du prix whitelistees dans le miroir
Assert\Length (deja bornees par Choice).
2026-06-16 15:13:10 +02:00
Matthieu daa8224b8b feat(transport) : sous-ressource contacts transporteur (ERP-160) 2026-06-16 15:13:10 +02:00
Matthieu 7012306a78 feat(transport) : sous-ressource adresses transporteur (ERP-159)
POST /api/carriers/{id}/addresses + PATCH/DELETE /api/carrier_addresses/{id}
(security transport.carriers.manage), spec-back § 4.5. Jumelle de SupplierAddress
(M2) / ProviderAddress (M3), sans address_type ni M2M.

- CarrierAddress : ajout #[ApiResource] (Get/Post/Patch/Delete) + groupe
  d'ecriture carrier:write:addresses + contraintes FR. RG-4.06 : code postal
  ^[0-9]{4,5}$ (Assert\Regex). Mapping ORM/colonnes inchange.
- CarrierAddressProcessor : rattachement parent (404 si absent) + RG-4.05
  (transporteur affrete -> Pays/CP/Ville/Adresse obligatoires, 422 par champ).
  RG-4.05 portee par le processor car le parent est indisponible a la validation
  Symfony sur un POST sous-ressource read:false. RG-4.07 = front (PATCH accepte).
- EXCLUDED_LENGTH_MIRROR : CarrierAddress::postalCode (Regex borne la longueur).
- Tests : CP invalide 422, affrete incomplet 422, affrete complet 201,
  PATCH/DELETE OK (manage), 403 sans manage.
2026-06-16 15:13:10 +02:00
Matthieu 397fb22c62 feat(transport) : endpoint recherche QualimatCarrier (ERP-156)
GET /api/qualimat_carriers?search= pour la saisie assistee du nom (RG-4.01,
spec-back § 4.7) : recherche fuzzy sur name (+ siret), restreinte aux lignes
actives (is_active = true), triee name ASC, paginee (regle n°13).

- QualimatCarrierRepositoryInterface + DoctrineQualimatCarrierRepository :
  QueryBuilder de recherche (forcage is_active cote serveur, fuzzy multi-champs).
- QualimatCarrierSearchProvider : provider de la GetCollection (pagination Hydra
  + echappatoire ?pagination=false), branche uniquement sur la collection.
- ApiResource : provider custom sur GetCollection, retrait des ApiFilter natifs
  (incapables d'unifier name/siret sous ?search= ni d'imposer l'actif). Mapping
  ORM inchange (schema:update reste no-op). Aucune ecriture exposee.
- Tests : actifs seuls, tri name, match siret, pagination Hydra, 403 sans perm.
2026-06-16 15:13:10 +02:00
Matthieu 13d4a08bc9 feat(transport) : CarrierProcessor + champs conditionnels formulaire principal (ERP-158)
Ecriture du formulaire principal transporteur (M4, WT4) : POST/PATCH via
CarrierProcessor + CarrierFieldNormalizer, contraintes conditionnelles sur
l'entite Carrier.

- RG-4.01 : POST qualimatCarrier -> certificationType=QUALIMAT + FK persistee ;
  cas LIOT (name=LIOT) -> certification non requise, liotPlates accepte.
- RG-4.02 : certificationType=AUTRE sans dischargeDocument -> 422 (Assert\Callback).
- RG-4.03 : isChartered=true sans indexationRate/containerType/volumeM3 -> 422.
- RG-4.12 : doublon de nom (parmi actifs) -> 409 (index partiel uq_carrier_name_active).
- RG-4.13 : normalisation serveur (name UPPER, liotPlates ;-split/trim/UPPER) +
  methodes personne/telephone/email pour les sous-ressources Contact (WT7).
- RG-4.14 : PATCH isArchived exige transport.carriers.archive (Admin seul),
  mode strict -> 403 + 422 si autre champ ; restauration en conflit -> 409.

Operations Post/Patch ajoutees a l'ApiResource (lecture posee au WT3 conservee).
RG conditionnelles portees par validateMainFormConsistency (Assert\Callback +
->atPath()) pour un propertyPath mappable inline (useFormErrors, ERP-101).

certificationType / containerType whitelistes dans EXCLUDED_LENGTH_MIRROR (Choice
borne deja les valeurs, miroir SupplierAddress::addressType).

Tests : CarrierWriteApiTest (RG-4.01->4.03/4.12->4.14), CarrierRBACMatrixTest
(matrice bureau/compta/commerciale/usine), CarrierArchiveTest (409 restauration),
CarrierFieldNormalizerTest (RG-4.13). make test vert (750).
2026-06-16 15:13:10 +02:00
Matthieu aa23189fe1 feat(transport) : filtre archivedOnly sur le repertoire (coherence M1/M2/M3)
Aligne CarrierProvider/DoctrineCarrierRepository sur Client/Supplier/Provider :
?archivedOnly=true n'expose que les archives (prioritaire sur includeArchived),
pour que le toggle « Voir les archives » du front (ERP-173/ERP-164) soit operant.
Parametre optionnel en fin de signature : retro-compatible avec les appels existants.
2026-06-16 15:13:10 +02:00
Matthieu dc75945f3e feat(transport) : schéma + entités Carrier + contrat lecture (ERP-155/157)
Schéma BDD du répertoire transporteurs (M4) + entités + contrat de lecture
(liste + détail), socle du front.

- Migration Version20260615150000 : tables carrier / carrier_address /
  carrier_contact / carrier_price (FK cross-module, CHECK enum, index partiel
  uq_carrier_name_active, COMMENT ON COLUMN). uploaded_document et
  qualimat_carrier réutilisées (non recréées).
- Entités Carrier* (#[Auditable], Timestampable/Blamable) + ApiResource
  LECTURE seule (GetCollection + Get via CarrierProvider, anti-N+1, exclusion
  archivés + ?includeArchived). Écriture (POST/PATCH + Processor) reportée WT4+.
- QualimatCarrier : mapping ORM lecture seule sur la table référentielle
  existante (sortie du schema_filter, mapping aligné DDL ERP-39, schema:update
  no-op) + endpoint de recherche read-only (§ 4.7).
- Relations cross-module des prix (Client/Supplier/adresses) via contrats
  Shared (ClientInterface, SupplierInterface, ClientAddressInterface,
  SupplierAddressInterface) + resolve_target_entities — sans import inter-module
  (règle n°1). Ajout du groupe supplier_address:read aux champs de
  SupplierAddress pour l'embed.
- Garde-fous : ColumnCommentsCatalog (carrier* + qualimat_carrier), makefile
  test-db-setup (index partiel carrier), i18n audit (transport_carrier*),
  EntitiesAreTimestampableBlamableTest (QualimatCarrier whitelisté).
- CarrierSerializationContractTest : contrat JSON liste + détail vérifié
  (embeds objet, booléens, enveloppe Hydra) ; JSON réel capturé dans
  spec-back § 4.0.bis.

make db-reset OK, make test vert (731), make nuxt-test vert (480),
php-cs-fixer OK.
2026-06-16 15:13:10 +02:00
Matthieu 2be9cd05d4 feat(transport) : permissions carriers + sidebar (ERP-153)
Pull Request — Quality gate / Backend (PHP CS + PHPUnit) (pull_request) Successful in 2m41s
Pull Request — Quality gate / Frontend (lint + Vitest + build) (pull_request) Successful in 1m24s
Socle RBAC du module Transport (M4 § 5) :
- TransportModule::permissions() declare transport.carriers.{view,manage,archive}
- RbacSeeder::MATRIX (§ 5.2) : Bureau (view+manage), Commerciale (view) ;
  Compta/Usine aucun acces ; archive admin seul
- config/sidebar.php : section Transport + item /carriers (gate transport.carriers.view)
- i18n sidebar.transport.{section,carriers}
- 3 miroirs RBAC alignes : sidebar.php, personas.ts (user-full), SeedE2ECommand.php
- TransportModuleTest : garde-fou sur le jeu de permissions
2026-06-16 15:13:10 +02:00
tristan f9fec3e908 feat(transport) : synchronisation du référentiel codes IDTF (ERP-149) (#101)
Auto Tag Develop / tag (push) Successful in 12s
## ERP-149 — Récupération des codes IDTF (transport routier)

> ⚠️ MR **empilée** sur `feat/erp-39-qualimat-sync` (PR #99), elle-même sur la PR #97. Ordre de merge : **#97 → #99 → celle-ci**. Les bases se recibleront automatiquement.

Commande console `app:idtf:sync` : récupère l'export Excel des codes IDTF (régimes de nettoyage transport) depuis icrt-idtf.com, le parse et synchronise une table référentielle. Scope **road** ; discriminant `schema` road/water conservé pour un futur fluvial.

### Contenu
- **Migration** `Version20260612160000` (namespace racine) : `idtf_product` + `idtf_sync_log`, `COMMENT ON COLUMN` sur chaque colonne, unique `(schema, idtf_number)`, `cas_numbers` JSONB, soft-delete.
- **`IdtfSheetParser`** : parsing **pur** d'une matrice (sans dépendance PhpSpreadsheet) — détection **dynamique** de la ligne d'en-tête, mapping par libellé normalisé (résiste au réordonnancement), CAS split sur `;`, date `dd-mm-yyyy` → ISO + `checkdate`, skip des lignes non numériques.
- **`SyncIdtfCommand`** : options `--schema` (road|water) / `--file` / `--dry-run`. POST avec les **10 `fields[]` explicites** (le piège `fields[]=all` ne sort que 6 colonnes) → export 11 colonnes ; garde-fou content-type/signature ZIP. Upsert DBAL transactionnel + soft-delete + journal.
- Cible `make idtf-sync`.

### Tests
- Unitaires (`IdtfSheetParser` : en-tête dynamique, mapping, CAS, date, skip, ordre de colonnes).
- Fonctionnels de la commande via un `.xlsx` **généré** par PhpSpreadsheet (parsing → upsert → journal → soft-delete + schéma invalide rejeté).
- Suite complète **608** verte (hors flaky JWT connu). `ColumnsHaveSqlCommentTest` .
- Bout-en-bout réel : sync de **687 codes IDTF** (road).

### Décisions
- Migration **namespace racine** (convention réelle ; pas de FK cross-module).
- **Aucun changement Composer** : `phpoffice/phpspreadsheet` était déjà une dépendance (^5.7) — le bump initial vers ^5.8 a été reverté.
- Réutilise `framework.http_client` activé par la PR QUALIMAT (raison de l'empilement sur #99).

---------

Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #101
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-15 15:45:23 +00:00
matthieu 1e783bd753 feat(shared) : infra upload générique (ERP-154) (#108)
Auto Tag Develop / tag (push) Successful in 8s
Infra d'upload de fichiers générique et réutilisable dans `Shared` (spec M4 § 2.7). Ne touche pas au module Transport.

## Livré
- **Table `uploaded_document`** (migration racine `DoctrineMigrations`) : fichier téléversé immuable (PDF / images) — `original_filename`, `stored_path`, `mime_type`, `size_bytes`, `checksum` (sha256), `created_at`, `created_by`. COMMENT ON COLUMN sur toutes les colonnes + bloc dans `ColumnCommentsCatalog`.
- **Service `Shared\Infrastructure\Upload\FileUploader`** : validation MIME server-side via `getMimeType()` (jamais `getClientMimeType()`), whitelist explicite (PDF + images), bornage taille (10 Mo), checksum sha256, écriture disque `var/uploads/{yyyy}/{mm}/`.
- **Endpoint `POST /api/uploaded_documents`** (multipart, `deserialize:false`) + `UploadedDocumentProcessor` -> renvoie l'IRI ; MIME hors whitelist -> 422.
- Wiring : mapping Doctrine `Shared` + path API Platform `Shared`.

## Tests
- `FileUploaderTest` (unitaire) + `UploadedDocumentApiTest` (fonctionnel : 201/IRI/checksum, 422 MIME interdit, 422 sans fichier, 401 anonyme).

`make test` vert (701 tests), `php-cs-fixer` propre.

## Hors scope
Pas d'antivirus / S3 / purge (§ 9). Pas de `carrier.discharge_document_id` (ticket consommateur M4).

Ticket ERP-154.

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #108
2026-06-15 15:25:32 +00:00
tristan f057866e75 feat(transport) : synchronisation du référentiel transporteurs QUALIMAT (ERP-39) (#99)
## ERP-39 — Intégration QUALIMAT (transporteurs)

> ⚠️ MR **empilée** sur `feat/erp-150-module-transport` (PR #97). À merger après #97 (la base se recible automatiquement sur `develop`).

Commande console `app:qualimat:sync` : récupère les opérateurs de transport agréés depuis l'API publique qualimat.org, normalise et synchronise une table référentielle. Idempotente (refresh complet), prévue pour un **cron quotidien**.

### Contenu
- **Migration** `Version20260612150000` (namespace racine) : tables `qualimat_carrier` + `qualimat_sync_log`, `COMMENT ON COLUMN` sur chaque colonne, unique sur `siret`, index `is_active`.
- **`QualimatRowMapper`** : normalisation pure — SIRET sans espaces (clé naturelle, source "sale" non contrainte à 14), `dd/mm/yyyy` → ISO avec `checkdate`, skip des items sans SIRET, `Nom`=`Societe` → une colonne.
- **`SyncQualimatCommand`** : options `--file` / `--ppp` / `--dry-run`, fetch via http-client, upsert DBAL transactionnel (`ON CONFLICT (siret)`) + soft-delete des absents + journal, garde-fou troncature (`count == ppp`).
- Activation de `framework.http_client` (l'alias `HttpClientInterface` n'était pas enregistré).

### Tests
- Unitaires (`QualimatRowMapper`) + fonctionnels de la commande via `--file` (upsert, normalisation, journal, soft-delete).
- Suite complète **598/598** verte. `ColumnsHaveSqlCommentTest` .
- Bout-en-bout réel : sync de **2332 transporteurs** (1 ignoré sans SIRET, 0 désactivé, 1 journal).

### Décisions
- Migration au **namespace racine** `migrations/` (convention réelle M2/M3 ; pas de FK cross-module ; évite le tri FQCN) — écart assumé vs le mot "modulaire" du ticket.
- `status` sans CHECK contraignant (feed externe), `siret` non contraint à 14 (source incomplète).

---------

Co-authored-by: Matthieu <contact@malio.fr>
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Reviewed-on: #99
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-15 14:39:56 +00:00
tristan 368bb50ffb feat(transport) : créer le module Transport (ERP-150) (#97)
Auto Tag Develop / tag (push) Successful in 8s
## ERP-150 — Créer le module Transport

Scaffold du module **Transport** (prérequis commun à ERP-149 IDTF et ERP-39 QUALIMAT). Le module hébergera des référentiels externes synchronisés par commandes console.

### Contenu
- `src/Module/Transport/TransportModule.php` — ID `transport`, LABEL `Transport`, REQUIRED `false`, `permissions()` vide à ce stade (référentiels console, sans écran ni action protégée).
- `config/modules.php` — activation du module.
- `frontend/modules/transport/nuxt.config.ts` — layer Nuxt minimal (pas d'écran ni d'item sidebar à ce stade).

### Vérifications
- `GET /api/modules` → liste `transport`.
- `cache:clear` + `app:sync-permissions` OK (0 permission, rien cassé).
- `nuxi prepare` → layer auto-détecté.
- Suite PHPUnit : seuls les flakies connus (JWT 401 / DB) échouent ; verts en isolation. Le changement ne touche ni BDD, ni JWT, ni logique testée.

Débloque ERP-149 et ERP-39.

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #97
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-15 14:03:35 +00:00
tristan c76c447aa2 feat(front) : consultation + modification prestataire (ERP-145) (#107)
Auto Tag Develop / tag (push) Successful in 8s
Empilée sur ERP-144 (#106).

## Périmètre ERP-145
Écrans **Consultation** (lecture seule) et **Modification** (édition par onglet), peuplés depuis la **seule** réponse `GET /api/providers/{id}` (embed contacts/adresses/ribs + refs comptables — pas de N+1).

### Consultation — `pages/providers/[id]/index.vue` (`/providers/{id}`)
- Ouverture par défaut sur **Contacts** ; tous champs readonly ; onglets **Contacts · Adresse · Rapports · Échanges · Comptabilité** (navigation libre). Rapports/Échanges = placeholders « À venir ».
- Flèche retour → répertoire. Bouton **Modifier** (si `manage` OU `accounting.manage`). Bouton **Archiver** (Admin seul, `archive`) → modal → PATCH `{isArchived:true}` ; **Restaurer** si archivé.
- Comptabilité visible seulement si `accounting.view` ; banque/RIB affichés selon le type de règlement (VIREMENT/LCR).

### Modification — `pages/providers/[id]/edit.vue` (`/providers/{id}/edit`)
- Pré-rempli ; **bloc principal éditable** (Nom/Catégories/Sites, PATCH `provider:write:main` via `updateMain`) ; onglets Contact/Adresse/Comptabilité en **navigation libre**, PATCH partiel par onglet (réutilise `useProviderForm` en `editMode`).
- Onglets sans permission `manage` / `accounting.manage` restent **readonly** (pas de bouton Valider / suppression). Accès réservé à `manage` OU `accounting.manage`.

### Composables / helpers
- **`useProvider(id)`** : charge le détail (ld+json) + archive/restore (PATCH isArchived seul, puis rechargement).
- **`useProviderForm`** étendu : `updateMain()` (PATCH principal en édition) + `editMode` (completeTab ne verrouille/avance plus).
- **`providerDetail.ts`** : mapping embed → brouillons + options role-indépendantes (libellés depuis l'embed) + règles d'actions (Modifier/Archiver/Restaurer).

## Conformité
- `useApi()` only ; `Malio*` only ; `usePermissions()` pour boutons/onglets ; aucun texte FR en dur ; pas d'import inter-module (règle ABSOLUE n°1).

## Vérifications
- Vitest : 470/470 (16 nouveaux : mapping détail, actions par permission, updateMain + editMode).
- ESLint : OK · `nuxi typecheck` : 0 erreur sur les fichiers source du ticket.
- Golden path navigateur : **Consultation** (ACME) — bloc principal readonly + libellés catégories/sites résolus depuis l'embed, 5 onglets, Modifier+Archiver visibles (admin), Comptabilité readonly. **Modification** — bloc principal éditable pré-rempli (Site « 86 17 »), 3 onglets navigation libre, onglet Contact pré-rempli.

Reviewed-on: #107
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-15 09:29:44 +00:00
matthieu 3fe0f676f6 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
2026-06-12 14:44:43 +00:00
matthieu 54d8327fa5 feat(technique) : entités + repositories Provider* (ERP-133) (#91)
Auto Tag Develop / tag (push) Successful in 9s
PR **empilée sur ERP-132** (#90) — base = \`feature/ERP-132-migrer-schema-bdd-m3\` (ERP-132 pas encore mergé dans develop). À rebaser sur develop une fois #90 mergée.

## Périmètre (ticket Lesstime #133, M3 § 3.3/3.4/2.12/4.0)
Entités Doctrine + mapping ApiResource (squelette) + repository avec hydratation anti-N+1. Miroir des entités `Supplier*` (M2), **amputé de l'onglet Information** et **augmenté de `provider.sites`** (M2M direct, RG-3.03).

### Créé
- `Provider`, `ProviderContact`, `ProviderAddress` (simplifiée : pas de `addressType`/`bennes`/`triageProvider`), `ProviderRib` — `#[Auditable]` + Timestampable/Blamable.
- `ProviderRepositoryInterface` + `DoctrineProviderRepository` : `createListQueryBuilder` (filtres + tri seuls) + `hydrateListCollections` anti-N+1 (catégories puis **sites en relation directe**, requêtes `IN` bornées séparées — § 2.12).

### Contrat de sérialisation (RETEX M1 — 3 maillons)
Groupes posés sur l'entité (source unique) : liste = `provider:read`+`category:read`+`site:read` ; détail = +`provider:item:read`. Piège booléen `isArchived` traité (`#[Groups]`+`#[SerializedName]` sur le getter). Embed `categories[].code/name` + `sites[].name/postalCode` (objet, pas IRI).

### Consommation cross-module (§ 2.1)
- Site/Category via contrats Shared (`SiteInterface`/`CategoryInterface` + `resolve_target_entities`) — comme Supplier, conforme règle ABSOLUE n°1.
- Référentiels comptables (`TvaMode`/`PaymentDelay`/`PaymentType`/`Bank`) en relation ORM partagée directe (décision § 2.1, remontée Shared tracée HP-M4-2).

### Garde-fous / infra (requis pour le vert)
- Mapping ORM du module `Technique` dans `doctrine.yaml` (sinon les 9 tables `provider*` vues orphelines → DROP).
- Tables `provider*` ajoutées à `ColumnCommentsCatalog` + ligne `dbal:run-sql uq_provider_company_name_active` au makefile `test-db-setup`.
- 4 libellés `audit.entity.technique_*` (fr.json) ; `ProviderAddress::postalCode` whitelisté dans `EXCLUDED_LENGTH_MIRROR` (Regex CP {4,5}).

## Hors périmètre (→ ERP-134)
ApiResource **sans** `ProviderProvider`/`ProviderProcessor` ; sous-entités **sans** `#[ApiResource]`. Hydratation effective, gating accounting, cloisonnement par site, normalisation, 409 doublon, RG-3.07/3.08 → ERP-134. Sous-ressources POST/PATCH/DELETE → ticket ultérieur.

## Tests
- \`make test\` → **589/589 ✓** · \`php-cs-fixer\` → 0 correction.
- \`schema:validate\` : mapping OK ; « not in sync » résiduel strictement homologue à supplier (COMMENT via catalogue + index FK auto-Doctrine), non régressif.

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #91
2026-06-12 14:25:27 +00:00
matthieu 09a4b9d464 feat(technique) : migration schema repertoire prestataires (ERP-132) (#90)
Auto Tag Develop / tag (push) Successful in 9s
## ERP-132 — Migrer le schéma BDD M3 (provider + sous-collections)

> ⚠️ **MR stackée** sur `feat/erp-m3-technique-module-taxonomie` (ERP-131, module Technique + type PRESTATAIRE). À merger **après** ERP-131. Base volontairement ≠ develop tant qu'ERP-131 n'est pas mergé.

### Contenu
Crée tout le schéma Postgres du répertoire prestataires (1 migration, namespace racine `DoctrineMigrations` — FK cross-module user/category/site + référentiels comptables M1).

**Tables (9)** :
- `provider` : company_name + bloc Comptabilité (siren/account_number/n_tva + FK tva_mode/payment_delay/payment_type/bank ON DELETE RESTRICT) + is_archived/archived_at/deleted_at + Timestampable/Blamable. **Pas d'onglet Information** (≠ supplier).
- M2M formulaire principal : `provider_category` (RG-3.09), `provider_site` (sites du prestataire — RG-3.03, **nouveau vs supplier** + `idx_provider_site_site` pour le cloisonnement par site).
- Sous-collections : `provider_contact` (CHECK `chk_provider_contact_name` : ≥1 champ parmi first_name/last_name/phone_primary/email), `provider_address` (**sans** address_type/bennes/triage), `provider_rib`.
- Jointures adresse : `provider_address_site` (RG-3.05), `provider_address_contact`, `provider_address_category`.
- Index partiel unique `uq_provider_company_name_active` (LOWER(company_name) WHERE non archivé/non supprimé — RG-3.10) + index FK.
- `COMMENT ON COLUMN/TABLE` inline sur **toutes** les colonnes (règle ABSOLUE n°12).

### Décisions
- **CategoryType PRESTATAIRE non re-seedé** : déjà créé par ERP-131. Migration purement structurelle.
- **COMMENT inline (pas via ColumnCommentsCatalog)** : tant que les entités Provider* n'existent pas (ERP-133), `schema:update --force` du setup test droppe les tables non mappées → les référencer dans le catalogue ferait planter `app:apply-column-comments`. Catalogue + ligne `dbal:run-sql uq_provider` différés à ERP-133, exactement comme supplier (ERP-86 après ERP-85).

### Tests
-  `make db-reset` (dev + test-db-setup)
-  `make test` — 589 tests, `ColumnsHaveSqlCommentTest` vert
-  Index partiel vérifié partiel (clause WHERE), `idx_provider_site_site` présent, 0 colonne sans COMMENT
-  Cycle `down()`/`up()` OK
-  `make php-cs-fixer-allow-risky` (0 fichier)

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #90
2026-06-12 14:19:46 +00:00
matthieu d97b9ce6d0 feat(technique) : module Technique + taxonomie categories prestataires (#89)
Auto Tag Develop / tag (push) Successful in 11s
## M3 — Ticket 1.1 : module Technique + taxonomie catégories prestataires

Prérequis de tout le M3 (répertoire prestataires). Spec : `docs/specs/M3-prestataires/spec-back.md` § 2.1 + § 2.4.

### Contenu
- **Nouveau module `Technique`** (`src/Module/Technique/TechniqueModule.php`) : `ID=technique`, `LABEL=Technique`, `REQUIRED=false`, `permissions()` (5 codes `technique.providers.*` : view / manage / accounting.view / accounting.manage / archive).
- Activation dans `config/modules.php` → `/api/modules` expose `technique`.
- **Layer front** `frontend/modules/technique/` (auto-détecté).
- **Seed taxonomie PRESTATAIRE** : nouveau `CategoryType` (code `PRESTATAIRE` / label `Prestataire`) + 3 catégories (Maintenance industrielle, Nettoyage, Transport).
  - Migration racine idempotente `Version20260612080000` (`ON CONFLICT DO NOTHING` + `NOT EXISTS`, jonction M2M `category_category_type` — schéma courant, pas l'ancien `category_type_id`).
  - Fixtures `CategoryTypeFixtures` / `CategoryFixtures` étendues (survivent au purger `db-reset`).

### Critères d'acceptation 
- [x] Module + permissions déclarées (`app:sync-permissions` → 5 codes en base)
- [x] `TechniqueModule::class` dans `config/modules.php`
- [x] Layer front
- [x] Seed CategoryType PRESTATAIRE (migration + fixture idempotente)
- [x] ≥ 3 catégories PRESTATAIRE
- [x] `GET /api/categories?typeCode=PRESTATAIRE` filtre correctement

### Tests
- `TechniqueModuleTest` : identité + jeu de 5 permissions figé.
- `CategoryPrestataireSeedTest` : `?typeCode=PRESTATAIRE` ne renvoie QUE le type PRESTATAIRE + pagination Hydra préservée.
- `make test` : **589 tests OK** · `php-cs-fixer` : 0 correction · `make db-reset` : type + 3 catégories présents, idempotent.

### Hors-périmètre (tickets M3 suivants)
Section sidebar « Technique », personas RBAC E2E, et entités `Provider*` (l'écran `/providers` n'existe pas encore → pas de lien mort introduit ici).

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #89
2026-06-12 14:19:14 +00:00
tristan a340d8139a feat(commercial) : amélioration et validation stricte des champs date (ERP-148) (#92)
Auto Tag Develop / tag (push) Successful in 8s
## Contexte
ERP-148 — mise à jour @malio/layer-ui et amélioration des champs date (onglet Information, Client & Fournisseur).

## Changements
- **MalioDate v1.7.10** : le composant expose désormais son état de validité (`@update:valid`) et la saisie brute invalide (`@update:rawValue`).
- **Validation back-autoritaire du format** : `foundedAt` n'accepte plus que l'ISO strict `Y-m-d` (`#[Context]` DateTimeNormalizer) + `collectDenormalizationErrors` sur `Client` et `Supplier`. Toute saisie non-ISO renvoie un **422 porté sur le champ**.
  - Corrige un cas piège : `12/25/2026` (invalide en JJ/MM/AAAA côté front) était auparavant accepté par PHP en M/J/AAAA → 25 décembre. Désormais rejeté.
- **Front** : la saisie invalide est transmise au back ; le message technique de type-error est surchargé par une clé i18n via le **code de violation** (`resolveViolationMessage` / `VIOLATION_MESSAGE_I18N`), affiché inline par `useFormErrors`.
- Réorganisation des utils de formulaire sous `utils/forms/`.

## Tests
- Back : `ClientFoundedAtFormatTest` / `SupplierFoundedAtFormatTest` (dont le cas piège `12/25/2026`).
- Front : résolveur i18n (`api.test.ts`, `useFormErrors.test.ts`) + payloads (`clientEdit`/`supplierEdit` specs).
- Suite Commercial verte ; vérifié bout-en-bout en navigateur (PATCH → 422, erreur inline, submit bloqué).

## Note
Échecs JWT aléatoires connus du hook pre-commit (401/500 sur tests d'auth sans rapport) ; tous verts en isolation.

Reviewed-on: #92
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-12 08:45:38 +00:00
matthieu 3f356f0679 feat(commercial) : referentiel pays (country) en base + branchement front (ERP-116) (#79)
Auto Tag Develop / tag (push) Successful in 9s
## Objectif (ERP-116, 1re iteration minimale)

Sortir la liste des pays du **code en dur** cote front et la poser en base comme **referentiel `country`**, source unique du select pays. **Perimetre volontairement minimal** : code ISO + libelle + ordre uniquement — **aucune longueur bancaire/fiscale** (numero de compte, IBAN, TVA, BIC, SIREN) a ce stade.

## Backend
- Entite `Country` (`code` ISO 3166-1 alpha-2 unique, `name`, `position`), calquee sur `Bank` : referentiel statique **lecture seule** (`GetCollection` + `Get`), gating `commercial.clients.view OR commercial.suppliers.view`.
- Migration `Version20260609100000` : table `country` + `COMMENT ON COLUMN` + seed des **6 pays** (France, Allemagne, Belgique, Espagne, Italie, Royaume-Uni), `ON CONFLICT DO NOTHING`.
- `CommercialReferentialFixtures` : re-seed des pays en dev/test.
- Garde-fous : ajout au `ColumnCommentsCatalog` + whitelist `EntitiesAreTimestampableBlamableTest`.

## Frontend
- `useClientReferentials` charge `/countries` (value = **nom** du pays : l'adresse stocke `country` en chaine libre, **pas de FK ni migration de donnees**).
- Les 3 listes `countryOptions` en dur (clients new / edit / consultation) sont supprimees ; la consultation derive ses options de l'embed.

## Hors-scope (iterations suivantes du ticket)
- Longueurs bancaires/fiscales par pays + validation associee.
- FK `country_id` sur les adresses + migration de donnees.

## Tests
- Back : suite complete verte (583), tests API dedies countries (200/seed/405/403/401).
- Front : Vitest vert (256), spec `useClientReferentials` mise a jour.
- Migration appliquee en dev + test.

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #79
2026-06-11 08:09:38 +00:00
tristan d6790dd37d feat(front) : page Ajouter un fournisseur (/suppliers/new) + workflow par onglets (ERP-94) (#83)
Auto Tag Develop / tag (push) Successful in 7s
ERP-94 (etape front 7/7 du M2). **Stack sur #97** (base = `feature/ERP-97-suppliers-i18n-sidebar`, elle-meme sur #93) pour un diff isole. A recibler sur `develop` une fois #93 (MR #81) et #97 (MR #82) mergees.

Page « Ajouter un fournisseur » — **replique a l'identique le fonctionnement de l'ecran Client** (workflow inline par onglets, blocs reutilisables, validation 422 inline ERP-101), avec les specificites M2.

## Architecture (miroir Client)
- Workflow par onglets **inline dans `suppliers/new.vue`** (comme `clients/new.vue` — il n'existe pas de `useClientForm` monolithique). Helpers paralleles : `useSupplierReferentials`, `useSupplierFormErrors`, `supplierFormRules`, `supplierEdit` (payloads), `types/supplierForm`.
- Blocs `SupplierContactBlock` / `SupplierAddressBlock` (miroir des blocs Client).
- POST `/suppliers` puis PATCH partiels par onglet (mode strict, groupes de serialisation). Sous-ressources : `/suppliers/{id}/contacts|addresses|ribs`.
- Validation ERP-101 : 422 `violations[].propertyPath` mappees inline par champ (`useFormErrors` / `mapViolationsToRecord`), `{ toast: false }`, bouton Valider toujours actif.

## Specificites M2 (vs M1)
- Formulaire principal **sans contact inline** (ERP-106) : Entreprise + Categorie (type FOURNISSEUR, `?typeCode=FOURNISSEUR`).
- Adresse : **radio exclusif** Prospect/Depart/Rendu (`addressType` enum, RG-2.09), champs **Bennes** (stepper) + **Prestation de triage**, **pas d'email de facturation**.
- Information : champ **Volume previsionnel** (8e champ).
- Compta (Admin+Compta) : banque si VIREMENT (RG-2.07), RIB si LCR (RG-2.08) ; RIB sous-ressource gardee par `accounting.manage`.

## Tests (mirroir strategie Client)
- `make nuxt-test` : 338 passed (specs ajoutees : supplierFormRules, supplierEdit, useSupplierReferentials, SupplierContactBlock, SupplierAddressBlock).
- ESLint propre ; `nuxi typecheck` (lance en container) : **0 erreur**.
- Golden path navigateur valide end-to-end : POST /suppliers OK, companyName normalise UPPERCASE (RG-2.12), gating des onglets (Information actif, Contacts deverrouille).

## Note de revue
~30 `WARN Duplicated imports` au typecheck : les helpers Supplier exportent les memes noms generiques que leurs equivalents Client (`buildMainPayload`, `omitEmptyRequired`, `RefOption`...), tous deux auto-importes par Nuxt. **Sans impact runtime** : tous les consommateurs utilisent des imports explicites (qui priment). Consequence directe du miroir 1:1 ; une factorisation des generiques dans `shared/` pourrait etre un suivi.

Reviewed-on: #83
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-11 07:14:51 +00:00
tristan 8490de99da ERP-119 : revue validation front clients + évolutions écran client (types d'adresse, 2e email, saisies manuelles, redirection) (#80)
Auto Tag Develop / tag (push) Successful in 7s
## Contexte
Branche ERP-119 — revue de la validation des formulaires clients (déclencheur : écran « Ajouter un client »), accompagnée de plusieurs évolutions de l'écran client (M1).

## Contenu

### Validation front (clients)
- Boutons « Valider » toujours actifs (retrait du gating de validité) : c'est le back qui renvoie les 422, mappées en rouge par champ.
- Champs requis adossés à une colonne non-nullable : la clé est omise du payload si vide (companyName, RIB, adresse) → 422 NotBlank au lieu d'un 400 de type.
- Onglet Contact : au moins un contact requis (l'amorce vide est soumise → 422 RG-1.05).
- Onglet Adresse : affichage inline des erreurs type / sites / catégories + RG back « au moins un type d'adresse obligatoire ».

### Nouveaux types d'adresse
- Courtier / Distributeur, types autonomes exclusifs : colonnes `is_broker` / `is_distributor` (migration + CHECK miroir d'exclusivité), entité + Callback, et front (select, drapeaux, payloads).

### Saisies manuelles
- Adresse : `allow-create` sur le champ Adresse → saisie libre si la BAN ne propose rien.
- Date de création : `MalioDate :editable` → saisie clavier JJ/MM/AAAA en plus du calendrier.

### 2e email de facturation
- Colonne `billing_email_secondary` (optionnel, max 2), miroir du téléphone secondaire. Bump `@malio/layer-ui` 1.7.8 (prop `addable`).

### Fin d'ajout d'un client
- Redirection vers la liste à la validation du dernier onglet remplissable par le rôle (Adresse pour Bureau/Commerciale, Comptabilité pour Admin) + toast « Client ajouté ». Dérivé de `tabKeys`, sans règle RBAC custom.

## Vérifications
- Back : suites Module/Commercial + Architecture vertes (Client : 124/124). Migrations appliquées (dev + test).
- Front : Vitest vert (272), ESLint OK.

> Note : le hook pré-commit flake aléatoirement (JWT 401 / timeout DB) sur des tests sans rapport (Supplier) ; les commits ont été faits après vérification des suites concernées en isolation.

Reviewed-on: #80
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-09 19:47:40 +00:00
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
tristan b8dc3cb696 Correctifs écran Client (ERP-115) (#76)
Auto Tag Develop / tag (push) Successful in 7s
Lot de correctifs sur l'écran Client (M1), + un retrait de règle métier et une petite fonctionnalité.

## Formulaire client (création / édition)
- Boutons « ajouter un bloc » (Adresse, RIB) désactivés tant que le dernier bloc n'est pas valide.
- Onglet Information : bouton Valider désactivé si aucun champ rempli (création) ; onglet Contact accessible dès la création (Information facultatif).
- Champs « Relation » (Distributeur/Courtier) et « Prestation de triage » masqués par défaut, révélés seulement si une catégorie ordinaire (≠ Distributeur/Courtier) est sélectionnée.
- Bloc RIB affiché uniquement si le type de règlement est LCR (création, édition, consultation) ; plus de RIB fantôme soumis.
- Alignement du bas du textarea « Description » sur les autres champs.

## Recherche d'adresse (BAN)
- Une erreur de l'API ne bloque plus définitivement la recherche : chaque frappe réessaie (le mode dégradé restait verrouillé).
- Garde minimum 3 caractères avant l'appel à l'API.

## Répertoire client
- Titres de colonne en noir 16px, corps + tags de site en 14px.

## Navigation
- L'onglet actif est conservé au passage consultation ↔ édition (via history.state, hors URL).

## Règle métier
- Retrait de RG-1.04 : l'onglet Information n'est plus obligatoire pour le rôle Commerciale — facultatif pour tous (back + tests + docs).

Tests : suites front (Vitest) et back (PHPUnit) vertes hormis flakes d'infra connus.
Reviewed-on: #76
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
2026-06-08 14:40:18 +00:00
matthieu a9c14704b7 feat(catalog) : categories multi-types (M:N) + bouton Filtres liste (#75)
Auto Tag Develop / tag (push) Successful in 7s
## Contexte

Une `Category` ne pouvait appartenir qu'à **un seul** `CategoryType` (ManyToOne). Le besoin métier : plusieurs types par catégorie. Cette MR fait passer la relation en **ManyToMany** et ajoute le bouton **« Filtres »** à droite de la liste des catégories (modèle Répertoire Clients).

Slice vertical complet (le passage M:N casse mécaniquement le contrat inter-module `CategoryInterface`, consommé par la RG-2.10 fournisseurs).

## Volet A — Relation M:N
- `Category.categoryType` (ManyToOne) → `categoryTypes` (ManyToMany, jonction `category_category_type`). Au moins un type obligatoire (`Assert\\Count(min:1)`).
- **Unicité du nom GLOBALE** parmi les actifs (`uq_category_name_active`, remplace `uq_category_name_type_active`). Message 409 reformulé.
- Migration : table de jonction + backfill + drop colonne `category_type_id` + nouvel index. Validée **rejouable sur base fraîche**.
- Contrat Shared : `getCategoryTypeCode()` → `getCategoryTypeCodes(): array`. `Supplier`/`SupplierAddress`/`SupplierFixtures` revalident « contient FOURNISSEUR » (RG-2.10).
- Provider/Repository : filtre type via sous-requête `EXISTS` (ne tronque pas la collection embarquée), eager-load anti-N+1.

## Volet B — Bouton « Filtres »
- Drawer recherche par nom + types multi (OR). Compteur de filtres actifs. État local, jamais persisté en URL.
- Back : filtres `?name=` et `?typeId[]=` sur la collection.

## Front
- Multi-select `MalioSelectCheckbox`, `useCategoryForm` en `categoryTypeIds[]`, colonne « Types », clés i18n.

## Tests / vérifs
- `make test` : **582 tests, 2474 assertions, 0 échec** 
- `make nuxt-test` : **236 tests** 
- `make php-cs-fixer-allow-risky` 
- Migration rejouée sur base fraîche (`make db-reset`) 
- Nouveau `CategoryFilterTest` (name partiel + typeId[] OR + multi-type non dupliqué)

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #75
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
2026-06-08 09:47:15 +00:00
matthieu 9cda225bdf Correctifs post-review M2 fournisseurs (P1 + P2/P3 + alignement M1) (#74)
Auto Tag Develop / tag (push) Successful in 8s
Correctifs issus de la review lead du stack M2 fournisseurs (ERP-84→113), répartis en priorités. Base : `develop`. Suite verte : `make test` 577 tests / 2475 assertions, `php-cs-fixer` 0 correction.

## P1 — défauts bloquants
- **ERP-89** — Le message de complétude Information ne fuit plus le nom de champ technique (`(champ "%s")` retiré). Correction miroir appliquée aux deux validators (Supplier + Client), accent uniformisé. Le `propertyPath` est conservé pour le mapping inline front.
- **ERP-112** — La fixture fournisseurs résout désormais la catégorie en filtrant sur le type `FOURNISSEUR` (via `CategoryInterface::getCategoryTypeCode()`), évitant de rattacher une catégorie homonyme d'un autre type (RG-2.10).
- **ERP-113** — Tests d'export complétés : dédup F3 (fournisseur multi-catégories rendu sur une seule ligne) ; gating SIREN prouvé via un utilisateur minimal non-admin portant `suppliers.view` + `suppliers.accounting.view` (nouveau helper `createUserWithPermissions`).

## P2 / P3
- **ERP-86** — `maxMessage` explicite sur `competitors` (Supplier).
- **ERP-92** — Garde `skipIfSitesModuleDisabled()` sur le test POST adresse sans site (évite un faux positif si le module Sites est désactivé).
- **ERP-89 bis** — Nouveau test : Admin authentifié non-Commerciale + Information incomplète → 200 (distinct du cas `user=null`).
- **ERP-85** — `down()` de la migration fournisseurs en `DROP TABLE IF EXISTS`.
- **ERP-87** — Reset de la mémoïsation payload en début de `process()` du SupplierProcessor + documentation du filtre `?archivedOnly` de l'export (parité avec le provider liste).
- **spec-back.md (M2)** — Alignée sur le code (le code fait foi) : security PATCH `manage or accounting.manage`, gating accounting par ajout de groupe (`SupplierReadGroupContextBuilder`), anti-N+1 via `hydrateListCollections` (pas de fetch-join), types de colonnes réels (`IDENTITY` / `TIMESTAMP(0)`).

## Alignement M1 ↔ M2
- **ERP-86/87 (Client)** — Mêmes corrections appliquées aux jumeaux M1 : message `competitors` explicite + reset mémoïsation `ClientProcessor`.

## Décision actée
- **RG-2.10 (catégorie)** : court-circuit conservé (une seule violation sur `categories`). Les violations partageant path + message sont fusionnées côté front ; ERP-101 (toutes les erreurs en un aller-retour) est déjà respecté car le Callback n'interrompt pas la validation des autres champs.

---------

Co-authored-by: admin malio <malio@yuno.malio.fr>
Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #74
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
2026-06-08 08:47:43 +00:00
matthieu b35deed8fe feat(commercial) : fixtures Doctrine fournisseurs (≈13 suppliers complets + sous-collections) (ERP-112) (#72)
Auto Tag Develop / tag (push) Successful in 6s
## ERP-112 — Fixtures Doctrine fournisseurs (M2)

`SupplierFixtures` (calquée sur `ClientFixtures` / ERP-68) : ~13 fournisseurs de démonstration couvrant les cas pivots du répertoire fournisseurs (M2), chargés par `make db-reset`.

### Contenu
- **13 fournisseurs** (dont **2 archivés** — RG-2.17), `companyName` variés (UPPERCASE serveur), mono et multi-catégories de type FOURNISSEUR (RG-2.10).
- **19 contacts** (1 à 3 par fournisseur, dont un avec téléphone secondaire et un nommé par le seul nom — RG-2.04).
- **15 adresses** multi-types PROSPECT / DEPART / RENDU (RG-2.09) et multi-sites 86/17/82 (RG-2.06), avec `bennes` et `triageProvider`.
- **3 RIB**, compta complète sur une partie (siren, tvaMode, paymentDelay, paymentType).

### Cas pivots
- VIREMENT → banque renseignée (RG-2.07) ; LCR → 1 puis 2 RIB (RG-2.08) ; CHEQUE et NON_SOUMISE sans RIB.
- Onglet Information complet (dont `volumeForecast`, spécifique fournisseur).
- Cohérence gating comptable (un rôle sans `accounting.view` ne voit pas la compta) — support des tests ERP-92 et du golden path front.

### Notes
- **Idempotent** (lookup par companyName normalisé, aligné sur `uq_supplier_company_name_active`) ; rejouable sans doublon même purger désactivé.
- Référentiels comptables **réutilisés de M1** (tva_modes / payment_delays / payment_types / banks) — aucune nouvelle table.
- Données de démonstration **dev uniquement** : early return en env `test` (les tests seedent leurs propres données).

### Vérifications
- `make db-reset` : 13 fournisseurs (2 archivés), 19 contacts, 15 adresses, 3 RIB chargés sans erreur.
- Idempotence `--append` : compteurs inchangés.
- `make php-cs-fixer-allow-risky` : 0 fichier à corriger.
- `make test` : 574 tests OK.

Base : `feature/ERP-92-tests-phpunit-m2` (sommet de la pile M2).
---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #72
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
2026-06-08 07:49:28 +00:00
matthieu 6f9bb68170 test(commercial) : tests PHPUnit M2 fournisseurs (matrice RG + contrat sérialisation + DoD JSON réel) (ERP-92) (#71)
Auto Tag Develop / tag (push) Successful in 7s
## ERP-92 — Tests PHPUnit M2 fournisseurs (#521)

Suite fonctionnelle M2 assertant sur le **corps JSON** (jamais les annotations), jumelle de la suite clients M1.

### Couverture
- **Contrat de sérialisation** (`SupplierSerializationContractTest`) : 4 régressions M1 re-testées — RIB gaté **absent** pour la Commerciale, booléens `triageProvider`/`isArchived` présents, embed `categories[].code/name`, embed `sites[].name/postalCode` (objet, pas IRI) — + enveloppe AP4 (`member`/`totalItems`/`view`, archivés exclus) + suppression du contact inline.
- **Matrice RBAC réelle** (`app:seed-rbac`, pas de mock) : bureau/compta/commerciale/usine 200/403, gating `accounting` par **omission de clé**, mode strict PATCH (RG-2.16).
- **Matrice RG-2.03 → RG-2.17** (création, normalisation RG-2.12, catégorie FOURNISSEUR RG-2.10, unicité RG-2.11, archivage RG-2.14/2.15, RG-2.07/2.08 compta, sous-ressources RG-2.04/2.05/2.06/2.09).
- **Anti N+1 liste** : nombre de requêtes constant entre 2 et 4 fournisseurs. **Audit** Supplier + RIB (`iban`/`bic` dans le diff).

### Fix de contrat (découvert par la DoD)
Les référentiels comptables (`TvaMode`/`PaymentType`/`PaymentDelay`/`Bank`) ne portaient que `client:read:accounting` (M1) → sur un fournisseur ils sortaient en **IRI nu**. Ajout de `supplier:read:accounting` → objet `{id, code, label}` embarqué (additif, zéro impact M1). Sans ce fix, #95/#96 auraient été développés contre un contrat faux.

### Infra
`makefile` : `test-db-setup` recrée l'index partiel `uq_supplier_company_name_active` (droppé par `schema:update` comme celui du client — oubli M2).

### DoD 
§ 4.0.bis : réponses JSON **réelles** (liste + détail admin/commerciale) collées. Front #93→#96 peuvent démarrer.

### Vérifs
- `make test` : **574 tests OK** (suite complète verte)
- `make php-cs-fixer-allow-risky` : 0 correction

---------

Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #71
Co-authored-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-committed-by: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
2026-06-08 07:49:17 +00:00