feat(logistique) : WeighingTicketProvider + Processor — numérotation, contrepartie, net, normalisation (ERP-185)
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).
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Logistique\Application\Service;
|
||||
|
||||
use App\Module\Logistique\Domain\Exception\InvalidImmatriculationException;
|
||||
|
||||
/**
|
||||
* Normalisation serveur des champs texte d'un WeighingTicket, appliquee par le
|
||||
* WeighingTicketProcessor AVANT persistance. Cf. spec-back M5 § 6 + RG-5.01 /
|
||||
* RG-5.10. Jumeau leger de CarrierFieldNormalizer (M4).
|
||||
*
|
||||
* - immatriculation (RG-5.01 / RG-5.10) : trim + UPPER. Si « Tout format » N'EST
|
||||
* PAS coche (freeFormat = false), la saisie est ramenee au masque SIV
|
||||
* canonique XX-000-XX (separateurs/espaces ignores a la saisie, re-poses) ; une
|
||||
* plaque qui ne s'y conforme pas leve InvalidImmatriculationException (-> 422
|
||||
* par le Processor). En « Tout format » (anciennes plaques, etranger, engins),
|
||||
* seul le trim + UPPER s'applique.
|
||||
* - otherLabel (RG-5.03) : trim ; une chaine vide apres trim devient null (evite
|
||||
* de persister "" dans une colonne nullable).
|
||||
*
|
||||
* Methodes null-safe : une entree null ressort null (l'obligation eventuelle est
|
||||
* portee par les Assert de l'entite / la coherence contrepartie, pas ici).
|
||||
*/
|
||||
final class WeighingTicketFieldNormalizer
|
||||
{
|
||||
/**
|
||||
* Plaque SIV « nue » (sans separateurs) : 2 lettres, 3 chiffres, 2 lettres.
|
||||
* Les lettres interdites du SIV (I, O, U + SS) ne sont pas filtrees ici : le
|
||||
* masque de saisie reste volontairement simple (le metier accepte ces cas via
|
||||
* « Tout format » si besoin).
|
||||
*/
|
||||
private const string SIV_BARE_PATTERN = '/^[A-Z]{2}[0-9]{3}[A-Z]{2}$/';
|
||||
|
||||
/**
|
||||
* Normalise l'immatriculation (RG-5.01 / RG-5.10).
|
||||
*
|
||||
* @param bool $freeFormat « Tout format » coche -> masque SIV desactive
|
||||
*
|
||||
* @throws InvalidImmatriculationException si !freeFormat et la plaque ne
|
||||
* respecte pas le masque XX-000-XX
|
||||
*/
|
||||
public function normalizeImmatriculation(?string $value, bool $freeFormat): ?string
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = mb_strtoupper(trim($value), 'UTF-8');
|
||||
if ('' === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// « Tout format » : aucune contrainte de masque (RG-5.01).
|
||||
if ($freeFormat) {
|
||||
return $value;
|
||||
}
|
||||
|
||||
// Masque SIV : on ignore tout ce qui n'est pas alphanumerique (l'operateur
|
||||
// peut saisir « ab123cd », « AB 123 CD » ou « AB-123-CD ») puis on valide
|
||||
// le squelette 2-3-2 et on repose les separateurs canoniques.
|
||||
$bare = preg_replace('/[^A-Z0-9]/', '', $value) ?? '';
|
||||
|
||||
if (1 !== preg_match(self::SIV_BARE_PATTERN, $bare)) {
|
||||
throw new InvalidImmatriculationException(
|
||||
'Format d\'immatriculation invalide : attendu XX-000-XX (cochez « Tout format » pour une plaque libre).',
|
||||
);
|
||||
}
|
||||
|
||||
return sprintf('%s-%s-%s', substr($bare, 0, 2), substr($bare, 2, 3), substr($bare, 5, 2));
|
||||
}
|
||||
|
||||
/**
|
||||
* Trim du libelle « Autre » (RG-5.03). Une chaine vide apres trim devient null.
|
||||
*/
|
||||
public function normalizeOtherLabel(?string $value): ?string
|
||||
{
|
||||
if (null === $value) {
|
||||
return null;
|
||||
}
|
||||
|
||||
$value = trim($value);
|
||||
|
||||
return '' === $value ? null : $value;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Module\Logistique\Application\Service;
|
||||
|
||||
use App\Module\Sites\Domain\Entity\Site;
|
||||
|
||||
/**
|
||||
* Allocateur du numero de ticket de pesee (RG-5.02, § 2.5).
|
||||
*
|
||||
* Le numero a le format {siteCode}-TP-{NNNN} (ex. 86-TP-0001), UNIQUE PAR SITE
|
||||
* et immuable. Chaque site porte sa propre sequence : 86-TP-0001 et 17-TP-0001
|
||||
* coexistent.
|
||||
*
|
||||
* Le code du site (prefixe) vit sur l'entite Site (site.code, ERP-183) — d'ou le
|
||||
* type-hint sur Site concret (et non SiteInterface qui n'expose pas getCode()) ;
|
||||
* c'est la meme reference ORM partagee que celle consommee par WeighingTicket
|
||||
* (§ 2.1, pas de logique inter-module).
|
||||
*
|
||||
* Port (couche Application) ; l'implementation (WeighingTicketNumberAllocator,
|
||||
* Infrastructure) incremente le compteur weighing_ticket_counter sous verrou ligne
|
||||
* `SELECT ... FOR UPDATE` pour garantir l'unicite meme en concurrence.
|
||||
*/
|
||||
interface WeighingTicketNumberAllocatorInterface
|
||||
{
|
||||
/**
|
||||
* Attribue et renvoie le prochain numero formate {siteCode}-TP-{NNNN} pour le
|
||||
* site, en persistant l'increment de maniere atomique (verrou ligne).
|
||||
*/
|
||||
public function allocate(Site $site): string;
|
||||
}
|
||||
Reference in New Issue
Block a user