feat(transport) : schéma + entités Carrier + contrat lecture (ERP-155/157) #112
Reference in New Issue
Block a user
Delete Branch "feat/erp-155-carrier-schema-entities"
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-155 / ERP-157 (WT3) — Schéma + entités Carrier + contrat de lecture
Pose le schéma BDD du répertoire transporteurs (M4), les entités, et le contrat JSON de lecture (liste + détail) qui débloque le front. Scope : lecture seule (écriture POST/PATCH + Processor = WT4+).
Livré
Version20260615150000: tablescarrier,carrier_address,carrier_contact,carrier_price(FK cross-module, CHECK enum, index partieluq_carrier_name_active,COMMENT ON COLUMNpartout).uploaded_document(ERP-154) etqualimat_carrier(ERP-39) réutilisées.Carrier/CarrierAddress/CarrierContact/CarrierPrice(#[Auditable], Timestampable/Blamable) + ApiResource lecture (GetCollection+GetviaCarrierProvider: anti-N+1, exclusion archivés,?includeArchived,?search,?certificationType).QualimatCarrier: mapping ORM lecture seule sur la table référentielle existante (sortie duschema_filter, mapping aligné au DDL ERP-39 →schema:updateno-op) + endpoint de recherche read-only (§ 4.7).Shared(ClientInterface,SupplierInterface,ClientAddressInterface,SupplierAddressInterface) +resolve_target_entities. Groupesupplier_address:readajouté àSupplierAddress(M2 avaitsupplier:item:read, M1 avait déjàclient_address:read).ColumnCommentsCatalog(carrier* + qualimat_carrier),makefile test-db-setup(index partiel), i18n audittransport_carrier*,EntitiesAreTimestampableBlamableTest(QualimatCarrier whitelisté).CarrierSerializationContractTest: contrat JSON vérifié (embeds objet vs IRI, booléensisArchived/isChartered, enveloppe Hydra AP4). JSON réel capturé dansspec-back § 4.0.bis(signal de démarrage front).Décision tranchée (cf. spec)
Conflit règle n°1 ↔ relations cross-module
CarrierPricerésolu par l'option contrats Shared (validée avec Matthieu) plutôt que report ou import direct. Détail dans la conversation.Vérifications
make db-resetOK (schema:update no-op pour carrier* et qualimat_carrier).make test: 731 verts (dont contrat Carrier + matrices RBAC M1/M2/M3 inchangées).make nuxt-test: 480 verts.make php-cs-fixer-allow-risky: OK.Base
PR empilée sur
feat/erp-153-rbac(WT2). Se reciblera surdevelopà son merge.Code review ERP-155/157 (schéma + entités Carrier + contrat de lecture) — relue contre la spec
docs/specs/M4-transporteurs/spec-back.mdet les conventions backend (garde-fous Architecture).Verdict : PR solide, aucun constat bloquant. Les deux exigences critiques sont pleinement respectées :
CarrierPricene référence client/fournisseur/site que via les contratsShared+resolve_target_entities— zérouse App\Module\Commercial\….CarrierProviderretourne unPaginator(jamais d'array brut sur le chemin paginé) + échappatoire?pagination=false.Garde-fous vérifiés OK : 100 % des colonnes commentées (
ColumnsHaveSqlCommentTest), Timestampable/Blamable +QualimatCarrierwhitelisté, libellés i18n audit présents, enums alignés sur spec-back. Tests : le contrat de lecture est bien couvert (enveloppe Hydra, pagination réelle, filtre archives, boolean trap, embeds cross-module, 403 RBAC).Rappel de scope (légitime) : cette PR est read-only — pas de
#[Assert\*], POST/PATCH ni Processor (reportés à ERP-158). ⚠️ Le miroirAssert\Length(max)↔ORM\Column(length)et les messages FR seront donc à vérifier impérativement sur la PR d'écriture.Quelques points mineurs en commentaires inline (cohérence filtre archives, INT vs BIGINT à confirmer, injection repo, trous de tests). Rien qui bloque le merge.
@@ -0,0 +31,4 @@* racine garantit l'ordre apres la creation de ces tables sur base vide.** Decision IDs (spec § 2.2, tranchee a ce ticket) : carrier et ses sous-tables* utilisent `INT GENERATED BY DEFAULT AS IDENTITY` (homogeneite globale Starseed🟡 Écart spec à confirmer (PO). La spec-back § 2.2/§ 3.2 préconise
BIGINTpour les PK des tables M4 (homogénéité intra-module). La migration choisitINT GENERATED … IDENTITY(saufqualimat_carrier_id BIGINTpour matcher la PK ciblée). Le choix est documenté ici (homogénéité globale Starseed M1/M2/M3 + évite la friction bigint→string de l'ORM) et techniquement sain. La spec qualifie elle-même ce point de « raffinement non bloquant ». → juste s'assurer que la divergence avec la spec écrite a bien été actée.@@ -0,0 +65,4 @@private ?string $direction = null;// === Branche CLIENT (RG-4.10) ===#[ORM\ManyToOne(targetEntity: ClientInterface::class)]🟢 Isolation inter-modules (règle ABSOLUE n°1) parfaitement respectée :
targetEntity: ClientInterface::class(+ Supplier/Site/…Address) au lieu d'un import direct du module Commercial. Le mapping concret passe parresolve_target_entities(doctrine.yaml). C'est exactement le mécanisme attendu — vérifié : aucunuse App\Module\Commercial\…dans tout le module Transport.@@ -0,0 +35,4 @@final class CarrierProvider implements ProviderInterface{public function __construct(#[Autowire(service: 'App\Module\Transport\Infrastructure\Doctrine\DoctrineCarrierRepository')]🟡 Mineur (découplage). Le repository est injecté via
#[Autowire(service: 'App\Module\Transport\Infrastructure\Doctrine\DoctrineCarrierRepository')]alors que le type-hint est l'interface. Ça marche, mais ça couple le provider au FQCN de l'impl. Un alias DICarrierRepositoryInterface -> DoctrineCarrierRepository(ou l'autowiring par interface) serait plus idiomatique et évite de répéter le FQCN. Non bloquant.@@ -0,0 +57,4 @@private function provideCollection(Operation $operation, array $context): array|Paginator{$filters = $context['filters'] ?? [];$includeArchived = $this->readBool($filters['includeArchived'] ?? false);🟡 Cohérence filtre archives avec les 3 autres répertoires. Ce provider n'expose que
?includeArchived=true. OrClientProvider,SupplierProvideretProviderProviderexposent aussi?archivedOnly=true(afficher uniquement les archivés) — c'est précisément le toggle « Voir les archivés » aligné par ERP-173.La spec-back M4 § 2.4 (rédigée avant ERP-173) ne mentionne que
includeArchived, donc tu es conforme à ta spec. Mais le front Répertoire (ERP-164) réutilisera le composant « Voir les archivés » des autres écrans, qui envoiearchivedOnly→ le toggle sera silencieusement inopérant côté transporteurs.Reco : ajouter le paramètre
archivedOnly(prioritaire surincludeArchived, commeProviderProvider/DoctrineProviderRepository) pour rester l'exact « jumeau » annoncé du SupplierProvider.@@ -0,0 +81,4 @@// fetchJoinCollection: false — la seule jointure est un ManyToOne (sur),// pas une to-many : pas de besoin du mode collection du Paginator.return new Paginator(new DoctrinePaginator($qb->getQuery(), fetchJoinCollection: false));🟢 Pagination Hydra correcte :
Paginatorwrappant leDoctrinePaginator,totalItems/viewpréservés,fetchJoinCollection: falsejustifié (seule jointure = ManyToOnequalimatCarrier, pas de cartésien to-many). Échappatoire?pagination=falsegérée proprement (l.71-74). RAS.@@ -461,0 +479,4 @@],// === M4 Transport — repertoire transporteurs (ERP-155/157) ==='carrier' => [🟡 Risque de drift (mineur, pattern projet existant). Les textes de
COMMENT ON COLUMNsont dupliqués mot pour mot ici ET dans la migration (Version20260615150000.php). Actuellement identiques, mais rien ne teste leur égalité — une évolution future peut les désynchroniser silencieusement. C'est déjà le pattern M1/M2/M3, donc acceptable pour ce ticket ; à garder en tête (un test d'égalité catalog↔migration, ou une source unique, supprimerait le risque).@@ -0,0 +23,4 @@{// === Enveloppe AP4 + exclusion des archives (§ 4.1) ===public function testCollectionEnvelopeShapeAndArchivedExcluded(): void🟡 Trous de couverture (non bloquants pour un contrat de lecture, à compléter avec la consolidation des filtres en ERP-162/163) :
?certificationType=(feature livrée repo+provider mais non exercée) ;?pagination=false(chemin de code distinct retournant un array) ;name ASC;401(non authentifié) — seul le403est couvert, alors que la spec § 4.1 liste les deux.Le reste du contrat est très bien couvert (Hydra, pagination réelle, archives, embeds cross-module sur les 2 branches, boolean trap, 403).
0f62deb80ftoaa23189fe1