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.
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Domain\Contract;
|
||||
|
||||
/**
|
||||
* Contrat minimal d'une adresse de Client (M1 Commercial) exposable a un autre
|
||||
* module sans couplage direct (regle ABSOLUE n°1). Mappe vers
|
||||
* App\Module\Commercial\Domain\Entity\ClientAddress via `resolve_target_entities`.
|
||||
*
|
||||
* Implemente par App\Module\Commercial\Domain\Entity\ClientAddress. Utilise
|
||||
* comme type-hint des relations ORM cross-module (ex: CarrierPrice.clientDeliveryAddress,
|
||||
* M4). La serialisation passe par le read-group de l'entite concrete
|
||||
* (client_address:read), pas par cette interface.
|
||||
*/
|
||||
interface ClientAddressInterface
|
||||
{
|
||||
public function getId(): ?int;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Domain\Contract;
|
||||
|
||||
/**
|
||||
* Contrat minimal exposant ce qu'un autre module doit connaitre d'un Client
|
||||
* (M1 Commercial) sans creer de couplage direct vers le module Commercial
|
||||
* (regle ABSOLUE n°1). Mappe vers App\Module\Commercial\Domain\Entity\Client
|
||||
* via `resolve_target_entities` (doctrine.yaml).
|
||||
*
|
||||
* Implemente par App\Module\Commercial\Domain\Entity\Client. Utilise comme
|
||||
* type-hint dans les relations ORM cross-module (ex: CarrierPrice.client, M4).
|
||||
* La serialisation passe par les read-groups de l'entite concrete (client:read),
|
||||
* pas par cette interface.
|
||||
*/
|
||||
interface ClientInterface
|
||||
{
|
||||
public function getId(): ?int;
|
||||
|
||||
public function getCompanyName(): ?string;
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Domain\Contract;
|
||||
|
||||
/**
|
||||
* Contrat minimal d'une adresse de Supplier (M2 Commercial) exposable a un autre
|
||||
* module sans couplage direct (regle ABSOLUE n°1). Mappe vers
|
||||
* App\Module\Commercial\Domain\Entity\SupplierAddress via `resolve_target_entities`.
|
||||
*
|
||||
* Implemente par App\Module\Commercial\Domain\Entity\SupplierAddress. Utilise
|
||||
* comme type-hint des relations ORM cross-module (ex: CarrierPrice.supplierSupplyAddress,
|
||||
* M4). La serialisation passe par le read-group de l'entite concrete
|
||||
* (supplier_address:read), pas par cette interface.
|
||||
*/
|
||||
interface SupplierAddressInterface
|
||||
{
|
||||
public function getId(): ?int;
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Shared\Domain\Contract;
|
||||
|
||||
/**
|
||||
* Contrat minimal exposant ce qu'un autre module doit connaitre d'un Supplier
|
||||
* (M2 Commercial) sans creer de couplage direct vers le module Commercial
|
||||
* (regle ABSOLUE n°1). Mappe vers App\Module\Commercial\Domain\Entity\Supplier
|
||||
* via `resolve_target_entities` (doctrine.yaml).
|
||||
*
|
||||
* Implemente par App\Module\Commercial\Domain\Entity\Supplier. Utilise comme
|
||||
* type-hint dans les relations ORM cross-module (ex: CarrierPrice.supplier, M4).
|
||||
* La serialisation passe par les read-groups de l'entite concrete (supplier:read),
|
||||
* pas par cette interface.
|
||||
*/
|
||||
interface SupplierInterface
|
||||
{
|
||||
public function getId(): ?int;
|
||||
|
||||
public function getCompanyName(): ?string;
|
||||
}
|
||||
@@ -458,6 +458,86 @@ final class ColumnCommentsCatalog
|
||||
'iban' => 'IBAN du compte (≤ 34 caracteres).',
|
||||
'position' => 'Ordre d affichage du RIB dans la liste du prestataire (croissant).',
|
||||
] + self::timestampableBlamableComments(),
|
||||
|
||||
// === M4 Transport — referentiel QUALIMAT (ERP-39, mappe lecture seule des ERP-155) ===
|
||||
// Mappe par l'entite QualimatCarrier depuis M4 -> retire du schema_filter,
|
||||
// donc ses COMMENT sont rejoues par app:apply-column-comments apres schema:update.
|
||||
'qualimat_carrier' => [
|
||||
'_table' => "Referentiel des transporteurs agrees QUALIMAT, synchronise quotidiennement depuis l'API qualimat.org (type=operateur_transport).",
|
||||
'id' => 'Cle technique auto-incrementee.',
|
||||
'siret' => 'SIRET normalise (chiffres sans espaces). Cle naturelle de synchro (unique). Source parfois incomplete (longueur variable), non contrainte a 14.',
|
||||
'name' => 'Raison sociale du transporteur (champs Nom = Societe de la source, identiques).',
|
||||
'address' => 'Adresse postale (voie). Nullable.',
|
||||
'postal_code' => 'Code postal. Nullable.',
|
||||
'city' => 'Ville. Nullable.',
|
||||
'phone' => 'Telephone au format source "indicatif|numero" (ex: +33|0608890316). Nullable.',
|
||||
'department' => 'Departement au format source "code - libelle" (ex: 65 - Hautes-Pyrenees). Nullable.',
|
||||
'status' => "Statut d'agrement QUALIMAT (valeurs connues : Audite, Valide, Suspendu). Valeur brute de la source, non contrainte.",
|
||||
'validity_date' => 'Date de fin de validite de la certification (convertie depuis dd/mm/yyyy). Nullable.',
|
||||
'is_active' => 'Faux = transporteur absent du dernier import (soft-delete). Toute ligne non revue par le dernier run passe a FALSE.',
|
||||
'last_synced_at' => 'Horodatage du run de synchro ayant vu cette ligne en dernier (soft-delete : last_synced_at < run courant).',
|
||||
],
|
||||
|
||||
// === M4 Transport — repertoire transporteurs (ERP-155/157) ===
|
||||
'carrier' => [
|
||||
'_table' => 'Repertoire transporteurs (M4 Transport) — entites editables, archivables (is_archived) et soft-deletables (deleted_at). Distinct du referentiel qualimat_carrier.',
|
||||
'id' => 'Identifiant interne auto-incremente.',
|
||||
'qualimat_carrier_id' => 'Lien editable vers le referentiel QUALIMAT (saisie assistee RG-4.01). FK -> qualimat_carrier.id, ON DELETE SET NULL : transporteur conserve si la ligne QUALIMAT disparait.',
|
||||
'name' => 'Raison sociale du transporteur (stockee en MAJUSCULES). Unique case-insensitive parmi les non-archives/non-supprimes (uq_carrier_name_active, RG-4.12 / § 2.6).',
|
||||
'certification_type' => 'Type de certification : QUALIMAT (si lie, lecture seule) ou GMP_PLUS/OVOCOM/COMPTE_PROPRE/AUTRE. AUTRE declenche le champ Decharge (RG-4.02). Null en cas LIOT (RG-4.01).',
|
||||
'is_chartered' => '« Affreter » coche : declenche indexation/benne-fond mouvant/volume, obligatoires (RG-4.03). Faux par defaut.',
|
||||
'indexation_rate' => 'Taux d indexation en pourcentage (NUMERIC 5,2) — renseigne si affrete (RG-4.03).',
|
||||
'container_type' => 'Type de contenant BENNE|FOND_MOUVANT (chk_carrier_container_type) — renseigne si affrete (RG-4.03).',
|
||||
'volume_m3' => 'Volume en m3 (NUMERIC 10,2) — renseigne si affrete (RG-4.03).',
|
||||
'discharge_document_id' => 'Document de Decharge (visible si certification_type = AUTRE, RG-4.02). FK -> uploaded_document.id (infra Shared § 2.7), ON DELETE SET NULL.',
|
||||
'liot_plates' => 'Immatriculations LIOT separees par « ; » (cas special nom=LIOT, RG-4.01). Les autres champs sont masques dans ce cas.',
|
||||
'is_archived' => 'Drapeau fonctionnel d archivage — masque par defaut dans la liste. Bascule via permission transport.carriers.archive (Admin seul).',
|
||||
'archived_at' => 'Horodatage de l archivage — pose quand is_archived passe a vrai, remis a null a la restauration.',
|
||||
'deleted_at' => 'Horodatage du soft-delete technique — non expose par l API au M4. Null = ligne active.',
|
||||
] + self::timestampableBlamableComments(),
|
||||
|
||||
'carrier_address' => [
|
||||
'_table' => 'Adresses d un transporteur (1:n) — onglet Adresse (M4). Pre-remplie depuis QUALIMAT si applicable (RG-4.05).',
|
||||
'id' => 'Identifiant interne auto-incremente.',
|
||||
'carrier_id' => 'FK -> carrier.id, ON DELETE CASCADE — transporteur proprietaire de l adresse.',
|
||||
'country' => 'Pays de l adresse — defaut France.',
|
||||
'postal_code' => 'Code postal (saisie assistee BAN cote front, RG-4.06).',
|
||||
'city' => 'Ville — preremplie depuis le code postal via API BAN cote front.',
|
||||
'street' => 'Numero et voie de l adresse.',
|
||||
'street_complement' => 'Complement d adresse (etage, batiment...) — optionnel.',
|
||||
'position' => 'Ordre d affichage de l adresse dans la liste du transporteur (croissant).',
|
||||
] + self::timestampableBlamableComments(),
|
||||
|
||||
'carrier_contact' => [
|
||||
'_table' => 'Contacts d un transporteur (1:n) — onglet Contact (M4). Au moins un champ rempli (RG-4.08, chk_carrier_contact_filled), max 2 telephones.',
|
||||
'id' => 'Identifiant interne auto-incremente.',
|
||||
'carrier_id' => 'FK -> carrier.id, ON DELETE CASCADE — transporteur proprietaire du contact.',
|
||||
'first_name' => 'Prenom du contact (capitalise serveur). Au moins un champ du contact est requis (RG-4.08).',
|
||||
'last_name' => 'Nom du contact (capitalise serveur). Au moins un champ du contact est requis (RG-4.08).',
|
||||
'job_title' => 'Fonction / intitule de poste du contact (≤ 120 caracteres).',
|
||||
'phone_primary' => 'Telephone principal — chiffres uniquement (normalisation serveur).',
|
||||
'phone_secondary' => 'Telephone secondaire — chiffres uniquement (max 2 telephones, RG-4.08).',
|
||||
'email' => 'Email du contact (lowercase serveur).',
|
||||
'position' => 'Ordre d affichage du contact dans la liste du transporteur (croissant).',
|
||||
] + self::timestampableBlamableComments(),
|
||||
|
||||
'carrier_price' => [
|
||||
'_table' => 'Prix d un transporteur (1:n) — onglet Prix (M4). Branche CLIENT ou FOURNISSEUR selon direction (RG-4.09->4.11, CHECK chk_carrier_price_*).',
|
||||
'id' => 'Identifiant interne auto-incremente.',
|
||||
'carrier_id' => 'FK -> carrier.id, ON DELETE CASCADE — transporteur proprietaire du prix.',
|
||||
'direction' => 'Sens du prix : CLIENT ou FOURNISSEUR (RG-4.09). Pilote l affichage et l obligation des colonnes client_*/supplier_* (RG-4.10/4.11).',
|
||||
'client_id' => 'Branche CLIENT (RG-4.10) : client concerne. FK -> client.id, ON DELETE RESTRICT. Requis ssi direction = CLIENT.',
|
||||
'client_delivery_address_id' => 'Branche CLIENT : adresse de livraison du client. FK -> client_address.id, ON DELETE RESTRICT.',
|
||||
'departure_site_id' => 'Branche CLIENT : adresse de depart = un des 3 sites (86/17/82). FK -> site.id, ON DELETE RESTRICT.',
|
||||
'supplier_id' => 'Branche FOURNISSEUR (RG-4.11) : fournisseur concerne. FK -> supplier.id, ON DELETE RESTRICT. Requis ssi direction = FOURNISSEUR.',
|
||||
'supplier_supply_address_id' => 'Branche FOURNISSEUR : adresse d approvisionnement du fournisseur. FK -> supplier_address.id, ON DELETE RESTRICT.',
|
||||
'delivery_site_id' => 'Branche FOURNISSEUR : adresse de livraison = un des 3 sites (86/17/82). FK -> site.id, ON DELETE RESTRICT.',
|
||||
'container_type' => 'Type de contenant BENNE|FOND_MOUVANT (chk_carrier_price_container).',
|
||||
'pricing_unit' => 'Unite de tarification FORFAIT|TONNE (chk_carrier_price_unit).',
|
||||
'price' => 'Montant du prix (NUMERIC 12,2).',
|
||||
'price_state' => 'Etat du prix : EN_COURS, VALIDE ou NON_VALIDE (chk_carrier_price_state). Affiche dans le tableau Prix.',
|
||||
'position' => 'Ordre d affichage du prix dans la liste du transporteur (croissant).',
|
||||
] + self::timestampableBlamableComments(),
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user