36e947fd8e
Auto Tag Develop / tag (push) Successful in 8s
## ERP-187 (1.7) — Tests PHPUnit RG-5.01→5.10 + capture contrat JSON
Couvre les règles de gestion du M5 (tickets de pesée) par des tests PHPUnit et capture la **réponse JSON réelle** (DoD § 4.0.bis) collée dans `spec-back.md` avant les écrans front.
### Tests unitaires (Processor / Normalizer / Callback — sans BDD ni HTTP)
- **NetWeightTest** (RG-5.05) : net = plein − vide, `null` si une pesée manque, recalcul au PATCH.
- **CounterpartyValidationTest** (RG-5.03) : présence du champ requis par branche (propertyPath `client`/`supplier`/`otherLabel`) + exclusivité (null-ification hors-branche).
- **ImmatriculationNormalizationTest** (RG-5.01/5.10) : masque `XX-000-XX`, « Tout format », mapping 422 sur `immatriculation`.
### Tests fonctionnels (API réelle)
- **WeighingTicketNumberingTest** (RG-5.02/5.09) : format `{siteCode}-TP-{NNNN}`, séquence par site, isolation inter-sites, immuabilité numéro/site au PATCH.
- **WeighingTicketSerializationContractTest** (DoD § 4.0.bis) : 4 pièges verts (client embarqué, `plateFreeFormat` présent, `number` formaté, `netWeight` = full − empty) + dump JSON via `WEIGHING_TICKET_DOD_DUMP`.
- **WeighingTicketRBACMatrixTest** (§ 5.2) : admin/bureau/usine OK, compta/commerciale 403, anonyme 401.
> DSD / stub pont bascule / endpoint pesée déjà couverts (ERP-184/185).
### DoD
- `spec-back.md § 4.0.bis` : **JSON réel** (liste + détail) collé, 4 pièges marqués ✅ — feu vert front.
### Vérifications
- `make test` complet **vert** : 848 tests, 6302 assertions (0 échec ; deprecations/notices PHPUnit seuls).
- `make php-cs-fixer-allow-risky` : 0 correction.
Empilée sur ERP-186 (stack M5).
---------
Co-authored-by: Matthieu <contact@malio.fr>
Reviewed-on: #137
163 lines
6.4 KiB
PHP
163 lines
6.4 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Tests\Module\Logistique\Application\Service;
|
|
|
|
use ApiPlatform\Metadata\Post;
|
|
use ApiPlatform\State\ProcessorInterface;
|
|
use ApiPlatform\Validator\Exception\ValidationException;
|
|
use App\Module\Logistique\Application\Service\DsdAllocatorInterface;
|
|
use App\Module\Logistique\Application\Service\WeighingTicketFieldNormalizer;
|
|
use App\Module\Logistique\Application\Service\WeighingTicketNumberAllocatorInterface;
|
|
use App\Module\Logistique\Domain\Entity\WeighingTicket;
|
|
use App\Module\Logistique\Domain\Exception\InvalidImmatriculationException;
|
|
use App\Module\Logistique\Infrastructure\ApiPlatform\State\Processor\WeighingTicketProcessor;
|
|
use App\Module\Sites\Application\Service\CurrentSiteProviderInterface;
|
|
use App\Module\Sites\Domain\Entity\Site;
|
|
use Doctrine\ORM\EntityManagerInterface;
|
|
use PHPUnit\Framework\Attributes\DataProvider;
|
|
use PHPUnit\Framework\TestCase;
|
|
|
|
/**
|
|
* Normalisation de l'immatriculation (RG-5.01 / RG-5.10 / § 6 / § 2.10) — test
|
|
* unitaire en deux volets, sans BDD ni HTTP :
|
|
*
|
|
* 1. Le WeighingTicketFieldNormalizer applique trim + UPPER ; hors « Tout format »
|
|
* il ramene la saisie au masque SIV canonique XX-000-XX (separateurs/espaces
|
|
* ignores puis re-poses) et leve InvalidImmatriculationException si le squelette
|
|
* 2-3-2 n'est pas respecte. En « Tout format », seul trim + UPPER s'applique.
|
|
* 2. Le WeighingTicketProcessor traduit cette exception de domaine en 422 portant
|
|
* un propertyPath « immatriculation » (mapping inline useFormErrors, ERP-101).
|
|
*
|
|
* @internal
|
|
*/
|
|
final class ImmatriculationNormalizationTest extends TestCase
|
|
{
|
|
private WeighingTicketFieldNormalizer $normalizer;
|
|
|
|
protected function setUp(): void
|
|
{
|
|
$this->normalizer = new WeighingTicketFieldNormalizer();
|
|
}
|
|
|
|
// === Volet 1 : normalisation pure (masque + « Tout format ») ===
|
|
|
|
#[DataProvider('provideMaskedPlates')]
|
|
public function testMaskedPlateIsReformattedToCanonicalSiv(string $input): void
|
|
{
|
|
self::assertSame('AB-123-CD', $this->normalizer->normalizeImmatriculation($input, false));
|
|
}
|
|
|
|
/**
|
|
* @return iterable<string, array{string}>
|
|
*/
|
|
public static function provideMaskedPlates(): iterable
|
|
{
|
|
yield 'deja canonique' => ['AB-123-CD'];
|
|
yield 'minuscules nues' => ['ab123cd'];
|
|
yield 'espaces' => ['AB 123 CD'];
|
|
yield 'minuscules tirets'=> ['ab-123-cd'];
|
|
yield 'espaces de garde' => [' ab-123-cd '];
|
|
}
|
|
|
|
public function testInvalidPlateWithoutFreeFormatThrows(): void
|
|
{
|
|
$this->expectException(InvalidImmatriculationException::class);
|
|
$this->normalizer->normalizeImmatriculation('ABC-12-D', false);
|
|
}
|
|
|
|
public function testFreeFormatBypassesTheMask(): void
|
|
{
|
|
// Ancienne plaque / engin : aucune contrainte de masque, juste trim + UPPER.
|
|
self::assertSame('1234 WW 75', $this->normalizer->normalizeImmatriculation(' 1234 ww 75 ', true));
|
|
self::assertSame('ENGIN-XYZ', $this->normalizer->normalizeImmatriculation('engin-xyz', true));
|
|
}
|
|
|
|
public function testNullAndBlankAreNormalizedToNull(): void
|
|
{
|
|
self::assertNull($this->normalizer->normalizeImmatriculation(null, false));
|
|
self::assertNull($this->normalizer->normalizeImmatriculation(' ', false));
|
|
self::assertNull($this->normalizer->normalizeImmatriculation(' ', true));
|
|
}
|
|
|
|
public function testOtherLabelIsTrimmedAndBlankBecomesNull(): void
|
|
{
|
|
self::assertSame('Reprise interne', $this->normalizer->normalizeOtherLabel(' Reprise interne '));
|
|
self::assertNull($this->normalizer->normalizeOtherLabel(' '));
|
|
self::assertNull($this->normalizer->normalizeOtherLabel(null));
|
|
}
|
|
|
|
// === Volet 2 : mapping 422 par le Processor (RG-5.01, ERP-101) ===
|
|
|
|
public function testProcessorMapsInvalidPlateTo422OnImmatriculationPath(): void
|
|
{
|
|
$ticket = (new WeighingTicket())
|
|
->setCounterpartyType('AUTRE')
|
|
->setOtherLabel('Reprise')
|
|
->setImmatriculation('PLAQUE INVALIDE')
|
|
->setPlateFreeFormat(false)
|
|
;
|
|
|
|
try {
|
|
$this->makeProcessor()->process($ticket, new Post());
|
|
self::fail('Une ValidationException (422) etait attendue sur une immatriculation invalide.');
|
|
} catch (ValidationException $e) {
|
|
$paths = [];
|
|
foreach ($e->getConstraintViolationList() as $violation) {
|
|
$paths[] = $violation->getPropertyPath();
|
|
}
|
|
self::assertContains('immatriculation', $paths);
|
|
}
|
|
}
|
|
|
|
public function testProcessorReformatsValidPlateAndHonorsFreeFormat(): void
|
|
{
|
|
// Masque applique a la persistance (saisie nue -> canonique).
|
|
$masked = (new WeighingTicket())
|
|
->setCounterpartyType('AUTRE')
|
|
->setOtherLabel('Reprise')
|
|
->setImmatriculation('ab123cd')
|
|
->setPlateFreeFormat(false)
|
|
;
|
|
$this->makeProcessor()->process($masked, new Post());
|
|
self::assertSame('AB-123-CD', $masked->getImmatriculation());
|
|
|
|
// « Tout format » : la plaque libre passe (UPPER seulement), aucune 422.
|
|
$free = (new WeighingTicket())
|
|
->setCounterpartyType('AUTRE')
|
|
->setOtherLabel('Reprise')
|
|
->setImmatriculation('vieux 4321 zz')
|
|
->setPlateFreeFormat(true)
|
|
;
|
|
$this->makeProcessor()->process($free, new Post());
|
|
self::assertSame('VIEUX 4321 ZZ', $free->getImmatriculation());
|
|
}
|
|
|
|
private function makeProcessor(): WeighingTicketProcessor
|
|
{
|
|
$persist = $this->createStub(ProcessorInterface::class);
|
|
$persist->method('process')->willReturnArgument(0);
|
|
|
|
$siteProvider = $this->createStub(CurrentSiteProviderInterface::class);
|
|
$siteProvider->method('get')->willReturn(
|
|
(new Site('Châtellerault', 'Rue du Pont', null, '86000', 'Châtellerault', '#112233'))->setCode('86'),
|
|
);
|
|
|
|
$numberAllocator = $this->createStub(WeighingTicketNumberAllocatorInterface::class);
|
|
$numberAllocator->method('allocate')->willReturn('86-TP-0001');
|
|
|
|
$em = $this->createStub(EntityManagerInterface::class);
|
|
$em->method('contains')->willReturn(false);
|
|
|
|
return new WeighingTicketProcessor(
|
|
$persist,
|
|
$siteProvider,
|
|
$numberAllocator,
|
|
$this->createStub(DsdAllocatorInterface::class),
|
|
new WeighingTicketFieldNormalizer(),
|
|
$em,
|
|
);
|
|
}
|
|
}
|