UDT-011: Localización Temporal Argentina (infra transversal) #25
@@ -27,6 +27,7 @@ import {
|
|||||||
import { useNuevaVersionIngresosBrutos } from '../hooks/useIngresosBrutos'
|
import { useNuevaVersionIngresosBrutos } from '../hooks/useIngresosBrutos'
|
||||||
import type { IngresosBrutos } from '../types/ingresosBrutos.types'
|
import type { IngresosBrutos } from '../types/ingresosBrutos.types'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import { prevCivilDate, formatCivilDate } from '@/lib/dateFormat'
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
alicuota: z.coerce
|
alicuota: z.coerce
|
||||||
@@ -48,11 +49,12 @@ interface NuevaVigenciaIibbModalProps {
|
|||||||
onSuccess: () => void
|
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 '—'
|
if (!vigenciaDesde || !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaDesde)) return '—'
|
||||||
const d = new Date(vigenciaDesde + 'T00:00:00')
|
return formatCivilDate(prevCivilDate(vigenciaDesde))
|
||||||
d.setDate(d.getDate() - 1)
|
|
||||||
return d.toISOString().slice(0, 10)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBackendError(err: unknown): string | null {
|
function resolveBackendError(err: unknown): string | null {
|
||||||
@@ -212,7 +214,7 @@ export function NuevaVigenciaIibbModal({
|
|||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Versión actual ({item.alicuota}%) quedará cerrada el{' '}
|
Versión actual ({item.alicuota}%) quedará cerrada el{' '}
|
||||||
<strong>{fechaCierre(watchedVigencia)}</strong>.
|
<strong>{fechaCierreDisplay(watchedVigencia)}</strong>.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground font-medium">
|
<p className="text-xs text-muted-foreground font-medium">
|
||||||
Esta acción no se puede deshacer.
|
Esta acción no se puede deshacer.
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ import {
|
|||||||
import { useNuevaVersionTipoDeIva } from '../hooks/useTiposDeIva'
|
import { useNuevaVersionTipoDeIva } from '../hooks/useTiposDeIva'
|
||||||
import type { TipoDeIva } from '../types/tipoDeIva.types'
|
import type { TipoDeIva } from '../types/tipoDeIva.types'
|
||||||
import { toast } from 'sonner'
|
import { toast } from 'sonner'
|
||||||
|
import { prevCivilDate, formatCivilDate } from '@/lib/dateFormat'
|
||||||
|
|
||||||
const formSchema = z.object({
|
const formSchema = z.object({
|
||||||
porcentaje: z.coerce
|
porcentaje: z.coerce
|
||||||
@@ -49,12 +50,12 @@ interface NuevaVigenciaModalProps {
|
|||||||
onSuccess: () => void
|
onSuccess: () => void
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Devuelve la fecha anterior (vigenciaDesde - 1 día) como string "yyyy-MM-dd" */
|
/** Formatea la fecha de cierre (vigenciaDesde - 1 día) para display "dd/MM/yyyy".
|
||||||
function fechaCierre(vigenciaDesde: string): string {
|
* 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 '—'
|
if (!vigenciaDesde || !/^\d{4}-\d{2}-\d{2}$/.test(vigenciaDesde)) return '—'
|
||||||
const d = new Date(vigenciaDesde + 'T00:00:00')
|
return formatCivilDate(prevCivilDate(vigenciaDesde))
|
||||||
d.setDate(d.getDate() - 1)
|
|
||||||
return d.toISOString().slice(0, 10)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolveBackendError(err: unknown): string | null {
|
function resolveBackendError(err: unknown): string | null {
|
||||||
@@ -218,7 +219,7 @@ export function NuevaVigenciaModal({
|
|||||||
</p>
|
</p>
|
||||||
<p className="text-muted-foreground">
|
<p className="text-muted-foreground">
|
||||||
Versión actual ({item.porcentaje}%) quedará cerrada el{' '}
|
Versión actual ({item.porcentaje}%) quedará cerrada el{' '}
|
||||||
<strong>{fechaCierre(watchedVigencia)}</strong>.
|
<strong>{fechaCierreDisplay(watchedVigencia)}</strong>.
|
||||||
</p>
|
</p>
|
||||||
<p className="text-xs text-muted-foreground font-medium">
|
<p className="text-xs text-muted-foreground font-medium">
|
||||||
Esta acción no se puede deshacer.
|
Esta acción no se puede deshacer.
|
||||||
|
|||||||
@@ -87,6 +87,20 @@ export function parseCivilDate(yyyyMmDd: string): { year: number; month: number;
|
|||||||
return { year: y, month: m, day: d };
|
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 <input type="datetime-local"> (ART implícito) a ISO UTC.
|
* Convierte un valor de <input type="datetime-local"> (ART implícito) a ISO UTC.
|
||||||
* Input: "2026-05-01T22:30" (sin TZ) → interpretado como ART → "2026-05-02T01:30:00.000Z".
|
* Input: "2026-05-01T22:30" (sin TZ) → interpretado como ART → "2026-05-02T01:30:00.000Z".
|
||||||
|
|||||||
@@ -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()
|
renderModal()
|
||||||
|
|
||||||
const porcentajeInput = screen.getByLabelText(/porcentaje nuevo/i)
|
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)
|
const vigenciaInput = screen.getByLabelText(/vigencia desde/i)
|
||||||
await userEvent.type(vigenciaInput, '2026-05-01')
|
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(() =>
|
await waitFor(() =>
|
||||||
expect(screen.getByText(/2026-04-30/)).toBeInTheDocument(),
|
expect(screen.getByText(/30\/04\/2026/)).toBeInTheDocument(),
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
formatCivilDateRange,
|
formatCivilDateRange,
|
||||||
todayArgentina,
|
todayArgentina,
|
||||||
parseCivilDate,
|
parseCivilDate,
|
||||||
|
prevCivilDate,
|
||||||
} from '@/lib/dateFormat';
|
} from '@/lib/dateFormat';
|
||||||
|
|
||||||
describe('dateFormat.ts', () => {
|
describe('dateFormat.ts', () => {
|
||||||
@@ -108,4 +109,22 @@ describe('dateFormat.ts', () => {
|
|||||||
expect(result.day).toBe(1);
|
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');
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user