feat : cycle de vie brouillon/validé du ticket de pesée (ERP-193)
Une pesée (bascule ou manuelle) s'enregistre désormais dès la validation de sa
modale, sans exiger la contrepartie ni l'immatriculation : le ticket naît
« brouillon » (status DRAFT, sans numéro). Le bouton « Valider » finalise quand
les 3 champs du haut (contrepartie + champ associé + immatriculation) ET les 2
pesées sont renseignés : attribution du numéro {siteCode}-TP-{NNNN} et passage
en VALIDATED, puis ouverture du bon de pesée PDF.
Back : counterparty_type/immatriculation/number nullables + colonne status
(migration racine), contraintes strictes déplacées en groupe de validation
finalize, opération PATCH /weighing_tickets/{id}/validate, numéro attribué à la
validation. Front : 4 champs en haut hors blocs, persistance immédiate des
pesées, écrans Ajouter/Modifier refondus, colonne Statut dans la liste, form à
plat pleine largeur. Tests back (lifecycle brouillon/validate) + front à jour.
This commit is contained in:
@@ -220,7 +220,8 @@ abstract class AbstractWeighingTicketApiTestCase extends AbstractApiTestCase
|
||||
|
||||
/**
|
||||
* POST un ticket et renvoie la reponse (assertions de statut a la charge de
|
||||
* l'appelant).
|
||||
* l'appelant). Cree un BROUILLON (status DRAFT, sans numero, ERP-193) — la
|
||||
* validation est portee par validateTicket().
|
||||
*/
|
||||
protected function postTicket(Client $http, array $payload): ResponseInterface
|
||||
{
|
||||
@@ -230,6 +231,32 @@ abstract class AbstractWeighingTicketApiTestCase extends AbstractApiTestCase
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* « Valider » un ticket : PATCH /weighing_tickets/{id}/validate (ERP-193).
|
||||
* Declenche la validation stricte (groupe finalize) + attribution du numero +
|
||||
* passage en VALIDATED. Body vide par defaut = on valide l'etat deja persiste.
|
||||
*/
|
||||
protected function validateTicket(Client $http, int $id, array $payload = []): ResponseInterface
|
||||
{
|
||||
return $http->request('PATCH', '/api/weighing_tickets/'.$id.'/validate', [
|
||||
'headers' => ['Content-Type' => self::MERGE],
|
||||
'json' => [] === $payload ? new \stdClass() : $payload,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST un brouillon complet puis le valide ; renvoie le ticket VALIDE (numero
|
||||
* attribue). Le payload doit porter contrepartie + immatriculation + 2 pesees.
|
||||
*
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
protected function createValidatedTicket(Client $http, array $payload): array
|
||||
{
|
||||
$id = (int) $this->postTicket($http, $payload)->toArray()['id'];
|
||||
|
||||
return $this->validateTicket($http, $id)->toArray();
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrouve un membre d'une collection Hydra par son id.
|
||||
*
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Tests\Module\Logistique\Api;
|
||||
|
||||
/**
|
||||
* Cycle de vie brouillon -> valide du ticket de pesee (ERP-193, spec-back § 2.14).
|
||||
*
|
||||
* Couvre :
|
||||
* - une pesee peut etre enregistree SANS contrepartie ni immatriculation : le POST
|
||||
* cree un BROUILLON (status DRAFT, pas de numero) ;
|
||||
* - la validation (PATCH /validate) exige les 3 champs du haut (type + champ
|
||||
* contrepartie + immatriculation) ET les 2 pesees (groupe `finalize`) ;
|
||||
* - une validation complete attribue le numero {siteCode}-TP-{NNNN} et passe le
|
||||
* ticket en VALIDATED.
|
||||
*
|
||||
* @internal
|
||||
*/
|
||||
final class WeighingTicketLifecycleTest extends AbstractWeighingTicketApiTestCase
|
||||
{
|
||||
public function testWeighingOnlyCreatesDraftWithoutNumber(): void
|
||||
{
|
||||
$http = $this->authManageOnSite($this->siteByCode('86'));
|
||||
|
||||
// Pesee a vide seule : ni contrepartie, ni immatriculation.
|
||||
$body = $this->postTicket($http, [
|
||||
'emptyDate' => '2026-06-17T09:00:00+02:00',
|
||||
'emptyWeight' => 7150,
|
||||
'emptyMode' => 'AUTO',
|
||||
])->toArray();
|
||||
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
self::assertSame('DRAFT', $body['status']);
|
||||
self::assertArrayNotHasKey('number', $body, 'Un brouillon n\'a pas encore de numero (skip_null_values).');
|
||||
self::assertSame(7150, $body['emptyWeight']);
|
||||
}
|
||||
|
||||
public function testValidateRequiresCounterparty(): void
|
||||
{
|
||||
$http = $this->authManageOnSite($this->siteByCode('86'));
|
||||
|
||||
// Brouillon complet cote pesees + immatriculation, mais SANS contrepartie.
|
||||
$id = (int) $this->postTicket($http, [
|
||||
'immatriculation' => 'AB-123-CD',
|
||||
'emptyDate' => '2026-06-17T09:00:00+02:00',
|
||||
'emptyWeight' => 7150,
|
||||
'emptyMode' => 'AUTO',
|
||||
'fullDate' => '2026-06-17T09:12:00+02:00',
|
||||
'fullWeight' => 14300,
|
||||
'fullMode' => 'AUTO',
|
||||
])->toArray()['id'];
|
||||
|
||||
$response = $this->validateTicket($http, $id);
|
||||
|
||||
self::assertResponseStatusCodeSame(422);
|
||||
self::assertViolationOnPath($response, 'counterpartyType');
|
||||
}
|
||||
|
||||
public function testValidateRequiresBothWeighings(): void
|
||||
{
|
||||
$http = $this->authManageOnSite($this->siteByCode('86'));
|
||||
$client = $this->seedTestClient('Lifecycle');
|
||||
|
||||
// Brouillon avec contrepartie + immat + UNE seule pesee (a vide).
|
||||
$id = (int) $this->postTicket($http, [
|
||||
'counterpartyType' => 'CLIENT',
|
||||
'client' => $this->clientIri($client),
|
||||
'immatriculation' => 'AB-123-CD',
|
||||
'emptyDate' => '2026-06-17T09:00:00+02:00',
|
||||
'emptyWeight' => 7150,
|
||||
'emptyMode' => 'AUTO',
|
||||
])->toArray()['id'];
|
||||
|
||||
$response = $this->validateTicket($http, $id);
|
||||
|
||||
self::assertResponseStatusCodeSame(422);
|
||||
self::assertViolationOnPath($response, 'fullWeight');
|
||||
}
|
||||
|
||||
public function testValidateAssignsNumberAndStatus(): void
|
||||
{
|
||||
$http = $this->authManageOnSite($this->siteByCode('86'));
|
||||
$client = $this->seedTestClient('LifecycleOk');
|
||||
|
||||
$validated = $this->createValidatedTicket($http, $this->validClientTicketPayload($client));
|
||||
|
||||
self::assertSame('VALIDATED', $validated['status']);
|
||||
self::assertMatchesRegularExpression('/^86-TP-\d{4}$/', (string) $validated['number']);
|
||||
self::assertSame(7150, $validated['netWeight']);
|
||||
}
|
||||
}
|
||||
@@ -26,13 +26,12 @@ final class WeighingTicketNumberingTest extends AbstractWeighingTicketApiTestCas
|
||||
$http = $this->authManageOnSite($site);
|
||||
$client = $this->seedTestClient('Num');
|
||||
|
||||
$first = $this->postTicket($http, $this->validClientTicketPayload($client));
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
$second = $this->postTicket($http, $this->validClientTicketPayload($client));
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
// Le numero est attribue a la VALIDATION (brouillon -> valide, ERP-193).
|
||||
$first = $this->createValidatedTicket($http, $this->validClientTicketPayload($client));
|
||||
$second = $this->createValidatedTicket($http, $this->validClientTicketPayload($client));
|
||||
|
||||
$n1 = (string) $first->toArray()['number'];
|
||||
$n2 = (string) $second->toArray()['number'];
|
||||
$n1 = (string) $first['number'];
|
||||
$n2 = (string) $second['number'];
|
||||
|
||||
self::assertMatchesRegularExpression('/^86-TP-\d{4}$/', $n1);
|
||||
self::assertMatchesRegularExpression('/^86-TP-\d{4}$/', $n2);
|
||||
@@ -49,8 +48,8 @@ final class WeighingTicketNumberingTest extends AbstractWeighingTicketApiTestCas
|
||||
$http86 = $this->authManageOnSite($this->siteByCode('86'));
|
||||
$http17 = $this->authManageOnSite($this->siteByCode('17'));
|
||||
|
||||
$n86 = (string) $this->postTicket($http86, $this->validClientTicketPayload($client))->toArray()['number'];
|
||||
$n17 = (string) $this->postTicket($http17, $this->validClientTicketPayload($client))->toArray()['number'];
|
||||
$n86 = (string) $this->createValidatedTicket($http86, $this->validClientTicketPayload($client))['number'];
|
||||
$n17 = (string) $this->createValidatedTicket($http17, $this->validClientTicketPayload($client))['number'];
|
||||
|
||||
// Chaque site encode son propre code dans le numero ; sequences disjointes.
|
||||
self::assertStringStartsWith('86-TP-', $n86);
|
||||
@@ -63,7 +62,8 @@ final class WeighingTicketNumberingTest extends AbstractWeighingTicketApiTestCas
|
||||
$http = $this->authManageOnSite($site);
|
||||
$client = $this->seedTestClient('Immutable');
|
||||
|
||||
$created = $this->postTicket($http, $this->validClientTicketPayload($client))->toArray();
|
||||
// Ticket valide (numero attribue) puis tentative de re-ecriture.
|
||||
$created = $this->createValidatedTicket($http, $this->validClientTicketPayload($client));
|
||||
$id = (int) $created['id'];
|
||||
$number = (string) $created['number'];
|
||||
|
||||
|
||||
@@ -31,12 +31,12 @@ final class WeighingTicketSerializationContractTest extends AbstractWeighingTick
|
||||
$http = $this->authManageOnSite($site);
|
||||
$clientEntity = $this->seedTestClient('Negoce');
|
||||
|
||||
$created = $this->postTicket($http, $this->validClientTicketPayload($clientEntity));
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
$createdBody = $created->toArray();
|
||||
// Brouillon cree puis valide (numero attribue a la validation, ERP-193).
|
||||
$createdBody = $this->createValidatedTicket($http, $this->validClientTicketPayload($clientEntity));
|
||||
|
||||
$id = (int) $createdBody['id'];
|
||||
$number = (string) $createdBody['number'];
|
||||
self::assertSame('VALIDATED', $createdBody['status']);
|
||||
|
||||
$detail = $http->request('GET', '/api/weighing_tickets/'.$id, ['headers' => ['Accept' => self::LD]])->toArray();
|
||||
$list = $http->request('GET', '/api/weighing_tickets?search='.$number, ['headers' => ['Accept' => self::LD]])->toArray();
|
||||
@@ -69,6 +69,9 @@ final class WeighingTicketSerializationContractTest extends AbstractWeighingTick
|
||||
// displayDate (date du ticket = fullDate ?? emptyDate) expose en liste.
|
||||
self::assertArrayHasKey('displayDate', $row);
|
||||
|
||||
// Statut du cycle de vie expose en liste (colonne « En attente / Terminée »).
|
||||
self::assertSame('VALIDATED', $row['status']);
|
||||
|
||||
// === DETAIL : site embarque (avec code), immatriculation, les 2 pesees ===
|
||||
self::assertIsArray($detail['site']);
|
||||
self::assertSame('86', $detail['site']['code']);
|
||||
@@ -95,9 +98,7 @@ final class WeighingTicketSerializationContractTest extends AbstractWeighingTick
|
||||
$http = $this->authManageOnSite($site);
|
||||
$supplierEntity = $this->seedTestSupplier('Ferraille');
|
||||
|
||||
$created = $this->postTicket($http, $this->validSupplierTicketPayload($supplierEntity));
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
$createdBody = $created->toArray();
|
||||
$createdBody = $this->createValidatedTicket($http, $this->validSupplierTicketPayload($supplierEntity));
|
||||
|
||||
$id = (int) $createdBody['id'];
|
||||
$number = (string) $createdBody['number'];
|
||||
|
||||
+5
-2
@@ -145,14 +145,17 @@ final class CounterpartyValidationTest extends TestCase
|
||||
}
|
||||
|
||||
/**
|
||||
* Liste des propertyPath des violations de l'entite.
|
||||
* Liste des propertyPath des violations de l'entite, validee dans le groupe
|
||||
* `finalize` (la coherence contrepartie ne joue qu'a la validation depuis
|
||||
* ERP-193 ; un brouillon peut ne pas porter de contrepartie). Miroir du
|
||||
* validationContext de l'operation `validate` (['Default', 'finalize']).
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
private function violationPaths(WeighingTicket $ticket): array
|
||||
{
|
||||
$paths = [];
|
||||
foreach ($this->validator->validate($ticket) as $violation) {
|
||||
foreach ($this->validator->validate($ticket, null, ['Default', 'finalize']) as $violation) {
|
||||
$paths[] = $violation->getPropertyPath();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user