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 */ 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, ); } }