feat(transport) : schéma + entités Carrier + contrat lecture (ERP-155/157) #112

Merged
matthieu merged 3 commits from feat/erp-155-carrier-schema-entities into feat/erp-153-rbac 2026-06-16 13:47:40 +00:00
Owner

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é

  • Migration Version20260615150000 : tables carrier, carrier_address, carrier_contact, carrier_price (FK cross-module, CHECK enum, index partiel uq_carrier_name_active, COMMENT ON COLUMN partout). uploaded_document (ERP-154) et qualimat_carrier (ERP-39) réutilisées.
  • Entités Carrier / CarrierAddress / CarrierContact / CarrierPrice (#[Auditable], Timestampable/Blamable) + ApiResource lecture (GetCollection + Get via CarrierProvider : anti-N+1, exclusion archivés, ?includeArchived, ?search, ?certificationType).
  • QualimatCarrier : mapping ORM lecture seule sur la table référentielle existante (sortie du schema_filter, mapping aligné au DDL ERP-39 → schema:update no-op) + endpoint de recherche read-only (§ 4.7).
  • Relations cross-module des prix (Client/Supplier/adresses) câblées sans import inter-module (règle ABSOLUE n°1) via contrats Shared (ClientInterface, SupplierInterface, ClientAddressInterface, SupplierAddressInterface) + resolve_target_entities. Groupe supplier_address:read ajouté à SupplierAddress (M2 avait supplier:item:read, M1 avait déjà client_address:read).
  • Garde-fous : ColumnCommentsCatalog (carrier* + qualimat_carrier), makefile test-db-setup (index partiel), i18n audit transport_carrier*, EntitiesAreTimestampableBlamableTest (QualimatCarrier whitelisté).
  • CarrierSerializationContractTest : contrat JSON vérifié (embeds objet vs IRI, booléens isArchived/isChartered, enveloppe Hydra AP4). JSON réel capturé dans spec-back § 4.0.bis (signal de démarrage front).

Décision tranchée (cf. spec)

Conflit règle n°1 ↔ relations cross-module CarrierPrice ré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-reset OK (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 sur develop à son merge.

⚠ Reste WT4+ : Processor d'écriture (RG-4.01→4.14, 409 doublon, gating archive), sous-ressources adresses/contacts/prix, export.

## 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é - **Migration** `Version20260615150000` : tables `carrier`, `carrier_address`, `carrier_contact`, `carrier_price` (FK cross-module, CHECK enum, index partiel `uq_carrier_name_active`, `COMMENT ON COLUMN` partout). `uploaded_document` (ERP-154) et `qualimat_carrier` (ERP-39) réutilisées. - **Entités** `Carrier` / `CarrierAddress` / `CarrierContact` / `CarrierPrice` (`#[Auditable]`, Timestampable/Blamable) + **ApiResource lecture** (`GetCollection` + `Get` via `CarrierProvider` : anti-N+1, exclusion archivés, `?includeArchived`, `?search`, `?certificationType`). - **`QualimatCarrier`** : mapping ORM **lecture seule** sur la table référentielle existante (sortie du `schema_filter`, mapping aligné au DDL ERP-39 → `schema:update` no-op) + endpoint de recherche read-only (§ 4.7). - **Relations cross-module des prix** (Client/Supplier/adresses) câblées **sans import inter-module** (règle ABSOLUE n°1) via contrats `Shared` (`ClientInterface`, `SupplierInterface`, `ClientAddressInterface`, `SupplierAddressInterface`) + `resolve_target_entities`. Groupe `supplier_address:read` ajouté à `SupplierAddress` (M2 avait `supplier:item:read`, M1 avait déjà `client_address:read`). - **Garde-fous** : `ColumnCommentsCatalog` (carrier* + qualimat_carrier), `makefile test-db-setup` (index partiel), i18n audit `transport_carrier*`, `EntitiesAreTimestampableBlamableTest` (QualimatCarrier whitelisté). - **`CarrierSerializationContractTest`** : contrat JSON vérifié (embeds objet vs IRI, booléens `isArchived`/`isChartered`, enveloppe Hydra AP4). **JSON réel capturé dans `spec-back § 4.0.bis`** (signal de démarrage front). ### Décision tranchée (cf. spec) Conflit règle n°1 ↔ relations cross-module `CarrierPrice` ré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-reset` OK (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 sur `develop` à son merge. > ⚠ Reste WT4+ : Processor d'écriture (RG-4.01→4.14, 409 doublon, gating archive), sous-ressources adresses/contacts/prix, export.
matthieu added the backdbfrontM4-Transporteurtype/feat labels 2026-06-15 17:15:50 +00:00
tristan reviewed 2026-06-16 09:18:31 +00:00
tristan left a comment
Owner

Code review ERP-155/157 (schéma + entités Carrier + contrat de lecture) — relue contre la spec docs/specs/M4-transporteurs/spec-back.md et les conventions backend (garde-fous Architecture).

Verdict : PR solide, aucun constat bloquant. Les deux exigences critiques sont pleinement respectées :

  • Isolation inter-modules (règle n°1) : CarrierPrice ne référence client/fournisseur/site que via les contrats Shared + resolve_target_entities — zéro use App\Module\Commercial\….
  • Pagination Hydra (règle n°13) : CarrierProvider retourne un Paginator (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 + QualimatCarrier whitelisté, 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 miroir Assert\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.

Code review ERP-155/157 (schéma + entités Carrier + contrat de lecture) — relue contre la spec `docs/specs/M4-transporteurs/spec-back.md` et les conventions backend (garde-fous Architecture). **Verdict : PR solide, aucun constat bloquant.** Les deux exigences critiques sont pleinement respectées : - **Isolation inter-modules (règle n°1)** : `CarrierPrice` ne référence client/fournisseur/site que via les contrats `Shared` + `resolve_target_entities` — zéro `use App\Module\Commercial\…`. - **Pagination Hydra (règle n°13)** : `CarrierProvider` retourne un `Paginator` (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 + `QualimatCarrier` whitelisté, 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 miroir `Assert\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
Owner

🟡 Écart spec à confirmer (PO). La spec-back § 2.2/§ 3.2 préconise BIGINT pour les PK des tables M4 (homogénéité intra-module). La migration choisit INT GENERATED … IDENTITY (sauf qualimat_carrier_id BIGINT pour 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.

🟡 **Écart spec à confirmer (PO).** La spec-back § 2.2/§ 3.2 préconise `BIGINT` pour les PK des tables M4 (homogénéité intra-module). La migration choisit `INT GENERATED … IDENTITY` (sauf `qualimat_carrier_id BIGINT` pour 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)]
Owner

🟢 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 par resolve_target_entities (doctrine.yaml). C'est exactement le mécanisme attendu — vérifié : aucun use App\Module\Commercial\… dans tout le module Transport.

🟢 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 par `resolve_target_entities` (doctrine.yaml). C'est exactement le mécanisme attendu — vérifié : aucun `use 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')]
Owner

🟡 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 DI CarrierRepositoryInterface -> DoctrineCarrierRepository (ou l'autowiring par interface) serait plus idiomatique et évite de répéter le FQCN. Non bloquant.

🟡 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 DI `CarrierRepositoryInterface -> 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);
Owner

🟡 Cohérence filtre archives avec les 3 autres répertoires. Ce provider n'expose que ?includeArchived=true. Or ClientProvider, SupplierProvider et ProviderProvider exposent 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 envoie archivedOnly → le toggle sera silencieusement inopérant côté transporteurs.

Reco : ajouter le paramètre archivedOnly (prioritaire sur includeArchived, comme ProviderProvider/DoctrineProviderRepository) pour rester l'exact « jumeau » annoncé du SupplierProvider.

🟡 **Cohérence filtre archives avec les 3 autres répertoires.** Ce provider n'expose que `?includeArchived=true`. Or `ClientProvider`, `SupplierProvider` et `ProviderProvider` exposent 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 envoie `archivedOnly` → le toggle sera silencieusement inopérant côté transporteurs. **Reco** : ajouter le paramètre `archivedOnly` (prioritaire sur `includeArchived`, comme `ProviderProvider`/`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));
Owner

🟢 Pagination Hydra correcte : Paginator wrappant le DoctrinePaginator, totalItems/view préservés, fetchJoinCollection: false justifié (seule jointure = ManyToOne qualimatCarrier, pas de cartésien to-many). Échappatoire ?pagination=false gérée proprement (l.71-74). RAS.

🟢 Pagination Hydra correcte : `Paginator` wrappant le `DoctrinePaginator`, `totalItems`/`view` préservés, `fetchJoinCollection: false` justifié (seule jointure = ManyToOne `qualimatCarrier`, pas de cartésien to-many). Échappatoire `?pagination=false` gérée proprement (l.71-74). RAS.
@@ -461,0 +479,4 @@
],
// === M4 Transport — repertoire transporteurs (ERP-155/157) ===
'carrier' => [
Owner

🟡 Risque de drift (mineur, pattern projet existant). Les textes de COMMENT ON COLUMN sont 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).

🟡 Risque de drift (mineur, pattern projet existant). Les textes de `COMMENT ON COLUMN` sont 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
Owner

🟡 Trous de couverture (non bloquants pour un contrat de lecture, à compléter avec la consolidation des filtres en ERP-162/163) :

  • pas de test du filtre ?certificationType= (feature livrée repo+provider mais non exercée) ;
  • pas de test ?pagination=false (chemin de code distinct retournant un array) ;
  • pas de test du tri par défaut name ASC ;
  • pas de test 401 (non authentifié) — seul le 403 est 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).

🟡 Trous de couverture (non bloquants pour un contrat de lecture, à compléter avec la consolidation des filtres en ERP-162/163) : - pas de test du filtre `?certificationType=` (feature livrée repo+provider mais non exercée) ; - pas de test `?pagination=false` (chemin de code distinct retournant un array) ; - pas de test du tri par défaut `name ASC` ; - pas de test `401` (non authentifié) — seul le `403` est 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).
matthieu added 2 commits 2026-06-16 13:15:01 +00:00
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.
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.
matthieu force-pushed feat/erp-155-carrier-schema-entities from 0f62deb80f to aa23189fe1 2026-06-16 13:15:01 +00:00 Compare
matthieu added 1 commit 2026-06-16 13:47:38 +00:00
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).
matthieu merged commit 13d4a08bc9 into feat/erp-153-rbac 2026-06-16 13:47:40 +00:00
matthieu deleted branch feat/erp-155-carrier-schema-entities 2026-06-16 13:47:40 +00:00
Sign in to join this conversation.