76e7a59ba7
Logique métier d'écriture et de lecture du ticket de pesée (M5).
Processor (POST/PATCH) :
- résolution du site courant (CurrentSiteProvider) + attribution du numéro
{siteCode}-TP-{NNNN} à la création, immuables ensuite (RG-5.02 / RG-5.09) ;
- exclusivité de la contrepartie CLIENT/FOURNISSEUR/AUTRE — null-ification des
champs hors-branche (RG-5.03, garde-fou CHECK Postgres) ;
- normalisation immatriculation trim/UPPER + masque XX-000-XX hors « Tout
format », 422 inline sur le champ si invalide (RG-5.01 / RG-5.10) ;
- DSD autoritaire pour les pesées AUTO via DsdAllocator (verrou), MANUEL conservé
(RG-5.04) ;
- poids net = plein − vide recalculé (RG-5.05).
Provider (GET) : liste paginée (Paginator ORM, règle n°13), recherche ?search=,
tri ?order[displayDate], cloisonnement par site courant appliqué dans le provider
(le SiteScopedQueryExtension ne traverse pas un provider custom), fetch-join
client/supplier/site anti-N+1, 404 hors périmètre / soft-delete.
Ajouts : WeighingTicketNumberAllocator (compteur weighing_ticket_counter,
SELECT FOR UPDATE), WeighingTicketFieldNormalizer, InvalidImmatriculationException
+ alias DI.
make test vert (811), Architecture vert (CollectionsArePaginatedTest).
78 lines
2.7 KiB
PHP
78 lines
2.7 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Module\Logistique\Infrastructure\Doctrine;
|
|
|
|
use App\Module\Logistique\Domain\Entity\WeighingTicket;
|
|
use App\Module\Logistique\Domain\Repository\WeighingTicketRepositoryInterface;
|
|
use Doctrine\Bundle\DoctrineBundle\Repository\ServiceEntityRepository;
|
|
use Doctrine\ORM\QueryBuilder;
|
|
use Doctrine\Persistence\ManagerRegistry;
|
|
|
|
/**
|
|
* @extends ServiceEntityRepository<WeighingTicket>
|
|
*/
|
|
class DoctrineWeighingTicketRepository extends ServiceEntityRepository implements WeighingTicketRepositoryInterface
|
|
{
|
|
public function __construct(ManagerRegistry $registry)
|
|
{
|
|
parent::__construct($registry, WeighingTicket::class);
|
|
}
|
|
|
|
public function findById(int $id): ?WeighingTicket
|
|
{
|
|
return $this->find($id);
|
|
}
|
|
|
|
public function save(WeighingTicket $ticket): void
|
|
{
|
|
$this->getEntityManager()->persist($ticket);
|
|
$this->getEntityManager()->flush();
|
|
}
|
|
|
|
public function createListQueryBuilder(?string $search = null): QueryBuilder
|
|
{
|
|
// Fetch-join (addSelect) des relations ManyToOne client / supplier / site :
|
|
// sert a la fois la recherche par nom et l'anti-N+1 a la serialisation
|
|
// (§ 4.0 / § 4.1) — aucune demultiplication de lignes (cardinalite to-one).
|
|
// Le cloisonnement par site courant n'est PAS pose ici : un provider custom
|
|
// court-circuite le SiteScopedQueryExtension, le WeighingTicketProvider
|
|
// l'applique donc lui-meme (§ 2.3). Tri par defaut number DESC.
|
|
$qb = $this->createQueryBuilder('wt')
|
|
->leftJoin('wt.client', 'c')->addSelect('c')
|
|
->leftJoin('wt.supplier', 's')->addSelect('s')
|
|
->leftJoin('wt.site', 'st')->addSelect('st')
|
|
->andWhere('wt.deletedAt IS NULL')
|
|
->orderBy('wt.number', 'DESC')
|
|
;
|
|
|
|
$this->applySearch($qb, $search);
|
|
|
|
return $qb;
|
|
}
|
|
|
|
/**
|
|
* Recherche fuzzy insensible a la casse sur le numero, le nom du client /
|
|
* fournisseur, le libelle « Autre » et l'immatriculation (§ 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(wt.number) LIKE :search '
|
|
.'OR LOWER(c.companyName) LIKE :search '
|
|
.'OR LOWER(s.companyName) LIKE :search '
|
|
.'OR LOWER(wt.otherLabel) LIKE :search '
|
|
.'OR LOWER(wt.immatriculation) LIKE :search',
|
|
)->setParameter('search', $pattern);
|
|
}
|
|
}
|