fix(logistique) : bloque la saisie manuelle poids/DSD à 5 chiffres
Masque front 5 chiffres sur la modale manuelle + Assert\LessThanOrEqual(99999) sur WeighbridgeReadingResource (weight/dsd, mode MANUAL) et backstop entité (validateManualEntryDigits). Le DSD auto (compteur de site) n'est pas contraint.
This commit is contained in:
@@ -161,14 +161,14 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
<MalioInputText
|
||||
v-model="manualModal.weight"
|
||||
:mask="NUMERIC_MASK"
|
||||
:mask="MANUAL_NUMERIC_MASK"
|
||||
:label="t('logistique.weighingTickets.form.manual.weight')"
|
||||
:required="true"
|
||||
:error="manualModal.errors.weight"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="manualModal.dsd"
|
||||
:mask="NUMERIC_MASK"
|
||||
:mask="MANUAL_NUMERIC_MASK"
|
||||
:label="t('logistique.weighingTickets.form.manual.dsd')"
|
||||
:required="true"
|
||||
:error="manualModal.errors.dsd"
|
||||
@@ -192,7 +192,7 @@ import { useWeighingTicketForm, type WeighingBlockState } from '~/modules/logist
|
||||
import { useWeighbridge } from '~/modules/logistique/composables/useWeighbridge'
|
||||
import { useWeighingTicket, type WeighingTicketDetail } from '~/modules/logistique/composables/useWeighingTicket'
|
||||
import { useWeighingTicketReferentials, type RefOption } from '~/modules/logistique/composables/useWeighingTicketReferentials'
|
||||
import { NUMERIC_MASK, PLATE_MASK, FREE_PLATE_MASK } from '~/modules/logistique/utils/weighingMasks'
|
||||
import { MANUAL_NUMERIC_MASK, PLATE_MASK, FREE_PLATE_MASK } from '~/modules/logistique/utils/weighingMasks'
|
||||
import { mapViolationsToRecord } from '~/shared/utils/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -151,14 +151,14 @@
|
||||
<div class="flex flex-col gap-2">
|
||||
<MalioInputText
|
||||
v-model="manualModal.weight"
|
||||
:mask="NUMERIC_MASK"
|
||||
:mask="MANUAL_NUMERIC_MASK"
|
||||
:label="t('logistique.weighingTickets.form.manual.weight')"
|
||||
:required="true"
|
||||
:error="manualModal.errors.weight"
|
||||
/>
|
||||
<MalioInputText
|
||||
v-model="manualModal.dsd"
|
||||
:mask="NUMERIC_MASK"
|
||||
:mask="MANUAL_NUMERIC_MASK"
|
||||
:label="t('logistique.weighingTickets.form.manual.dsd')"
|
||||
:required="true"
|
||||
:error="manualModal.errors.dsd"
|
||||
@@ -181,7 +181,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
|
||||
import { useWeighingTicketForm, type WeighingBlockState } from '~/modules/logistique/composables/useWeighingTicketForm'
|
||||
import { useWeighbridge } from '~/modules/logistique/composables/useWeighbridge'
|
||||
import { useWeighingTicketReferentials, type RefOption } from '~/modules/logistique/composables/useWeighingTicketReferentials'
|
||||
import { NUMERIC_MASK, PLATE_MASK, FREE_PLATE_MASK } from '~/modules/logistique/utils/weighingMasks'
|
||||
import { MANUAL_NUMERIC_MASK, PLATE_MASK, FREE_PLATE_MASK } from '~/modules/logistique/utils/weighingMasks'
|
||||
import { mapViolationsToRecord } from '~/shared/utils/api'
|
||||
|
||||
const { t } = useI18n()
|
||||
|
||||
@@ -15,6 +15,17 @@ export const NUMERIC_MASK: MaskInputOptions = {
|
||||
tokens: { D: { pattern: /[0-9]/, multiple: true } },
|
||||
}
|
||||
|
||||
/**
|
||||
* Masque « chiffres, maximum 5 » — SAISIE MANUELLE du poids et du DSD (modale de
|
||||
* pesée manuelle). Borne la saisie à 5 chiffres (≤ 99999) ; le garde-fou serveur
|
||||
* (Callback mode MANUAL) reste autoritaire. NE PAS utiliser pour l'AFFICHAGE des
|
||||
* valeurs (WeighingBlock) : un DSD auto-alloué peut dépasser 5 chiffres.
|
||||
*/
|
||||
export const MANUAL_NUMERIC_MASK: MaskInputOptions = {
|
||||
mask: 'DDDDD',
|
||||
tokens: { D: { pattern: /[0-9]/ } },
|
||||
}
|
||||
|
||||
/**
|
||||
* Masque plaque FR SIV `XX-000-XX` : 2 lettres, 3 chiffres, 2 lettres, majuscules
|
||||
* forcées. Utilisé quand « Tout format » n'est pas coché (RG-5.01).
|
||||
|
||||
@@ -184,6 +184,9 @@ class WeighingTicket implements TimestampableInterface, BlamableInterface
|
||||
/** Contrepartie « Autre » (libelle libre) — RG-5.03. */
|
||||
public const string COUNTERPARTY_AUTRE = 'AUTRE';
|
||||
|
||||
/** Plafond des poids/DSD saisis a la main (5 chiffres) — cf. validateManualEntryDigits. */
|
||||
public const int MANUAL_VALUE_MAX = 99999;
|
||||
|
||||
#[ORM\Id]
|
||||
#[ORM\GeneratedValue]
|
||||
#[ORM\Column]
|
||||
@@ -379,6 +382,25 @@ class WeighingTicket implements TimestampableInterface, BlamableInterface
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Plafond des valeurs SAISIES A LA MAIN (poids et DSD) : 5 chiffres, soit
|
||||
* 99999. Garde-fou serveur du masque front 5 chiffres de la modale de pesee
|
||||
* manuelle. Ne s'applique QU'EN mode MANUAL (decision metier) :
|
||||
* - le poids AUTO (pont-bascule) tient deja dans 5 chiffres (10000-50000) ;
|
||||
* - le DSD AUTO est un compteur de site croissant (DsdAllocator) qu'on ne
|
||||
* doit PAS contraindre, sinon l'allocation echouerait au-dela de 99999.
|
||||
* Jouee dans le groupe Default (POST/PATCH brouillon ET validate) -> chaque
|
||||
* 422 est mappee inline sous le champ via useFormErrors (ERP-101).
|
||||
*/
|
||||
#[Assert\Callback]
|
||||
public function validateManualEntryDigits(ExecutionContextInterface $context): void
|
||||
{
|
||||
$this->assertManualDigitCap($context, $this->emptyMode, $this->emptyWeight, 'emptyWeight', 'Le poids saisi ne peut pas dépasser 5 chiffres (99999 kg maximum).');
|
||||
$this->assertManualDigitCap($context, $this->emptyMode, $this->emptyDsd, 'emptyDsd', 'Le DSD saisi ne peut pas dépasser 5 chiffres (99999 maximum).');
|
||||
$this->assertManualDigitCap($context, $this->fullMode, $this->fullWeight, 'fullWeight', 'Le poids saisi ne peut pas dépasser 5 chiffres (99999 kg maximum).');
|
||||
$this->assertManualDigitCap($context, $this->fullMode, $this->fullDsd, 'fullDsd', 'Le DSD saisi ne peut pas dépasser 5 chiffres (99999 maximum).');
|
||||
}
|
||||
|
||||
/**
|
||||
* Date du ticket affichee en LISTE (§ 4.0) : date de la pesee a plein si
|
||||
* disponible, sinon date de la pesee a vide. Getter calcule (jamais
|
||||
@@ -646,4 +668,14 @@ class WeighingTicket implements TimestampableInterface, BlamableInterface
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function assertManualDigitCap(ExecutionContextInterface $context, ?string $mode, ?int $value, string $path, string $message): void
|
||||
{
|
||||
if ('MANUAL' === $mode && null !== $value && $value > self::MANUAL_VALUE_MAX) {
|
||||
$context->buildViolation($message)
|
||||
->atPath($path)
|
||||
->addViolation()
|
||||
;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+3
@@ -6,6 +6,7 @@ namespace App\Module\Logistique\Infrastructure\ApiPlatform\Resource;
|
||||
|
||||
use ApiPlatform\Metadata\ApiResource;
|
||||
use ApiPlatform\Metadata\Post;
|
||||
use App\Module\Logistique\Domain\Entity\WeighingTicket;
|
||||
use App\Module\Logistique\Infrastructure\ApiPlatform\State\Processor\WeighbridgeReadingProcessor;
|
||||
use Symfony\Component\Serializer\Attribute\Groups;
|
||||
use Symfony\Component\Validator\Constraints as Assert;
|
||||
@@ -59,6 +60,7 @@ final class WeighbridgeReadingResource
|
||||
* fournit le poids). En sortie : poids effectif de la pesee.
|
||||
*/
|
||||
#[Assert\Positive(message: 'Le poids doit être un entier positif (kg).')]
|
||||
#[Assert\LessThanOrEqual(value: WeighingTicket::MANUAL_VALUE_MAX, message: 'Le poids saisi ne peut pas dépasser 5 chiffres (99999 kg maximum).')]
|
||||
#[Groups(['weighbridge_reading:write', 'weighbridge_reading:read'])]
|
||||
public ?int $weight = null;
|
||||
|
||||
@@ -68,6 +70,7 @@ final class WeighbridgeReadingResource
|
||||
* (l'obligation en MANUAL est portee par le Callback ci-dessous).
|
||||
*/
|
||||
#[Assert\Positive(message: 'Le DSD doit être un entier positif.')]
|
||||
#[Assert\LessThanOrEqual(value: WeighingTicket::MANUAL_VALUE_MAX, message: 'Le DSD saisi ne peut pas dépasser 5 chiffres (99999 maximum).')]
|
||||
#[Groups(['weighbridge_reading:write', 'weighbridge_reading:read'])]
|
||||
public ?int $dsd = null;
|
||||
|
||||
|
||||
@@ -133,6 +133,49 @@ final class WeighbridgeReadingApiTest extends AbstractApiTestCase
|
||||
self::assertViolationOnPath($response, 'dsd');
|
||||
}
|
||||
|
||||
public function testManualWeighingRejectsWeightOverFiveDigits(): void
|
||||
{
|
||||
$client = $this->manageClientWithCurrentSite();
|
||||
|
||||
$response = $client->request('POST', '/api/weighbridge_readings', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
// 100000 = 6 chiffres → au-dela du plafond 5 chiffres (99999).
|
||||
'json' => ['mode' => 'MANUAL', 'weight' => 100000, 'dsd' => 16619],
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(422);
|
||||
self::assertViolationOnPath($response, 'weight');
|
||||
}
|
||||
|
||||
public function testManualWeighingRejectsDsdOverFiveDigits(): void
|
||||
{
|
||||
$client = $this->manageClientWithCurrentSite();
|
||||
|
||||
$response = $client->request('POST', '/api/weighbridge_readings', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
'json' => ['mode' => 'MANUAL', 'weight' => 23187, 'dsd' => 100000],
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(422);
|
||||
self::assertViolationOnPath($response, 'dsd');
|
||||
}
|
||||
|
||||
public function testManualWeighingAcceptsFiveDigitBoundary(): void
|
||||
{
|
||||
$client = $this->manageClientWithCurrentSite();
|
||||
|
||||
$response = $client->request('POST', '/api/weighbridge_readings', [
|
||||
'headers' => ['Content-Type' => 'application/ld+json'],
|
||||
// 99999 = exactement 5 chiffres → derniere valeur acceptee.
|
||||
'json' => ['mode' => 'MANUAL', 'weight' => 99999, 'dsd' => 99999],
|
||||
]);
|
||||
|
||||
self::assertResponseStatusCodeSame(200);
|
||||
$data = $response->toArray();
|
||||
self::assertSame(99999, $data['weight']);
|
||||
self::assertSame(99999, $data['dsd']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Garde-fou ERP-101 (miroir AbstractWeighingTicketApiTestCase) : une 422 doit
|
||||
* porter une violation sur le `propertyPath` attendu, consommable inline par
|
||||
|
||||
Reference in New Issue
Block a user