feat(transport) : synchronisation du référentiel transporteurs QUALIMAT (ERP-39) #99
Reference in New Issue
Block a user
Delete Branch "feat/erp-39-qualimat-sync"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
ERP-39 — Intégration QUALIMAT (transporteurs)
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
Version20260612150000(namespace racine) : tablesqualimat_carrier+qualimat_sync_log,COMMENT ON COLUMNsur chaque colonne, unique sursiret, indexis_active.QualimatRowMapper: normalisation pure — SIRET sans espaces (clé naturelle, source "sale" non contrainte à 14),dd/mm/yyyy→ ISO aveccheckdate, 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).framework.http_client(l'aliasHttpClientInterfacen'était pas enregistré).Tests
QualimatRowMapper) + fonctionnels de la commande via--file(upsert, normalisation, journal, soft-delete).ColumnsHaveSqlCommentTest✅.Décisions
migrations/(convention réelle M2/M3 ; pas de FK cross-module ; évite le tri FQCN) — écart assumé vs le mot "modulaire" du ticket.statussans CHECK contraignant (feed externe),siretnon contraint à 14 (source incomplète).Revue de code ERP-39 (synchro QUALIMAT). 1 point bloquant (perte de référentiel sur réponse API vide/malformée) + 4 points secondaires. Détails en commentaires inline.
@@ -0,0 +2,4 @@declare(strict_types=1);namespace DoctrineMigrations;🔵 Création de tables de module au namespace racine
DoctrineMigrations— à confirmer avec l'équipe.architecture.md+ règle n°11 réservent la racine aux migrations d'init (user/RBAC/seed de base) et cantonnent le namespace modulaire aux « évolutions post-schema (ajout de colonnes, index) ». Une création de tables de module ne tombe dans aucun des deux libellés. Le docblock justifie par « comme les autres migrations de création de tables » (pratique de fait non écrite). L'argument « pas de FK cross-module » est valable mais n'est pas le critère énoncé.➡️ Trancher avec l'équipe et, le cas échéant, documenter le critère dans les règles.
@@ -0,0 +129,4 @@// toArray() leve une exception sur un statut non-2xx ou un corps non-JSON.$data = $response->toArray();return array_is_list($data) ? $data : [];🔴 Perte de données — réponse 2xx vide/malformée désactive tout le référentiel.
toArray()lève bien sur un non-2xx (chemin sûr). Mais un 2xx au corps inattendu ({},{"error":...}, enveloppe{"data":[...]}, ou liste légitimement vide lors d'un incident amont) retourne silencieusement[]. Il n'y a aucun garde-fou « zéro ligne » avant la partie destructive :upsertAll([])ne tamponne rien, puisdeactivateMissing()(l. 102) matche tous les transporteurs actifs (last_synced_at < :run), bascule tout en inactif etcommit().➡️ Faire échouer
fetchRemote(exception) sur un payload non-list au lieu de retourner[], et abortFAILUREsi$total === 0avant la transaction. À corriger avant merge.@@ -0,0 +182,4 @@$count = 0;foreach ($rows as $r) {🟡 Upsert ligne à ligne — ~10 000 allers-retours/run.
Avec
--ppp=10000par défaut, jusqu'à ~10kexecuteStatementparamétrés séparés par run. C'est le coût dominant d'un refresh quotidien.➡️
INSERT ... VALUES (...),(...) ON CONFLICTpar paquets (ou au moins unprepare()réutilisé hors boucle). Non bloquant.@@ -0,0 +195,4 @@'validity_date' => $r['validity_date'],'run' => $run,]);++$count;🟡
rows_upsertedsurcompte les SIRET dupliqués du même lot.++$countcompte parexecuteStatement, pas par carrier distinct. La source est « sale » : deux SIRET bruts distincts peuvent se normaliser aux mêmes chiffres et fusionner viaON CONFLICT, tout en comptant 2. Le journalrows_upsertedsurestime alors le nombre réel de transporteurs.➡️ Dédupliquer par SIRET en amont (dernier gagnant) — ça corrige aussi le compteur naturellement.
@@ -0,0 +208,4 @@private function deactivateMissing(string $run): int{return (int) $this->connection->executeStatement('UPDATE qualimat_carrier SET is_active = FALSE WHERE is_active = TRUE AND last_synced_at < :run',🟠 Runs concurrents : le run le plus tardif désactive ce que le précédent vient d'insérer.
$runest un timestamp par processus. Deux exécutions qui se chevauchent (cron qui déborde >24 h, ou invocation manuelle parallèle) → ceUPDATE ... WHERE last_synced_at < :rundu run B flippe toutes les lignes tamponnées par le run A (run_A < run_B). Aucun verrou.➡️
pg_try_advisory_lock(ou lock fichier) en tête de commande, abort si déjà tenu.