diff --git a/frontend/modules/commercial/pages/clients/[id]/edit.vue b/frontend/modules/commercial/pages/clients/[id]/edit.vue index bf078ae..74f27c5 100644 --- a/frontend/modules/commercial/pages/clients/[id]/edit.vue +++ b/frontend/modules/commercial/pages/clients/[id]/edit.vue @@ -110,11 +110,14 @@ :readonly="businessReadonly" :error="informationErrors.errors.competitors" /> + @@ -428,6 +431,7 @@ import { type MainFormDraft, } from '~/modules/commercial/utils/forms/clientEdit' import { clampRevenueAmount } from '~/modules/commercial/utils/forms/amountInput' +import { todayIso } from '~/shared/utils/date' import { buildClientFormTabKeys, isAddressValid, @@ -497,6 +501,9 @@ const main = reactive(mapMainDraft({} as ClientDetail)) const information = reactive(mapInformationDraft({} as ClientDetail)) const accounting = reactive(mapAccountingFormDraft({} as ClientDetail)) +// Borne haute de la date de creation : aujourd'hui (ERP-193, pas de date future). +const maxFoundedAt = todayIso() + // 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) diff --git a/frontend/modules/commercial/pages/clients/new.vue b/frontend/modules/commercial/pages/clients/new.vue index 1ab0949..051c01e 100644 --- a/frontend/modules/commercial/pages/clients/new.vue +++ b/frontend/modules/commercial/pages/clients/new.vue @@ -105,11 +105,14 @@ :readonly="isValidated('information')" :error="informationErrors.errors.competitors" /> + @@ -412,6 +415,7 @@ import { showsRelationAndTriageFields, } from '~/modules/commercial/utils/forms/clientFormRules' import { clampRevenueAmount } from '~/modules/commercial/utils/forms/amountInput' +import { todayIso } from '~/shared/utils/date' import { buildAddressPayload, buildMainPayload, @@ -670,6 +674,9 @@ const information = reactive({ directorName: null as string | null, }) +// Borne haute de la date de creation : aujourd'hui (ERP-193, pas de date future). +const maxFoundedAt = todayIso() + // 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) diff --git a/frontend/modules/commercial/pages/suppliers/[id]/edit.vue b/frontend/modules/commercial/pages/suppliers/[id]/edit.vue index 1399482..d549b3b 100644 --- a/frontend/modules/commercial/pages/suppliers/[id]/edit.vue +++ b/frontend/modules/commercial/pages/suppliers/[id]/edit.vue @@ -71,11 +71,14 @@ :readonly="businessReadonly" :error="informationErrors.errors.competitors" /> + @@ -397,6 +400,7 @@ import { type SupplierEditAbilities, } from '~/modules/commercial/utils/forms/supplierEdit' import { clampRevenueAmount } from '~/modules/commercial/utils/forms/amountInput' +import { todayIso } from '~/shared/utils/date' import { buildSupplierFormTabKeys, isAddressValid, @@ -463,6 +467,9 @@ const main = reactive(mapMainDraft({} as SupplierDetail)) const information = reactive(mapInformationDraft({} as SupplierDetail)) const accounting = reactive(mapAccountingFormDraft({} as SupplierDetail)) +// Borne haute de la date de creation : aujourd'hui (ERP-193, pas de date future). +const maxFoundedAt = todayIso() + // 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) diff --git a/frontend/modules/commercial/pages/suppliers/new.vue b/frontend/modules/commercial/pages/suppliers/new.vue index deccbc2..6e79a58 100644 --- a/frontend/modules/commercial/pages/suppliers/new.vue +++ b/frontend/modules/commercial/pages/suppliers/new.vue @@ -65,11 +65,14 @@ :readonly="isValidated('information')" :error="informationErrors.errors.competitors" /> + @@ -380,6 +383,7 @@ import { buildRibPayload, } from '~/modules/commercial/utils/forms/supplierEdit' import { clampRevenueAmount } from '~/modules/commercial/utils/forms/amountInput' +import { todayIso } from '~/shared/utils/date' import { emptyAddress, emptyContact, @@ -569,6 +573,9 @@ const information = reactive({ volumeForecast: null as string | null, }) +// Borne haute de la date de creation : aujourd'hui (ERP-193, pas de date future). +const maxFoundedAt = todayIso() + // 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) diff --git a/frontend/shared/utils/__tests__/date.test.ts b/frontend/shared/utils/__tests__/date.test.ts new file mode 100644 index 0000000..af5ec17 --- /dev/null +++ b/frontend/shared/utils/__tests__/date.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, it } from 'vitest' +import { todayIso } from '../date' + +describe('todayIso', () => { + it('formate la date locale en YYYY-MM-DD (zero-pad mois/jour)', () => { + // 7 mars 2026 (heure locale) -> '2026-03-07'. + expect(todayIso(new Date(2026, 2, 7, 10, 30))).toBe('2026-03-07') + }) + + it('utilise les composantes LOCALES, pas UTC (pas de decalage de minuit)', () => { + // 18 juin 2026 23:30 heure locale : la date locale reste le 18 meme si + // toISOString() (UTC) basculerait au 19 selon le fuseau. + expect(todayIso(new Date(2026, 5, 18, 23, 30))).toBe('2026-06-18') + }) + + it('gere le dernier jour de l\'annee', () => { + expect(todayIso(new Date(2026, 11, 31, 12, 0))).toBe('2026-12-31') + }) +}) diff --git a/frontend/shared/utils/date.ts b/frontend/shared/utils/date.ts new file mode 100644 index 0000000..7123ffa --- /dev/null +++ b/frontend/shared/utils/date.ts @@ -0,0 +1,17 @@ +/** + * Helpers de date purs / testables (partages inter-modules). + */ + +/** + * Date du jour au format ISO `YYYY-MM-DD` en heure LOCALE. + * + * On NE passe PAS par `toISOString()` (UTC) : pres de minuit, le decalage de + * fuseau (FR = UTC+1/+2) renverrait la veille ou le lendemain. On lit donc les + * composantes locales. Parametre `now` injectable pour les tests. + */ +export function todayIso(now: Date = new Date()): string { + const year = now.getFullYear() + const month = String(now.getMonth() + 1).padStart(2, '0') + const day = String(now.getDate()).padStart(2, '0') + return `${year}-${month}-${day}` +} diff --git a/src/Module/Commercial/Domain/Entity/Client.php b/src/Module/Commercial/Domain/Entity/Client.php index 478cf26..7386830 100644 --- a/src/Module/Commercial/Domain/Entity/Client.php +++ b/src/Module/Commercial/Domain/Entity/Client.php @@ -219,6 +219,8 @@ class Client implements TimestampableInterface, BlamableInterface, ClientInterfa #[ORM\Column(type: 'date_immutable', nullable: true)] #[Groups(['client:read', 'client:write:information'])] + // Date de creation jamais dans le futur (retour metier ERP-193) : <= aujourd'hui. + #[Assert\LessThanOrEqual(value: 'today', message: 'La date de création ne peut pas être dans le futur.')] // Format d'ENTREE strict ISO `Y-m-d` (le `!` remet l'heure a 00:00:00). Sans // ce format, PHP DateTime accepte des formes ambigues : « 12/25/2026 » (que // le front MalioDate juge invalide en JJ/MM/AAAA) serait sinon interprete en diff --git a/src/Module/Commercial/Domain/Entity/Supplier.php b/src/Module/Commercial/Domain/Entity/Supplier.php index a1ca155..78b0500 100644 --- a/src/Module/Commercial/Domain/Entity/Supplier.php +++ b/src/Module/Commercial/Domain/Entity/Supplier.php @@ -200,6 +200,8 @@ class Supplier implements TimestampableInterface, BlamableInterface, SupplierInt #[ORM\Column(type: 'date_immutable', nullable: true)] #[Groups(['supplier:read', 'supplier:write:information'])] + // Date de creation jamais dans le futur (retour metier ERP-193) : <= aujourd'hui. + #[Assert\LessThanOrEqual(value: 'today', message: 'La date de création ne peut pas être dans le futur.')] // Format d'ENTREE strict ISO `Y-m-d` : sans lui, PHP DateTime accepte des // formes ambigues (« 12/25/2026 », jugee invalide par MalioDate en JJ/MM/AAAA, // serait lue en M/J -> 25 decembre et acceptee a tort). Avec le format, toute diff --git a/tests/Module/Commercial/Api/FoundedAtFutureTest.php b/tests/Module/Commercial/Api/FoundedAtFutureTest.php new file mode 100644 index 0000000..31321e0 --- /dev/null +++ b/tests/Module/Commercial/Api/FoundedAtFutureTest.php @@ -0,0 +1,85 @@ + 422 portee sur foundedAt. */ + public function testClientFoundedAtFuturEst422(): void + { + $client = $this->createAdminClient(); + $seed = $this->seedClient('Founded Future SARL'); + + $body = $client->request('PATCH', '/api/clients/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => ['foundedAt' => $this->futureDate()], + ])->toArray(false); + + self::assertResponseStatusCodeSame(422); + self::assertArrayHasKey('foundedAt', $this->violationsByPath($body)); + } + + /** Client : date de creation passee -> acceptee (200). */ + public function testClientFoundedAtPasseEst200(): void + { + $client = $this->createAdminClient(); + $seed = $this->seedClient('Founded Past SARL'); + + $client->request('PATCH', '/api/clients/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => ['foundedAt' => '2000-06-15'], + ]); + + self::assertResponseStatusCodeSame(200); + } + + /** Fournisseur : date de creation future -> 422 portee sur foundedAt. */ + public function testSupplierFoundedAtFuturEst422(): void + { + $client = $this->createAdminClient(); + $seed = $this->seedSupplier('Founded Future Fournisseur SARL'); + + $body = $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => ['foundedAt' => $this->futureDate()], + ])->toArray(false); + + self::assertResponseStatusCodeSame(422); + self::assertArrayHasKey('foundedAt', $this->violationsByPath($body)); + } + + /** Fournisseur : date de creation passee -> acceptee (200). */ + public function testSupplierFoundedAtPasseEst200(): void + { + $client = $this->createAdminClient(); + $seed = $this->seedSupplier('Founded Past Fournisseur SARL'); + + $client->request('PATCH', '/api/suppliers/'.$seed->getId(), [ + 'headers' => ['Content-Type' => self::MERGE], + 'json' => ['foundedAt' => '2000-06-15'], + ]); + + self::assertResponseStatusCodeSame(200); + } + + /** Date ISO clairement dans le futur. */ + private function futureDate(): string + { + return new DateTimeImmutable('+1 year')->format('Y-m-d'); + } +}