diff --git a/frontend/modules/commercial/pages/clients/[id]/edit.vue b/frontend/modules/commercial/pages/clients/[id]/edit.vue
index 9604965..bf078ae 100644
--- a/frontend/modules/commercial/pages/clients/[id]/edit.vue
+++ b/frontend/modules/commercial/pages/clients/[id]/edit.vue
@@ -125,11 +125,15 @@
:readonly="businessReadonly"
:error="informationErrors.errors.employeesCount"
/>
+
client.value?.companyName ?? t('commercial.cl
const main = reactive(mapMainDraft({} as ClientDetail))
const information = reactive(mapInformationDraft({} as ClientDetail))
const accounting = reactive(mapAccountingFormDraft({} as ClientDetail))
+
+// CA plafonne a 999 999 999 999,99 (ERP-193). La :key force le re-affichage du
+// champ controle quand le plafonnement laisse le modelValue inchange.
+const revenueAmountKey = ref(0)
+
+/** Saisie du CA : plafonne au maximum metier et re-synchronise le champ si plafonne. */
+function onRevenueAmountInput(value: string | null): void {
+ const clamped = clampRevenueAmount(value)
+ information.revenueAmount = clamped ?? null
+ if (clamped !== value) {
+ revenueAmountKey.value += 1
+ }
+}
const contacts = ref([])
const addresses = ref([])
const ribs = ref([])
diff --git a/frontend/modules/commercial/pages/clients/new.vue b/frontend/modules/commercial/pages/clients/new.vue
index 4aa6a0c..1ab0949 100644
--- a/frontend/modules/commercial/pages/clients/new.vue
+++ b/frontend/modules/commercial/pages/clients/new.vue
@@ -120,11 +120,15 @@
:readonly="isValidated('information')"
:error="informationErrors.errors.employeesCount"
/>
+
{
if (clientId.value === null || tabSubmitting.value) return
diff --git a/frontend/modules/commercial/pages/suppliers/[id]/edit.vue b/frontend/modules/commercial/pages/suppliers/[id]/edit.vue
index 985a13c..1399482 100644
--- a/frontend/modules/commercial/pages/suppliers/[id]/edit.vue
+++ b/frontend/modules/commercial/pages/suppliers/[id]/edit.vue
@@ -86,11 +86,15 @@
:readonly="businessReadonly"
:error="informationErrors.errors.employeesCount"
/>
+
supplier.value?.companyName ?? t('commercial.
const main = reactive(mapMainDraft({} as SupplierDetail))
const information = reactive(mapInformationDraft({} as SupplierDetail))
const accounting = reactive(mapAccountingFormDraft({} as SupplierDetail))
+
+// CA plafonne a 999 999 999 999,99 (ERP-193). La :key force le re-affichage du
+// champ controle quand le plafonnement laisse le modelValue inchange.
+const revenueAmountKey = ref(0)
+
+/** Saisie du CA : plafonne au maximum metier et re-synchronise le champ si plafonne. */
+function onRevenueAmountInput(value: string | null): void {
+ const clamped = clampRevenueAmount(value)
+ information.revenueAmount = clamped ?? null
+ if (clamped !== value) {
+ revenueAmountKey.value += 1
+ }
+}
const contacts = ref([])
const addresses = ref([])
const ribs = ref([])
diff --git a/frontend/modules/commercial/pages/suppliers/new.vue b/frontend/modules/commercial/pages/suppliers/new.vue
index f471e3b..deccbc2 100644
--- a/frontend/modules/commercial/pages/suppliers/new.vue
+++ b/frontend/modules/commercial/pages/suppliers/new.vue
@@ -80,11 +80,15 @@
:readonly="isValidated('information')"
:error="informationErrors.errors.employeesCount"
/>
+
{
if (supplierId.value === null || tabSubmitting.value) return
diff --git a/frontend/modules/commercial/utils/forms/__tests__/amountInput.spec.ts b/frontend/modules/commercial/utils/forms/__tests__/amountInput.spec.ts
new file mode 100644
index 0000000..4a8bffb
--- /dev/null
+++ b/frontend/modules/commercial/utils/forms/__tests__/amountInput.spec.ts
@@ -0,0 +1,33 @@
+import { describe, expect, it } from 'vitest'
+import { clampRevenueAmount, REVENUE_AMOUNT_MAX } from '../amountInput'
+
+describe('clampRevenueAmount', () => {
+ it('laisse les valeurs vides / nulles telles quelles', () => {
+ expect(clampRevenueAmount(null)).toBeNull()
+ expect(clampRevenueAmount(undefined)).toBeUndefined()
+ expect(clampRevenueAmount('')).toBe('')
+ })
+
+ it('laisse une valeur sous le plafond inchangee', () => {
+ expect(clampRevenueAmount('1000.50')).toBe('1000.50')
+ expect(clampRevenueAmount('999999999999.99')).toBe('999999999999.99')
+ })
+
+ it('plafonne une valeur au-dessus du maximum', () => {
+ expect(clampRevenueAmount('1000000000000')).toBe('999999999999.99')
+ expect(clampRevenueAmount('999999999999999.99')).toBe('999999999999.99')
+ })
+
+ it('tolere une saisie a virgule / avec espaces (securite)', () => {
+ expect(clampRevenueAmount('1 000 000 000 000,00')).toBe('999999999999.99')
+ expect(clampRevenueAmount('12,5')).toBe('12,5')
+ })
+
+ it('ne touche pas une saisie non numerique', () => {
+ expect(clampRevenueAmount('abc')).toBe('abc')
+ })
+
+ it('expose le plafond metier', () => {
+ expect(REVENUE_AMOUNT_MAX).toBe(999_999_999_999.99)
+ })
+})
diff --git a/frontend/modules/commercial/utils/forms/amountInput.ts b/frontend/modules/commercial/utils/forms/amountInput.ts
new file mode 100644
index 0000000..8fc4310
--- /dev/null
+++ b/frontend/modules/commercial/utils/forms/amountInput.ts
@@ -0,0 +1,29 @@
+/**
+ * Helpers de saisie des montants des formulaires Client / Fournisseur (commercial).
+ * Purs / testables. Pendant FRONT de la contrainte back `LessThanOrEqual` posee sur
+ * `revenueAmount` (Client/Supplier) — retour metier ERP-193 : le chiffre d'affaires
+ * est plafonne a 999 999 999 999,99.
+ */
+
+/** Plafond metier du chiffre d'affaires (CA) : 999 999 999 999,99. */
+export const REVENUE_AMOUNT_MAX = 999_999_999_999.99
+
+/** Valeur « modele » (decimale a point, sans separateur) renvoyee quand on plafonne. */
+const REVENUE_AMOUNT_MAX_MODEL = '999999999999.99'
+
+/**
+ * Plafonne le CA au maximum metier. Recoit le modele emis par `MalioInputAmount`
+ * (chaine propre a decimale `.`, sans espaces) ; tolere malgre tout une virgule /
+ * des espaces par securite. Renvoie la valeur telle quelle si elle est vide, non
+ * numerique ou sous le plafond ; sinon la valeur plafonnee.
+ */
+export function clampRevenueAmount(value: string | null | undefined): string | null | undefined {
+ if (value === null || value === undefined || value === '') {
+ return value
+ }
+ const n = Number(String(value).replace(/\s/g, '').replace(',', '.'))
+ if (Number.isNaN(n)) {
+ return value
+ }
+ return n > REVENUE_AMOUNT_MAX ? REVENUE_AMOUNT_MAX_MODEL : value
+}
diff --git a/src/Module/Commercial/Domain/Entity/Client.php b/src/Module/Commercial/Domain/Entity/Client.php
index d0e9eb4..478cf26 100644
--- a/src/Module/Commercial/Domain/Entity/Client.php
+++ b/src/Module/Commercial/Domain/Entity/Client.php
@@ -233,7 +233,10 @@ class Client implements TimestampableInterface, BlamableInterface, ClientInterfa
#[Groups(['client:read', 'client:write:information'])]
private ?int $employeesCount = null;
+ // Chiffre d'affaires plafonne a 999 999 999 999,99 (12 chiffres, retour metier
+ // ERP-193). La colonne (decimal 15,2) tolere plus, mais le metier borne la saisie.
#[ORM\Column(type: 'decimal', precision: 15, scale: 2, nullable: true)]
+ #[Assert\LessThanOrEqual(value: 999_999_999_999.99, message: 'Le chiffre d\'affaires ne peut pas dépasser 999 999 999 999,99.')]
#[Groups(['client:read', 'client:write:information'])]
private ?string $revenueAmount = null;
diff --git a/src/Module/Commercial/Domain/Entity/Supplier.php b/src/Module/Commercial/Domain/Entity/Supplier.php
index fbd9c4d..a1ca155 100644
--- a/src/Module/Commercial/Domain/Entity/Supplier.php
+++ b/src/Module/Commercial/Domain/Entity/Supplier.php
@@ -212,7 +212,10 @@ class Supplier implements TimestampableInterface, BlamableInterface, SupplierInt
#[Groups(['supplier:read', 'supplier:write:information'])]
private ?int $employeesCount = null;
+ // Chiffre d'affaires plafonne a 999 999 999 999,99 (12 chiffres, retour metier
+ // ERP-193). La colonne (decimal 15,2) tolere plus, mais le metier borne la saisie.
#[ORM\Column(type: 'decimal', precision: 15, scale: 2, nullable: true)]
+ #[Assert\LessThanOrEqual(value: 999_999_999_999.99, message: 'Le chiffre d\'affaires ne peut pas dépasser 999 999 999 999,99.')]
#[Groups(['supplier:read', 'supplier:write:information'])]
private ?string $revenueAmount = null;
diff --git a/tests/Architecture/EntityConstraintsHaveFrenchMessageTest.php b/tests/Architecture/EntityConstraintsHaveFrenchMessageTest.php
index 74374e7..5a6790c 100644
--- a/tests/Architecture/EntityConstraintsHaveFrenchMessageTest.php
+++ b/tests/Architecture/EntityConstraintsHaveFrenchMessageTest.php
@@ -95,6 +95,7 @@ final class EntityConstraintsHaveFrenchMessageTest extends TestCase
Assert\Positive::class,
Assert\NegativeOrZero::class,
Assert\Negative::class,
+ Assert\LessThanOrEqual::class,
];
public function testEveryConstraintHasAnExplicitFrenchMessage(): void
@@ -302,7 +303,9 @@ final class EntityConstraintsHaveFrenchMessageTest extends TestCase
Assert\Length::class => new Assert\Length(max: 1),
Assert\Count::class => new Assert\Count(min: 1),
Assert\Regex::class => new Assert\Regex(pattern: '/^x$/'),
- default => new $class(),
+ // AbstractComparison exige value|propertyPath des l'instanciation.
+ Assert\LessThanOrEqual::class => new Assert\LessThanOrEqual(value: 0),
+ default => new $class(),
};
$value = $bare->{$prop} ?? null;
diff --git a/tests/Module/Commercial/Api/RevenueAmountCapTest.php b/tests/Module/Commercial/Api/RevenueAmountCapTest.php
new file mode 100644
index 0000000..e49af93
--- /dev/null
+++ b/tests/Module/Commercial/Api/RevenueAmountCapTest.php
@@ -0,0 +1,81 @@
+ 422 porte sur revenueAmount. */
+ public function testClientRevenueAmountAuDelaDuPlafondEst422(): void
+ {
+ $client = $this->createAdminClient();
+ $seed = $this->seedClient('CA Cap Client SARL');
+
+ $body = $client->request('PATCH', '/api/clients/'.$seed->getId(), [
+ 'headers' => ['Content-Type' => self::MERGE],
+ 'json' => ['revenueAmount' => '1000000000000.00'],
+ ])->toArray(false);
+
+ self::assertResponseStatusCodeSame(422);
+ self::assertArrayHasKey('revenueAmount', $this->violationsByPath($body));
+ }
+
+ /** Client : CA exactement au plafond -> accepte (200). */
+ public function testClientRevenueAmountAuPlafondEst200(): void
+ {
+ $client = $this->createAdminClient();
+ $seed = $this->seedClient('CA Max Client SARL');
+
+ $client->request('PATCH', '/api/clients/'.$seed->getId(), [
+ 'headers' => ['Content-Type' => self::MERGE],
+ 'json' => ['revenueAmount' => self::MAX],
+ ]);
+
+ self::assertResponseStatusCodeSame(200);
+ }
+
+ /** Fournisseur : CA au-dela du plafond -> 422 porte sur revenueAmount. */
+ public function testSupplierRevenueAmountAuDelaDuPlafondEst422(): void
+ {
+ $client = $this->createAdminClient();
+ $seed = $this->seedSupplier('CA Cap Fournisseur SARL');
+
+ $body = $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [
+ 'headers' => ['Content-Type' => self::MERGE],
+ 'json' => ['revenueAmount' => '1000000000000.00'],
+ ])->toArray(false);
+
+ self::assertResponseStatusCodeSame(422);
+ self::assertArrayHasKey('revenueAmount', $this->violationsByPath($body));
+ }
+
+ /** Fournisseur : CA exactement au plafond -> accepte (200). */
+ public function testSupplierRevenueAmountAuPlafondEst200(): void
+ {
+ $client = $this->createAdminClient();
+ $seed = $this->seedSupplier('CA Max Fournisseur SARL');
+
+ $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [
+ 'headers' => ['Content-Type' => self::MERGE],
+ 'json' => ['revenueAmount' => self::MAX],
+ ]);
+
+ self::assertResponseStatusCodeSame(200);
+ }
+}