feat : M5 — Tickets de pesée (ERP-188 → ERP-193) #144
@@ -82,6 +82,30 @@ describe('useWeighingTicketForm', () => {
|
||||
expect(form.buildDraftPayload().otherLabel).toBe('Reprise interne')
|
||||
})
|
||||
|
||||
it('buildDraftPayload : type choisi mais champ associé vide → contrepartie omise (pas de 500 chk_wt_*_branch)', () => {
|
||||
const form = useWeighingTicketForm()
|
||||
// L'opérateur ouvre le menu « Client » mais n'a pas encore choisi le client.
|
||||
form.setCounterpartyType('CLIENT')
|
||||
|
||||
const draft = form.buildDraftPayload()
|
||||
// On n'émet ni le type ni la FK : un brouillon incohérent serait rejeté en 500 par le back.
|
||||
expect(draft).not.toHaveProperty('counterpartyType')
|
||||
expect(draft).not.toHaveProperty('client')
|
||||
|
||||
// En revanche la validation envoie toujours le type, pour déclencher la 422 métier.
|
||||
expect(form.buildValidatePayload().counterpartyType).toBe('CLIENT')
|
||||
})
|
||||
|
||||
it('buildDraftPayload : AUTRE avec libellé vide → contrepartie omise', () => {
|
||||
const form = useWeighingTicketForm()
|
||||
form.setCounterpartyType('AUTRE')
|
||||
form.otherLabel.value = ' '
|
||||
|
||||
const draft = form.buildDraftPayload()
|
||||
expect(draft).not.toHaveProperty('counterpartyType')
|
||||
expect(draft).not.toHaveProperty('otherLabel')
|
||||
})
|
||||
|
||||
// ── Immatriculation / « Tout format » partagés entre blocs (RG-5.01) ──────
|
||||
it('immatriculation et plateFreeFormat sont partagés (une seule valeur)', () => {
|
||||
const form = useWeighingTicketForm()
|
||||
|
||||
@@ -185,6 +185,29 @@ export function useWeighingTicketForm() {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Contrepartie d'un BROUILLON : on n'envoie le type QUE si son champ associé est
|
||||
* renseigné. Un type sans son champ (l'opérateur a ouvert le menu avant de
|
||||
* choisir) est une contrepartie incohérente que le back devrait retirer (sinon
|
||||
* les CHECK chk_wt_*_branch lèvent une 500). On évite donc de l'émettre côté
|
||||
* front. La cohérence reste exigée à la validation : `buildValidatePayload()`
|
||||
* envoie toujours le type, pour déclencher la 422 métier sur le champ manquant.
|
||||
*/
|
||||
function draftCounterpartyPayload(): Record<string, unknown> {
|
||||
switch (counterpartyType.value) {
|
||||
case 'CLIENT':
|
||||
return clientIri.value ? { counterpartyType: 'CLIENT', client: clientIri.value } : {}
|
||||
case 'FOURNISSEUR':
|
||||
return supplierIri.value ? { counterpartyType: 'FOURNISSEUR', supplier: supplierIri.value } : {}
|
||||
case 'AUTRE':
|
||||
return otherLabel.value && otherLabel.value.trim() !== ''
|
||||
? { counterpartyType: 'AUTRE', otherLabel: otherLabel.value }
|
||||
: {}
|
||||
default:
|
||||
return {}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Champs d'un bloc de pesée, UNIQUEMENT s'il a été pesé (poids renseigné) — on
|
||||
* n'envoie pas la date par défaut d'un bloc vierge (sinon le back stockerait une
|
||||
@@ -208,8 +231,7 @@ export function useWeighingTicketForm() {
|
||||
*/
|
||||
function buildDraftPayload(): Record<string, unknown> {
|
||||
return compact({
|
||||
counterpartyType: counterpartyType.value,
|
||||
...counterpartyPayload(),
|
||||
...draftCounterpartyPayload(),
|
||||
immatriculation: immatriculation.value || null,
|
||||
plateFreeFormat: plateFreeFormat.value,
|
||||
...blockPayload('empty', empty),
|
||||
|
||||
+43
-6
@@ -121,36 +121,73 @@ final class WeighingTicketProcessor implements ProcessorInterface
|
||||
|
||||
/**
|
||||
* RG-5.03 : garantit l'exclusivite de la contrepartie en forcant a null les
|
||||
* champs hors-branche selon counterpartyType. La PRESENCE du champ requis est
|
||||
* deja validee en amont (Assert\Callback de l'entite) ; ici on evite qu'un
|
||||
* payload portant a la fois client_id ET supplier_id ne fasse echouer les CHECK
|
||||
* Postgres (500 generique au lieu d'une donnee coherente). otherLabel est
|
||||
* normalise (trim) dans la branche AUTRE.
|
||||
* champs hors-branche selon counterpartyType. La PRESENCE du champ requis n'est
|
||||
* validee qu'a la VALIDATION (Assert\Callback groupe finalize, ERP-193) : un
|
||||
* BROUILLON peut donc arriver ici avec un type choisi mais SANS son champ associe
|
||||
* (l'operateur a ouvert le menu avant de selectionner). On retire alors la
|
||||
* contrepartie entiere (clearCounterparty) au lieu de persister un etat
|
||||
* incoherent qui violerait les CHECK Postgres chk_wt_*_branch (500 generique).
|
||||
* Ne concerne que le brouillon : a la validation, le Callback finalize a deja
|
||||
* leve une 422 AVANT ce Processor. otherLabel est normalise (trim) en branche
|
||||
* AUTRE ; un libelle vide vaut « champ associe absent » -> contrepartie retiree.
|
||||
*/
|
||||
private function applyCounterpartyExclusivity(WeighingTicket $data): void
|
||||
{
|
||||
switch ($data->getCounterpartyType()) {
|
||||
case 'CLIENT':
|
||||
if (null === $data->getClient()) {
|
||||
$this->clearCounterparty($data);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$data->setSupplier(null);
|
||||
$data->setOtherLabel(null);
|
||||
|
||||
break;
|
||||
|
||||
case 'FOURNISSEUR':
|
||||
if (null === $data->getSupplier()) {
|
||||
$this->clearCounterparty($data);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$data->setClient(null);
|
||||
$data->setOtherLabel(null);
|
||||
|
||||
break;
|
||||
|
||||
case 'AUTRE':
|
||||
$label = $this->normalizer->normalizeOtherLabel($data->getOtherLabel());
|
||||
if (null === $label) {
|
||||
$this->clearCounterparty($data);
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
$data->setClient(null);
|
||||
$data->setSupplier(null);
|
||||
$data->setOtherLabel($this->normalizer->normalizeOtherLabel($data->getOtherLabel()));
|
||||
$data->setOtherLabel($label);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retire toute la contrepartie d'un brouillon a la selection incomplete (type
|
||||
* sans champ associe) : on ne persiste pas une contrepartie a moitie (qui
|
||||
* violerait chk_wt_*_branch). Le brouillon reste enregistrable sans contrepartie
|
||||
* (ERP-193) ; la coherence est exigee a la validation.
|
||||
*/
|
||||
private function clearCounterparty(WeighingTicket $data): void
|
||||
{
|
||||
$data->setCounterpartyType(null);
|
||||
$data->setClient(null);
|
||||
$data->setSupplier(null);
|
||||
$data->setOtherLabel(null);
|
||||
}
|
||||
|
||||
/**
|
||||
* RG-5.01 / RG-5.10 : normalisation serveur de l'immatriculation (trim + UPPER
|
||||
* + masque XX-000-XX hors « Tout format »). Un format invalide est traduit en
|
||||
|
||||
@@ -36,6 +36,49 @@ final class WeighingTicketLifecycleTest extends AbstractWeighingTicketApiTestCas
|
||||
self::assertSame(7150, $body['emptyWeight']);
|
||||
}
|
||||
|
||||
public function testDraftWithIncompleteCounterpartyIsPersistedWithoutBranch(): void
|
||||
{
|
||||
$http = $this->authManageOnSite($this->siteByCode('86'));
|
||||
|
||||
// Brouillon « contrepartie incomplete » : type CLIENT choisi mais client pas
|
||||
// encore selectionne (cas reel : l'operateur ouvre le menu puis pese). Le
|
||||
// Callback de coherence ne joue qu'a la validation (groupe finalize) ->
|
||||
// SANS normalisation cote Processor, le persist violerait chk_wt_client_branch
|
||||
// (counterparty_type='CLIENT' + client_id NULL) et leverait une 500.
|
||||
$body = $this->postTicket($http, [
|
||||
'counterpartyType' => 'CLIENT',
|
||||
'emptyDate' => '2026-06-17T09:00:00+02:00',
|
||||
'emptyWeight' => 7150,
|
||||
'emptyMode' => 'AUTO',
|
||||
])->toArray();
|
||||
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
self::assertSame('DRAFT', $body['status']);
|
||||
// La contrepartie incoherente est retiree (pas persistee a moitie) : le
|
||||
// brouillon reste enregistrable, la coherence est exigee a la validation.
|
||||
self::assertNull($body['counterpartyType'] ?? null);
|
||||
self::assertSame(7150, $body['emptyWeight']);
|
||||
}
|
||||
|
||||
public function testDraftWithEmptyOtherLabelIsPersistedWithoutBranch(): void
|
||||
{
|
||||
$http = $this->authManageOnSite($this->siteByCode('86'));
|
||||
|
||||
// Meme piege en branche AUTRE : type AUTRE mais libelle vide -> le normalizer
|
||||
// ramene otherLabel a NULL, ce qui violait chk_wt_other_branch (500).
|
||||
$body = $this->postTicket($http, [
|
||||
'counterpartyType' => 'AUTRE',
|
||||
'otherLabel' => ' ',
|
||||
'emptyDate' => '2026-06-17T09:00:00+02:00',
|
||||
'emptyWeight' => 7150,
|
||||
'emptyMode' => 'AUTO',
|
||||
])->toArray();
|
||||
|
||||
self::assertResponseStatusCodeSame(201);
|
||||
self::assertSame('DRAFT', $body['status']);
|
||||
self::assertNull($body['counterpartyType'] ?? null);
|
||||
}
|
||||
|
||||
public function testValidateRequiresCounterparty(): void
|
||||
{
|
||||
$http = $this->authManageOnSite($this->siteByCode('86'));
|
||||
|
||||
Reference in New Issue
Block a user