Le job backend de la quality gate échoue à `composer install` : `phpoffice/phpspreadsheet 5.7.0` (export XLSX) requiert `ext-gd`, absente du runner.
L'extension n'était jamais déclarée dans `setup-php` — le build passait tant que le runner la fournissait implicitement. Le runner ayant perdu `gd`, toutes les PR cassent désormais sur cette étape (PR #121/#122 passaient hier, #123 casse aujourd'hui : même code, même composer.lock, runner différent).
Fix : ajouter `gd` à la liste des extensions du job backend.
Reviewed-on: #124
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
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.
Repond aux retours de review (rigueur d'assertion transversale) :
- mutualise assertViolationOnPath dans AbstractCarrierApiTestCase (au lieu d'un
duplicata local a CarrierWriteApiTest) ;
- asserte le propertyPath des 422 des sous-ressources (adresses city/street/postalCode,
contacts firstName/phones/email, prix clientDeliveryAddress/supplierSupplyAddress/price)
-> evite les faux-verts du mapping inline (ERP-101) ;
- 404 parent (POST sur /carriers/999999/{addresses,contacts,prices}) ;
- 401 anonyme + filtre ?certificationType= sur la collection (trous releves sur le
contrat de lecture).
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).
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.
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).
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.
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.
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.
Le composant etait declare en require-dev alors que
config/packages/http_client.yaml active framework.http_client et que
SyncQualimatCommand (module Transport) autowire HttpClientInterface. En prod
(composer install --no-dev), la classe Symfony\Component\HttpClient\HttpClient
etait absente -> DefinitionErrorExceptionPass : Invalid service
"http_client.transport". Le package est un runtime prod (commandes de synchro
des referentiels QUALIMAT / IDTF) : il passe donc en require.
## Contexte (ERP-172)
Sur les ecrans de **modification**, supprimer un bloc Contact / Adresse / RIB ne supprimait pas la sous-ressource cote serveur :
- **M1 / M2** : DELETE differe au clic « Enregistrer » de l'onglet -> ne partait jamais si l'utilisateur ne re-validait pas.
- **M3** : aucun DELETE (`splice` local uniquement).
## Correctifs
### 1. DELETE immediat des sous-ressources
- Nouveau helper partage `frontend/shared/utils/collectionRow.ts` (`removeCollectionRow`) + tests Vitest.
- A la confirmation de la modale : bloc existant (`id` en base) -> `DELETE` immediat ; bloc jamais persiste -> retrait local ; echec serveur (ex. 409 dernier RIB d'une LCR) -> bloc conserve + message back.
- Branche sur M1 / M2 / M3 (contacts / adresses / RIB). Suppression du mecanisme differe (`removed*Ids` + boucles dans `submit*`) devenu mort.
### 2. Affichage de la poubelle unifie (`isRowRemovable`)
Regle identique sur les 3 modules : poubelle visible sur un bloc **seulement s'il reste un autre bloc deja enregistre** (`id` en base).
- Tant que rien n'est enregistre -> aucune poubelle (plus de suppression d'un simple brouillon non valide).
- On peut jeter un brouillon non enregistre s'il reste un bloc enregistre.
- On ne peut jamais supprimer son dernier bloc enregistre.
- Applique aux ecrans **new + edit** des 3 modules (contacts / adresses / RIB).
## Tests
- Helper couvert par Vitest (`removeCollectionRow` + `isRowRemovable`).
- `make nuxt-test` : 480 tests OK. `make nuxt-lint` : OK.
## A verifier (golden path)
Sur les 3 modules : supprimer un bloc existant -> `DELETE` part immediatement -> reload -> le bloc a disparu ; la poubelle n'apparait qu'avec un 2e bloc deja enregistre.
Reviewed-on: #109
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>
## 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: THOLOT DECHENE Matthieu <matthieu@yuno.malio.fr>
Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #99
Co-authored-by: tristan <tristan@yuno.malio.fr>
Co-committed-by: tristan <tristan@yuno.malio.fr>