From 71d092838957a1a0d922c9408842284f0d64aa2c Mon Sep 17 00:00:00 2001 From: dmolinari Date: Sat, 18 Apr 2026 10:24:15 -0300 Subject: [PATCH] fix(web/udt-011): NuevaVigenciaModal preview usa prevCivilDate+formatCivilDate sin Date() (fix BUG-FE-04) --- .../components/NuevaVigenciaIibbModal.tsx | 12 +++++++----- .../iva/components/NuevaVigenciaModal.tsx | 13 +++++++------ src/web/src/lib/dateFormat.ts | 14 ++++++++++++++ .../fiscal/iva/NuevaVigenciaModal.test.tsx | 6 +++--- src/web/src/tests/lib/dateFormat.test.ts | 19 +++++++++++++++++++ 5 files changed, 50 insertions(+), 14 deletions(-) diff --git a/src/web/src/features/fiscal/iibb/components/NuevaVigenciaIibbModal.tsx b/src/web/src/features/fiscal/iibb/components/NuevaVigenciaIibbModal.tsx index f435922..1c30bcc 100644 --- a/src/web/src/features/fiscal/iibb/components/NuevaVigenciaIibbModal.tsx +++ b/src/web/src/features/fiscal/iibb/components/NuevaVigenciaIibbModal.tsx @@ -27,6 +27,7 @@ import { import { useNuevaVersionIngresosBrutos } from '../hooks/useIngresosBrutos' import type { IngresosBrutos } from '../types/ingresosBrutos.types' import { toast } from 'sonner' +import { prevCivilDate, formatCivilDate } from '@/lib/dateFormat' const formSchema = z.object({ alicuota: z.coerce @@ -48,11 +49,12 @@ interface NuevaVigenciaIibbModalProps { onSuccess: () => void } -function fechaCierre(vigenciaDesde: string): string { +/** Formatea la fecha de cierre (vigenciaDesde - 1 día) para display "dd/MM/yyyy". + * Usa prevCivilDate (Date.UTC pura, sin TZ) + formatCivilDate (split manual). + */ +function fechaCierreDisplay(vigenciaDesde: string): string { if (!vigenciaDesde || !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaDesde)) return '—' - const d = new Date(vigenciaDesde + 'T00:00:00') - d.setDate(d.getDate() - 1) - return d.toISOString().slice(0, 10) + return formatCivilDate(prevCivilDate(vigenciaDesde)) } function resolveBackendError(err: unknown): string | null { @@ -212,7 +214,7 @@ export function NuevaVigenciaIibbModal({

Versión actual ({item.alicuota}%) quedará cerrada el{' '} - {fechaCierre(watchedVigencia)}. + {fechaCierreDisplay(watchedVigencia)}.

Esta acción no se puede deshacer. diff --git a/src/web/src/features/fiscal/iva/components/NuevaVigenciaModal.tsx b/src/web/src/features/fiscal/iva/components/NuevaVigenciaModal.tsx index 010f2b6..c132447 100644 --- a/src/web/src/features/fiscal/iva/components/NuevaVigenciaModal.tsx +++ b/src/web/src/features/fiscal/iva/components/NuevaVigenciaModal.tsx @@ -28,6 +28,7 @@ import { import { useNuevaVersionTipoDeIva } from '../hooks/useTiposDeIva' import type { TipoDeIva } from '../types/tipoDeIva.types' import { toast } from 'sonner' +import { prevCivilDate, formatCivilDate } from '@/lib/dateFormat' const formSchema = z.object({ porcentaje: z.coerce @@ -49,12 +50,12 @@ interface NuevaVigenciaModalProps { onSuccess: () => void } -/** Devuelve la fecha anterior (vigenciaDesde - 1 día) como string "yyyy-MM-dd" */ -function fechaCierre(vigenciaDesde: string): string { +/** Formatea la fecha de cierre (vigenciaDesde - 1 día) para display "dd/MM/yyyy". + * Usa prevCivilDate (Date.UTC pura, sin TZ) + formatCivilDate (split manual). + */ +function fechaCierreDisplay(vigenciaDesde: string): string { if (!vigenciaDesde || !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaDesde)) return '—' - const d = new Date(vigenciaDesde + 'T00:00:00') - d.setDate(d.getDate() - 1) - return d.toISOString().slice(0, 10) + return formatCivilDate(prevCivilDate(vigenciaDesde)) } function resolveBackendError(err: unknown): string | null { @@ -218,7 +219,7 @@ export function NuevaVigenciaModal({

Versión actual ({item.porcentaje}%) quedará cerrada el{' '} - {fechaCierre(watchedVigencia)}. + {fechaCierreDisplay(watchedVigencia)}.

Esta acción no se puede deshacer. diff --git a/src/web/src/lib/dateFormat.ts b/src/web/src/lib/dateFormat.ts index 39b931a..d757788 100644 --- a/src/web/src/lib/dateFormat.ts +++ b/src/web/src/lib/dateFormat.ts @@ -87,6 +87,20 @@ export function parseCivilDate(yyyyMmDd: string): { year: number; month: number; return { year: y, month: m, day: d }; } +/** + * Retorna el día anterior a una fecha civil Argentina en formato "yyyy-MM-dd". + * Usa Date.UTC para aritmética pura — sin conversión de timezone en ningún momento. + * Output: "yyyy-MM-dd" + */ +export function prevCivilDate(yyyyMmDd: string): string { + const [y, m, d] = yyyyMmDd.split('-').map(Number); + const prevDay = new Date(Date.UTC(y, m - 1, d - 1)); + const yy = prevDay.getUTCFullYear(); + const mm = String(prevDay.getUTCMonth() + 1).padStart(2, '0'); + const dd = String(prevDay.getUTCDate()).padStart(2, '0'); + return `${yy}-${mm}-${dd}`; +} + /** * Convierte un valor de (ART implícito) a ISO UTC. * Input: "2026-05-01T22:30" (sin TZ) → interpretado como ART → "2026-05-02T01:30:00.000Z". diff --git a/src/web/src/tests/features/fiscal/iva/NuevaVigenciaModal.test.tsx b/src/web/src/tests/features/fiscal/iva/NuevaVigenciaModal.test.tsx index ccc688f..30ff538 100644 --- a/src/web/src/tests/features/fiscal/iva/NuevaVigenciaModal.test.tsx +++ b/src/web/src/tests/features/fiscal/iva/NuevaVigenciaModal.test.tsx @@ -122,7 +122,7 @@ describe('NuevaVigenciaModal — preview con fechas correctas [REQ-UI-004]', () ) }) - it('preview muestra fecha de cierre = vigenciaDesde - 1 día', async () => { + it('preview muestra fecha de cierre = vigenciaDesde - 1 día (formato dd/MM/yyyy, BUG-FE-04)', async () => { renderModal() const porcentajeInput = screen.getByLabelText(/porcentaje nuevo/i) @@ -132,9 +132,9 @@ describe('NuevaVigenciaModal — preview con fechas correctas [REQ-UI-004]', () const vigenciaInput = screen.getByLabelText(/vigencia desde/i) await userEvent.type(vigenciaInput, '2026-05-01') - // La versión anterior cierra el día anterior → 2026-04-30 + // La versión anterior cierra el día anterior → 30/04/2026 (formato AR civil) await waitFor(() => - expect(screen.getByText(/2026-04-30/)).toBeInTheDocument(), + expect(screen.getByText(/30\/04\/2026/)).toBeInTheDocument(), ) }) }) diff --git a/src/web/src/tests/lib/dateFormat.test.ts b/src/web/src/tests/lib/dateFormat.test.ts index c7f1934..a7e4624 100644 --- a/src/web/src/tests/lib/dateFormat.test.ts +++ b/src/web/src/tests/lib/dateFormat.test.ts @@ -6,6 +6,7 @@ import { formatCivilDateRange, todayArgentina, parseCivilDate, + prevCivilDate, } from '@/lib/dateFormat'; describe('dateFormat.ts', () => { @@ -108,4 +109,22 @@ describe('dateFormat.ts', () => { expect(result.day).toBe(1); }); }); + + describe('prevCivilDate (BUG-FE-04 — fecha cierre NuevaVigencia)', () => { + it('returns day before (2026-05-01 → 2026-04-30)', () => { + expect(prevCivilDate('2026-05-01')).toBe('2026-04-30'); + }); + + it('crosses month boundary (2026-05-31 → 2026-05-30)', () => { + expect(prevCivilDate('2026-06-01')).toBe('2026-05-31'); + }); + + it('crosses year boundary (2026-01-01 → 2025-12-31)', () => { + expect(prevCivilDate('2026-01-01')).toBe('2025-12-31'); + }); + + it('handles leap year (2024-03-01 → 2024-02-29)', () => { + expect(prevCivilDate('2024-03-01')).toBe('2024-02-29'); + }); + }); });