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,101 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Transport\Infrastructure\Doctrine;
|
||||
|
||||
use App\Module\Transport\Domain\Entity\Carrier;
|
||||
use App\Module\Transport\Domain\Repository\CarrierRepositoryInterface;
|
||||
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Doctrine\Persistence\ManagerRegistry;
|
||||
|
||||
/**
|
||||
* @extends ServiceEntityRepository<Carrier>
|
||||
*/
|
||||
class DoctrineCarrierRepository extends ServiceEntityRepository implements CarrierRepositoryInterface
|
||||
{
|
||||
public function __construct(ManagerRegistry $registry)
|
||||
{
|
||||
parent::__construct($registry, Carrier::class);
|
||||
}
|
||||
|
||||
public function findById(int $id): ?Carrier
|
||||
{
|
||||
return $this->find($id);
|
||||
}
|
||||
|
||||
public function save(Carrier $carrier): void
|
||||
{
|
||||
$this->getEntityManager()->persist($carrier);
|
||||
$this->getEntityManager()->flush();
|
||||
}
|
||||
|
||||
public function createListQueryBuilder(
|
||||
bool $includeArchived = false,
|
||||
?string $search = null,
|
||||
array $certificationTypes = [],
|
||||
): QueryBuilder {
|
||||
// Fetch-join de la SEULE relation ManyToOne qualimatCarrier (sur, pas de
|
||||
// cartesien) pour exposer statut/date de validite QUALIMAT en liste sans
|
||||
// N+1 (§ 2.11). Aucune sous-collection (addresses/contacts/prices) jointe
|
||||
// en liste : elles ne sont embarquees qu'au detail (carrier:item:read).
|
||||
$qb = $this->createQueryBuilder('c')
|
||||
->leftJoin('c.qualimatCarrier', 'q')->addSelect('q')
|
||||
->andWhere('c.deletedAt IS NULL')
|
||||
->orderBy('c.name', 'ASC')
|
||||
;
|
||||
|
||||
// Pas de cloisonnement par site (§ 2.3) : referentiel global.
|
||||
if (!$includeArchived) {
|
||||
$qb->andWhere('c.isArchived = false');
|
||||
}
|
||||
|
||||
$this->applySearch($qb, $search);
|
||||
$this->applyCertificationTypes($qb, $certificationTypes);
|
||||
|
||||
return $qb;
|
||||
}
|
||||
|
||||
/**
|
||||
* Recherche fuzzy insensible a la casse sur le nom du transporteur (§ 4.1).
|
||||
* Metacaracteres LIKE (%, _, \) echappes pour rester litteraux.
|
||||
*/
|
||||
private function applySearch(QueryBuilder $qb, ?string $search): void
|
||||
{
|
||||
if (null === $search || '' === trim($search)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$escaped = str_replace(['\\', '%', '_'], ['\\\\', '\%', '\_'], trim($search));
|
||||
$pattern = '%'.mb_strtolower($escaped, 'UTF-8').'%';
|
||||
|
||||
$qb->andWhere('LOWER(c.name) LIKE :search')
|
||||
->setParameter('search', $pattern)
|
||||
;
|
||||
}
|
||||
|
||||
/**
|
||||
* Restreint aux transporteurs dont la certification figure dans la liste (OR).
|
||||
* Alimente le filtre « Certification » de la liste (§ 4.1).
|
||||
*
|
||||
* @param list<string> $certificationTypes
|
||||
*/
|
||||
private function applyCertificationTypes(QueryBuilder $qb, array $certificationTypes): void
|
||||
{
|
||||
$codes = [];
|
||||
foreach ($certificationTypes as $code) {
|
||||
if (is_string($code) && '' !== trim($code)) {
|
||||
$codes[] = trim($code);
|
||||
}
|
||||
}
|
||||
|
||||
if ([] === $codes) {
|
||||
return;
|
||||
}
|
||||
|
||||
$qb->andWhere('c.certificationType IN (:certificationTypes)')
|
||||
->setParameter('certificationTypes', $codes)
|
||||
;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user