feat(transport) : endpoint recherche QualimatCarrier (ERP-156) #114
Reference in New Issue
Block a user
Delete Branch "feat/erp-156-qualimat-search"
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?
Entité lecture seule + GET /api/qualimat_carriers?search=. Ticket ERP-156.
Recherche fuzzy name (+ siret), restreinte aux lignes actives (is_active = true), triée name ASC, paginée (règle n°13). Security transport.carriers.view. Aucune écriture exposée. Mapping ORM inchangé (schema:update reste no-op).
Implémentation : QualimatCarrierSearchProvider sur la GetCollection + repository de recherche (forçage actif côté serveur, ?search= unifié name/siret). Tests : actifs seuls, tri name, match siret, pagination Hydra, 403 sans permission.
Code review ERP-156 (endpoint recherche QualimatCarrier — saisie assistée RG-4.01) — relue contre spec-back § 4.7.
Verdict : PR petite, propre, aucun constat bloquant. Elle remplace les
ApiFilternatifs par un provider custom — bon choix, car unSearchFilternatif ne sait ni unifiername/siretsous un seul?search=ni imposer côté serveur le filtreis_active. Conforme § 4.7 : fuzzy name+siret, actifs seuls, tri name ASC, pagination Hydra (règle n°13) + échappatoire?pagination=false, lecture seule (aucune opération d'écriture), securitytransport.carriers.view. Échappement des métacaractères LIKE présent. Tests : actifs-seuls+tri, match siret, enveloppe Hydra paginée, 403 sans permission.Quelques 🟡 mineurs en inline (longueur min de recherche / volumétrie du référentiel, injection repo, trous de tests). Rien qui bloque.
@@ -39,2 +41,4 @@// ni imposer cote serveur le filtre actif.new GetCollection(security: "is_granted('transport.carriers.view')",provider: QualimatCarrierSearchProvider::class,🟢 Bon arbitrage : remplacer les
ApiFilternatifs par un provider custom. UnSearchFilternatif ne peut ni unifiername/siretsous un seul?search=, ni forceris_active=truecôté serveur (unBooleanFilterreste désactivable par le client). Le provider garantit ces deux invariants. Read-only confirmé (seulementGet+GetCollection, alimentation parapp:qualimat:sync), securitytransport.carriers.view. RAS.@@ -0,0 +31,4 @@final class QualimatCarrierSearchProvider implements ProviderInterface{public function __construct(#[Autowire(service: 'App\Module\Transport\Infrastructure\Doctrine\DoctrineQualimatCarrierRepository')]🟡 Mineur, identique à mon constat sur #112 (
CarrierProvider:38) : repository injecté via#[Autowire(service: '…\DoctrineQualimatCarrierRepository')](FQCN de l'impl) alors que le type-hint est l'interface. Couple le provider au FQCN de l'impl ; un alias DIQualimatCarrierRepositoryInterface -> Doctrine…serait plus idiomatique. À traiter de façon cohérente avec #112 (même pattern dans les deux providers).@@ -0,0 +59,4 @@$qb->setFirstResult($offset)->setMaxResults($limit);// fetchJoinCollection: false — aucune jointure to-many (referentiel plat).return new Paginator(new DoctrinePaginator($qb->getQuery(), fetchJoinCollection: false));🟢 Pagination Hydra correcte (jumeau de
CarrierProvider) :Paginatorwrappant leDoctrinePaginator,fetchJoinCollection: falsejustifié (référentiel plat, pas de to-many), et échappatoire?pagination=falsegérée (l.50-53).totalItems/viewpréservés. RAS.@@ -0,0 +43,4 @@*/private function applySearch(QueryBuilder $qb, ?string $search): void{if (null === $search || '' === trim($search)) {🟡 Durcissement optionnel (volumétrie).
qualimat_carrierest un référentiel synchronisé quotidiennement depuis qualimat.org → potentiellement plusieurs milliers de lignes. Or :?search=vide (ou très court) renvoie toutes les lignes actives (paginées à 10, OK) ;?pagination=falsesur cet endpoint renverrait l'intégralité du référentiel en une fois (deep-fetch).La spec § 4.7 ne fixe pas de longueur minimale, donc tu es conforme. Mais pour une saisie assistée, envisager : (a) une longueur min de recherche (ex. ≥2 caractères, sinon collection vide) côté provider, et/ou (b) ne pas exposer
?pagination=falseici (contrairement aux petits référentiels clients/fournisseurs). Non bloquant.@@ -0,0 +103,4 @@self::assertCount(2, $data['member'], 'La page doit etre bornee a itemsPerPage=2.');}public function testForbiddenWithoutPermission(): void🟡 Trous de couverture (mineurs, non bloquants) :
?pagination=false(chemin de code distinct retournant un array — non exercé, comme sur #112) ;?search=%doit être traité littéralement — la logique existe l.50 mais n'est pas testée).Le cœur du contrat (actifs-seuls, tri, siret, Hydra, 403) est bien couvert.
456c6682b0todf3b924b17df3b924b17to397fb22c62POST /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.