e88bb059e6
- WeighbridgeReaderInterface (contrat) + RandomWeighbridgeReader (stub,
poids aléatoire ∈ [10000,50000] kg, RG-5.06) + WeighbridgeUnavailableException
- DsdAllocator : compteur DSD par site (weighbridge_dsd_counter) incrémenté
sous verrou ligne SELECT ... FOR UPDATE (RG-5.04, § 2.7)
- endpoint POST /api/weighbridge_readings : ressource virtuelle
WeighbridgeReadingResource + WeighbridgeReadingProcessor (pas de controller)
- AUTO -> {weight, dsd, mode} ; MANUAL -> {weight, dsd, manualNumber, mode}
- WeighbridgeUnavailableException -> HTTP 503 explicite (RG-5.06)
- site courant via CurrentSiteProviderInterface (contrat Sites)
- is_granted('logistique.weighing_tickets.manage')
- dsd renvoyé prévisionnel : attribution autoritaire refaite à la création
du ticket (ERP-185)
- tests : WeighbridgeReaderStubTest, DsdAllocatorTest, processor (503/400),
WeighbridgeReadingApiTest (RBAC + AUTO/MANUAL + 422)
92 lines
4.0 KiB
PHP
92 lines
4.0 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Module\Logistique\Infrastructure\ApiPlatform\Resource;
|
|
|
|
use ApiPlatform\Metadata\ApiResource;
|
|
use ApiPlatform\Metadata\Post;
|
|
use App\Module\Logistique\Infrastructure\ApiPlatform\State\Processor\WeighbridgeReadingProcessor;
|
|
use Symfony\Component\Serializer\Attribute\Groups;
|
|
use Symfony\Component\Validator\Constraints as Assert;
|
|
use Symfony\Component\Validator\Context\ExecutionContextInterface;
|
|
|
|
/**
|
|
* Ressource API Platform virtuelle (non mappee Doctrine) portant l'action de
|
|
* pesee au pont bascule : `POST /api/weighbridge_readings` (§ 4.2).
|
|
*
|
|
* Action AUTONOME : declenchee depuis le formulaire AVANT que le ticket existe.
|
|
* Le site est resolu serveur (site courant) — jamais envoye par le client.
|
|
*
|
|
* - AUTO (`{ "mode": "AUTO" }`) → `{ weight, dsd, mode }` (stub : poids
|
|
* aleatoire ∈ [10000,50000] kg + DSD du site, RG-5.04 / RG-5.06).
|
|
* - MANUAL (`{ "mode": "MANUAL", "weight": <int>, "manualNumber": "<str>" }`)
|
|
* → `{ weight, dsd, manualNumber, mode }` (DSD = dernier DSD du site + 1).
|
|
*
|
|
* `read: false` : pas de chargement d'entite existante — le payload est
|
|
* denormalise directement dans cette ressource, puis le Processor prend le relais.
|
|
*
|
|
* ⚠ Le `dsd` renvoye ici est PREVISIONNEL : l'attribution AUTORITAIRE du DSD
|
|
* (et du numero de ticket) est refaite/verrouillee a la creation du ticket
|
|
* (`POST /api/weighing_tickets`, ERP-185) pour eviter les collisions si deux
|
|
* postes pesent en parallele. Le front affiche cette valeur, mais c'est le
|
|
* ticket persiste qui fait foi.
|
|
*/
|
|
#[ApiResource(
|
|
shortName: 'WeighbridgeReading',
|
|
operations: [
|
|
new Post(
|
|
uriTemplate: '/weighbridge_readings',
|
|
// Action de lecture du pont (pas une creation de ressource) : 200, pas 201.
|
|
status: 200,
|
|
security: "is_granted('logistique.weighing_tickets.manage')",
|
|
normalizationContext: ['groups' => ['weighbridge_reading:read']],
|
|
denormalizationContext: ['groups' => ['weighbridge_reading:write']],
|
|
processor: WeighbridgeReadingProcessor::class,
|
|
read: false,
|
|
),
|
|
],
|
|
)]
|
|
final class WeighbridgeReadingResource
|
|
{
|
|
/** AUTO (pesee bascule) | MANUAL (pesee manuelle) — pilote le comportement (§ 4.2). */
|
|
#[Assert\NotBlank(message: 'Le mode de pesée est obligatoire.')]
|
|
#[Assert\Choice(choices: ['AUTO', 'MANUAL'], message: 'Mode de pesée invalide (AUTO ou MANUAL).')]
|
|
#[Groups(['weighbridge_reading:write', 'weighbridge_reading:read'])]
|
|
public ?string $mode = null;
|
|
|
|
/**
|
|
* Poids en kg. En entree : requis et saisi en MANUAL, ignore en AUTO (le pont
|
|
* fournit le poids). En sortie : poids effectif de la pesee.
|
|
*/
|
|
#[Assert\Positive(message: 'Le poids doit être un entier positif (kg).')]
|
|
#[Groups(['weighbridge_reading:write', 'weighbridge_reading:read'])]
|
|
public ?int $weight = null;
|
|
|
|
/** Numero de pesee papier saisi en MANUAL (distinct du DSD, RG-5.04). */
|
|
#[Assert\Length(max: 50, maxMessage: 'Le numéro de pesée ne peut pas dépasser {{ limit }} caractères.', normalizer: 'trim')]
|
|
#[Groups(['weighbridge_reading:write', 'weighbridge_reading:read'])]
|
|
public ?string $manualNumber = null;
|
|
|
|
/** DSD attribue par le serveur (lecture seule) — previsionnel (cf. docbloc classe). */
|
|
#[Groups(['weighbridge_reading:read'])]
|
|
public ?int $dsd = null;
|
|
|
|
/**
|
|
* RG metier : en pesee MANUAL, le poids est saisi par l'operateur (le pont
|
|
* n'est pas lu) → il est obligatoire. Porte par un Callback pour que le 422
|
|
* cible le propertyPath `weight` (mapping inline front, ERP-101). En AUTO,
|
|
* le poids fourni par le client est ignore (renseigne par le pont).
|
|
*/
|
|
#[Assert\Callback]
|
|
public function validateManualWeight(ExecutionContextInterface $context): void
|
|
{
|
|
if ('MANUAL' === $this->mode && null === $this->weight) {
|
|
$context->buildViolation('Le poids est obligatoire en pesée manuelle.')
|
|
->atPath('weight')
|
|
->addViolation()
|
|
;
|
|
}
|
|
}
|
|
}
|