chore: catálogo obligaciones, cierre automático, fixes SAT y facturación

- Catálogo de obligaciones fiscales expandido a 30 entradas con campo requierePago.
- Soporte de frecuencia cuatrimestral en obligaciones y declaraciones.
- Automatización de cierre de obligaciones fiscales desde Documentos › Declaraciones.
- Nuevas tablas obligacion_evidencias, obligacion_periodos estados y declaracion_obligaciones.
- Nuevo servicio obligacion-evidencias.service.ts y endpoints REST.
- Refactor de declaraciones.service.ts para vincular obligaciones y crear evidencias.
- Notificaciones por email para evidencias de obligaciones.
- Adjuntar PDFs en correo de declaración subida.
- Fix drill-down de CFDIs: carga completa al visualizar.
- Fix sincronización SAT: tipos P/N, UUID case-insensitive, no reutilizar requestId.
- Fix suscripciones pending en /configuracion/planes-despacho.
- Fix sugerencias de Clave Producto SAT: importar catálogo y robustecer autocomplete.
- Quitar toggle manual de completado en Configuración › Obligaciones fiscales › Tareas.
- Scripts de soporte para Demo Ventas y utilerías (change-user-email, resend-welcome, import-clave-prod-serv).
- Documentación de cambios en docs/CAMBIOS-2026-05-04.md.
This commit is contained in:
Horux Dev
2026-06-22 04:53:59 +00:00
parent b217342a96
commit 7df27ce66d
39 changed files with 2791 additions and 191 deletions

View File

@@ -189,6 +189,7 @@ export default function ObligacionesPage() {
mensual: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300',
bimestral: 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300',
trimestral: 'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300',
cuatrimestral: 'bg-pink-100 text-pink-700 dark:bg-pink-900 dark:text-pink-300',
anual: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300',
eventual: 'bg-gray-100 text-gray-700 dark:bg-gray-800 dark:text-gray-300',
};

View File

@@ -7,6 +7,7 @@ import { apiClient } from '@/lib/api/client';
import { subscribeMe, changeMyPlan, cancelMySubscription, upgradeMe, generatePaymentLink } from '@/lib/api/subscription';
import { getPendingInvitation, acceptInvitation } from '@/lib/api/trial-invitations';
import { useAuthStore } from '@/stores/auth-store';
import { getSubscriptionState } from '@horux/shared';
type Despachoplan = 'trial' | 'business_control' | 'business_cloud' | 'mi_empresa' | 'mi_empresa_plus' | 'custom';
type PaidPlan = 'business_control' | 'business_cloud' | 'mi_empresa' | 'mi_empresa_plus';
@@ -89,15 +90,14 @@ export default function PlanesDespachoPage() {
// El usuario puede cancelar si tiene una suscripción que aún corre (paid, trial,
// custom). Si ya está cancelada o expirada, no hay nada que cancelar.
const subStatus = planInfo?.subscription?.status ?? null;
const hasActiveSub = subStatus != null
&& subStatus !== 'cancelled'
&& subStatus !== 'trial_expired';
// Estados en los que se puede generar un link de pago (incluye trial y vencido).
const subState = planInfo?.subscription ? getSubscriptionState(planInfo.subscription) : null;
const hasActiveSub = subState?.isActive || subState?.isTrial || subState?.isCancelledInPeriod || false;
// Estados en los que se puede generar un link de pago (incluye trial, vencido y pending).
const isPayableStatus = subStatus === 'trial'
|| subStatus === 'trial_expired'
|| subStatus === 'pending'
|| hasActiveSub;
const isCurrentPlanPaid = currentPlan === planInfo?.subscription?.plan
&& (subStatus === 'authorized' || subStatus === 'pending');
const isCurrentPlanPaid = currentPlan === planInfo?.subscription?.plan && subState?.isActive === true;
/** Resuelve la frecuencia para un plan. Mi Empresa y Mi Empresa+ leen su
* propio toggle; el resto (business_*) siempre annual. */
@@ -112,6 +112,15 @@ export default function PlanesDespachoPage() {
setBusy(plan);
setMessage(null);
try {
// Si el plan actual está pendiente de pago, solo regeneramos el link de pago.
if (currentPlan === plan && subState?.isPending) {
return await handlePagarAhora();
}
// Si tiene una sub pendiente en otro plan, no permitir cambiar hasta pagar.
if (subState?.isPending) {
setMessage({ kind: 'err', text: 'Completa el pago del plan actual antes de cambiar de plan.' });
return;
}
// Sin sub activa: subscribe directo → MP (preapproval del plan completo).
const result = await subscribeMe({ plan, frequency });
window.open(result.paymentUrl, '_blank');
@@ -197,10 +206,10 @@ export default function PlanesDespachoPage() {
}
}
function ActiveBadge() {
function CurrentPlanBadge({ pending }: { pending?: boolean }) {
return (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-green-600 text-white text-xs px-3 py-1 rounded-full font-medium whitespace-nowrap">
Plan actual
<div className={`absolute -top-3 left-1/2 -translate-x-1/2 text-white text-xs px-3 py-1 rounded-full font-medium whitespace-nowrap ${pending ? 'bg-yellow-600' : 'bg-green-600'}`}>
{pending ? 'Plan actual — pendiente' : 'Plan actual'}
</div>
);
}
@@ -325,7 +334,7 @@ export default function PlanesDespachoPage() {
)}
{/* Banner de suscripción activa */}
{!loading && planInfo?.subscription && hasPaidPlan && (subStatus === 'authorized' || subStatus === 'pending') && (() => {
{!loading && planInfo?.subscription && hasPaidPlan && subState?.isActive && (() => {
const sub = planInfo.subscription;
const periodEndDate = sub.currentPeriodEnd ? new Date(sub.currentPeriodEnd) : null;
const fechaFormato = periodEndDate
@@ -352,6 +361,21 @@ export default function PlanesDespachoPage() {
);
})()}
{/* Banner de suscripción pendiente */}
{!loading && planInfo?.subscription && hasPaidPlan && subState?.isPending && (
<div className="flex items-start gap-3 bg-yellow-50 dark:bg-yellow-950 border border-yellow-200 dark:border-yellow-800 rounded-lg px-4 py-3 max-w-3xl mx-auto">
<Clock className="h-5 w-5 text-yellow-600 dark:text-yellow-400 flex-shrink-0 mt-0.5" />
<div className="text-sm space-y-0.5">
<div className="font-semibold text-yellow-800 dark:text-yellow-300">
Suscripción pendiente de pago
</div>
<div className="text-yellow-700 dark:text-yellow-400">
Tu suscripción aún no está activa. Completa el pago para evitar la suspensión del servicio.
</div>
</div>
</div>
)}
{/* Banner de trial vencido */}
{!loading && subStatus === 'trial_expired' && hasPaidPlan && (
<div className="flex items-start gap-3 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg px-4 py-3 max-w-3xl mx-auto">
@@ -423,7 +447,7 @@ export default function PlanesDespachoPage() {
<div className="grid md:grid-cols-2 lg:grid-cols-4 gap-6 max-w-7xl mx-auto">
{/* Mi Empresa */}
<Card className={`relative flex flex-col${currentPlan === 'mi_empresa' ? ' ring-2 ring-green-500' : ''}`}>
{currentPlan === 'mi_empresa' && <ActiveBadge />}
{currentPlan === 'mi_empresa' && <CurrentPlanBadge pending={subState?.isPending} />}
<CardHeader className="text-center pb-2">
<div className="mx-auto bg-emerald-100 dark:bg-emerald-900 rounded-full p-3 w-fit mb-2">
<Cloud className="h-6 w-6 text-emerald-600 dark:text-emerald-400" />
@@ -457,7 +481,7 @@ export default function PlanesDespachoPage() {
{/* Mi Empresa + */}
<Card className={`relative flex flex-col${currentPlan === 'mi_empresa_plus' ? ' ring-2 ring-green-500' : ''}`}>
{currentPlan === 'mi_empresa_plus' && <ActiveBadge />}
{currentPlan === 'mi_empresa_plus' && <CurrentPlanBadge pending={subState?.isPending} />}
<CardHeader className="text-center pb-2">
<div className="mx-auto bg-teal-100 dark:bg-teal-900 rounded-full p-3 w-fit mb-2">
<Cloud className="h-6 w-6 text-teal-600 dark:text-teal-400" />
@@ -494,7 +518,7 @@ export default function PlanesDespachoPage() {
{/* Business Control */}
<Card className={`relative flex flex-col${currentPlan === 'business_control' ? ' ring-2 ring-green-500' : ' border-primary ring-2 ring-primary/20'}`}>
{currentPlan === 'business_control'
? <ActiveBadge />
? <CurrentPlanBadge pending={subState?.isPending} />
: (
<div className="absolute -top-3 left-1/2 -translate-x-1/2 bg-primary text-primary-foreground text-xs px-3 py-1 rounded-full">
Más popular
@@ -529,7 +553,7 @@ export default function PlanesDespachoPage() {
{/* Enterprise (key interna: business_cloud) */}
<Card className={`relative flex flex-col${currentPlan === 'business_cloud' ? ' ring-2 ring-green-500' : ''}`}>
{currentPlan === 'business_cloud' && <ActiveBadge />}
{currentPlan === 'business_cloud' && <CurrentPlanBadge pending={subState?.isPending} />}
<CardHeader className="text-center pb-2">
<div className="mx-auto bg-purple-100 dark:bg-purple-900 rounded-full p-3 w-fit mb-2">
<Cloud className="h-6 w-6 text-purple-600 dark:text-purple-400" />

View File

@@ -23,9 +23,11 @@ import {
import { PapeleriaTab } from '@/components/documentos/papeleria-tab';
import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query';
import * as docsApi from '@/lib/api/documentos';
import { getObligacionesPorPeriodo, type ObligacionPeriodo } from '@/lib/api/obligaciones';
const MESES = ['Enero', 'Febrero', 'Marzo', 'Abril', 'Mayo', 'Junio', 'Julio', 'Agosto', 'Septiembre', 'Octubre', 'Noviembre', 'Diciembre'];
const IMPUESTOS: Impuesto[] = ['IVA', 'ISR', 'IEPS', 'ISN', 'DIOT', 'OTRO', 'ISH'];
const OBLIGACIONES_ROLES_UPLOAD = ['owner', 'cfo', 'contador', 'auxiliar', 'supervisor'];
const PERIODICIDADES: { value: Periodicidad; label: string }[] = [
{ value: 'mensual', label: 'Mensual' },
{ value: 'bimestral', label: 'Bimestral' },
@@ -504,7 +506,7 @@ function UploadDialog({ onClose }: { onClose: () => void }) {
const [tipo, setTipo] = useState<'normal' | 'complementaria'>('normal');
const [periodicidad, setPeriodicidad] = useState<Periodicidad>('mensual');
const yearsOptions = Array.from({ length: 6 }, (_, i) => currentYear - i);
const [impuestos, setImpuestos] = useState<Impuesto[]>([]);
const [obligacionesIds, setObligacionesIds] = useState<string[]>([]);
const [montoPago, setMontoPago] = useState('');
const [file, setFile] = useState<File | null>(null);
const [ligaFile, setLigaFile] = useState<File | null>(null);
@@ -512,6 +514,15 @@ function UploadDialog({ onClose }: { onClose: () => void }) {
const [err, setErr] = useState<string | null>(null);
const periodOptions = getPeriodOptions(periodicidad);
const periodo = `${año}-${String(mes).padStart(2, '0')}`;
const obligacionesQ = useQuery({
queryKey: ['obligaciones-periodo-declaracion', selectedContribuyenteId, periodo],
queryFn: () => selectedContribuyenteId
? getObligacionesPorPeriodo(selectedContribuyenteId, periodo, false)
: Promise.resolve({ data: [], periodo }),
enabled: !!selectedContribuyenteId,
});
const handlePeriodicidadChange = (p: Periodicidad) => {
setPeriodicidad(p);
@@ -522,21 +533,21 @@ function UploadDialog({ onClose }: { onClose: () => void }) {
}
};
const toggleImpuesto = (i: Impuesto) => {
setImpuestos(prev => prev.includes(i) ? prev.filter(x => x !== i) : [...prev, i]);
const toggleObligacion = (id: string) => {
setObligacionesIds(prev => prev.includes(id) ? prev.filter(x => x !== id) : [...prev, id]);
};
const submit = async (e: React.FormEvent) => {
e.preventDefault();
setErr(null);
if (!file) return setErr('Selecciona el PDF de la declaración');
if (impuestos.length === 0) return setErr('Selecciona al menos un impuesto');
if (obligacionesIds.length === 0) return setErr('Selecciona al menos una obligación fiscal');
try {
const pdfBase64 = await fileToBase64(file);
const ligaPagoBase64 = ligaFile ? await fileToBase64(ligaFile) : undefined;
const montoNum = montoPago.trim() !== '' ? parseFloat(montoPago) : undefined;
await create.mutateAsync({
año, mes, tipo, periodicidad, impuestos,
año, mes, tipo, periodicidad, obligacionesIds,
montoPago: montoNum,
pdfBase64, pdfFilename: file.name,
ligaPagoBase64,
@@ -606,16 +617,51 @@ function UploadDialog({ onClose }: { onClose: () => void }) {
</div>
<div>
<Label>Impuestos cubiertos</Label>
<div className="grid grid-cols-3 gap-2 mt-1">
{IMPUESTOS.map(i => (
<label key={i} className={`flex items-center gap-2 px-3 py-2 rounded-md border cursor-pointer text-sm ${impuestos.includes(i) ? 'bg-primary/10 border-primary' : 'hover:bg-muted'}`}>
<input type="checkbox" checked={impuestos.includes(i)} onChange={() => toggleImpuesto(i)} className="accent-primary" />
{i}
</label>
))}
</div>
<p className="text-xs text-muted-foreground mt-1">Selecciona todos los impuestos que incluye esta declaración definen qué recordatorios se desactivan.</p>
<Label>Obligaciones fiscales cubiertas</Label>
{!selectedContribuyenteId ? (
<p className="text-sm text-muted-foreground mt-1">Selecciona un contribuyente para ver sus obligaciones.</p>
) : obligacionesQ.isLoading ? (
<div className="flex items-center gap-2 text-sm text-muted-foreground mt-2">
<Loader2 className="h-4 w-4 animate-spin" /> Cargando obligaciones...
</div>
) : obligacionesQ.error ? (
<p className="text-sm text-red-600 mt-1">Error al cargar obligaciones.</p>
) : obligacionesQ.data?.data.length === 0 ? (
<p className="text-sm text-muted-foreground mt-1">No hay obligaciones fiscales configuradas para este periodo.</p>
) : (
<div className="space-y-3 mt-2 max-h-60 overflow-y-auto rounded-md border p-3">
{Array.from(new Set((obligacionesQ.data?.data || []).map(o => o.categoria || 'Sin categoría'))).map((categoria) => (
<div key={categoria}>
<p className="text-xs font-semibold uppercase text-muted-foreground mb-1.5">{categoria}</p>
<div className="grid grid-cols-1 gap-2">
{(obligacionesQ.data?.data || [])
.filter(o => (o.categoria || 'Sin categoría') === categoria)
.map((o) => (
<label
key={o.id}
className={`flex items-start gap-2 px-3 py-2 rounded-md border cursor-pointer text-sm ${obligacionesIds.includes(o.id) ? 'bg-primary/10 border-primary' : 'hover:bg-muted'}`}
>
<input
type="checkbox"
checked={obligacionesIds.includes(o.id)}
onChange={() => toggleObligacion(o.id)}
className="accent-primary mt-0.5"
/>
<div className="flex-1">
<span className="font-medium">{o.nombre}</span>
<span className="text-xs text-muted-foreground ml-2 capitalize">({o.frecuencia || '—'})</span>
{o.requierePago && (
<span className="block text-[10px] text-muted-foreground">Requiere comprobante de pago</span>
)}
</div>
</label>
))}
</div>
</div>
))}
</div>
)}
<p className="text-xs text-muted-foreground mt-1">Selecciona las obligaciones fiscales que cubre esta declaración. Al guardar se marcarán como presentadas y, si aplica, quedarán a la espera de su comprobante de pago.</p>
</div>
<div>

View File

@@ -11,6 +11,7 @@ import { formatCurrency, toCfdiDate } from '@/lib/utils';
import { exportToExcel } from '@/lib/export-excel';
import { useTableSort } from '@horux/shared-ui';
import { CfdiViewerModal } from '@/components/cfdi/cfdi-viewer-modal';
import { getCfdiById } from '@/lib/api/cfdi';
import { Eye, Download } from 'lucide-react';
import type { Cfdi } from '@horux/shared';
@@ -44,6 +45,7 @@ export default function DrillDownPage() {
const searchParams = useSearchParams();
const titulo = searchParams.get('titulo') || 'Detalle de CFDIs';
const [selectedCfdi, setSelectedCfdi] = useState<Cfdi | null>(null);
const [loadingCfdiId, setLoadingCfdiId] = useState<number | null>(null);
const { selectedContribuyenteId } = useContribuyenteStore();
const params = new URLSearchParams();
@@ -154,7 +156,23 @@ export default function DrillDownPage() {
<td className="py-2 text-xs font-mono">{cfdi.regimenEmisor || '-'}</td>
<td className="py-2 text-xs font-mono">{cfdi.regimenReceptor || '-'}</td>
<td className="py-2">
<Button variant="ghost" size="sm" onClick={() => setSelectedCfdi(cfdi)} title="Ver factura">
<Button
variant="ghost"
size="sm"
disabled={loadingCfdiId === cfdi.id}
onClick={async () => {
setLoadingCfdiId(cfdi.id);
try {
const fullCfdi = await getCfdiById(String(cfdi.id));
setSelectedCfdi(fullCfdi);
} catch (err) {
console.error('Error cargando CFDI completo:', err);
} finally {
setLoadingCfdiId(null);
}
}}
title="Ver factura"
>
<Eye className="h-4 w-4" />
</Button>
</td>

View File

@@ -554,12 +554,26 @@ export default function FacturacionPage() {
? clavesUnidad?.filter(u => !SERVICE_UNITS.includes(u.clave))
: clavesUnidad;
const prodSearchAbort = useRef<AbortController | null>(null);
const handleSearchProduct = async (q: string, idx: number) => {
setProdSearch(q);
setSearchingIdx(idx);
if (q.length < 2) { setProdResults([]); return; }
const results = await searchClaveProdServ(q);
setProdResults(results);
setProdResults([]);
if (q.length < 2) return;
prodSearchAbort.current?.abort();
prodSearchAbort.current = new AbortController();
try {
const results = await searchClaveProdServ(q, prodSearchAbort.current.signal);
setProdResults(results ?? []);
} catch (err: any) {
if (err.name !== 'AbortError' && err.code !== 'ERR_CANCELED') {
console.error('Error buscando clave SAT:', err);
}
setProdResults([]);
}
};
const selectProduct = (idx: number, clave: string, descripcion: string) => {
@@ -1418,6 +1432,7 @@ export default function FacturacionPage() {
onChange={e => handleSearchProduct(e.target.value, idx)}
onFocus={() => { setSearchingIdx(idx); setProdSearch(c.productKey); }}
placeholder="Buscar clave SAT..."
autoComplete="off"
required
/>
<Search className="absolute right-3 top-2.5 h-4 w-4 text-muted-foreground" />

View File

@@ -147,6 +147,7 @@ export default function PendientesPage() {
mensual: 'bg-blue-100 text-blue-700 dark:bg-blue-900 dark:text-blue-300',
bimestral: 'bg-purple-100 text-purple-700 dark:bg-purple-900 dark:text-purple-300',
trimestral: 'bg-orange-100 text-orange-700 dark:bg-orange-900 dark:text-orange-300',
cuatrimestral: 'bg-pink-100 text-pink-700 dark:bg-pink-900 dark:text-pink-300',
anual: 'bg-green-100 text-green-700 dark:bg-green-900 dark:text-green-300',
};
return f ? (

View File

@@ -123,20 +123,6 @@ export function TareasTab({ contribuyenteId }: { contribuyenteId: string | null
onSuccess: invalidate,
});
const completarMutation = useMutation({
mutationFn: async (periodoId: string) => apiClient.post(`/tareas/periodo/${periodoId}/completar`),
onSuccess: invalidate,
onError: (err: unknown) => {
const e = err as { response?: { data?: { message?: string } } };
alert(e.response?.data?.message || 'No se pudo marcar como completada');
},
});
const descompletarMutation = useMutation({
mutationFn: async (periodoId: string) => apiClient.delete(`/tareas/periodo/${periodoId}/completar`),
onSuccess: invalidate,
});
const handleEdit = (t: Tarea) => {
setEditingId(t.id);
setForm({
@@ -206,16 +192,11 @@ export function TareasTab({ contribuyenteId }: { contribuyenteId: string | null
return (
<Card key={t.id}>
<CardContent className="py-3 flex items-center gap-3">
<button
onClick={() => p && (p.completada ? descompletarMutation.mutate(p.id) : completarMutation.mutate(p.id))}
disabled={!p || completarMutation.isPending}
title={p?.completada ? 'Marcar pendiente' : 'Marcar completada'}
className="flex-shrink-0"
>
<div className="flex-shrink-0" title={p?.completada ? 'Completada' : atrasada ? 'Atrasada' : 'Pendiente'}>
{p?.completada
? <CheckCircle2 className="h-5 w-5 text-success" />
: <Circle className={`h-5 w-5 ${atrasada ? 'text-destructive' : 'text-muted-foreground'}`} />}
</button>
</div>
<div className="flex-1 min-w-0">
<div className="flex items-center gap-2 flex-wrap">
<span className={`text-sm font-medium ${p?.completada ? 'line-through text-muted-foreground' : ''}`}>

View File

@@ -20,5 +20,6 @@ export const getMetodosPago = () => apiClient.get<CatalogoItem[]>('/catalogos/me
export const getUsosCfdi = () => apiClient.get<UsoCfdiItem[]>('/catalogos/uso-cfdi').then(r => r.data);
export const getMonedas = () => apiClient.get<MonedaItem[]>('/catalogos/moneda').then(r => r.data);
export const getClavesUnidad = () => apiClient.get<CatalogoItem[]>('/catalogos/clave-unidad').then(r => r.data);
export const searchClaveProdServ = (q: string) => apiClient.get<CatalogoItem[]>(`/catalogos/clave-prod-serv?q=${encodeURIComponent(q)}`).then(r => r.data);
export const searchClaveProdServ = (q: string, signal?: AbortSignal) =>
apiClient.get<CatalogoItem[]>(`/catalogos/clave-prod-serv?q=${encodeURIComponent(q)}`, { signal }).then(r => r.data);
export const getObjetosImp = () => apiClient.get<CatalogoItem[]>('/catalogos/objeto-imp').then(r => r.data);

View File

@@ -28,7 +28,10 @@ export interface CreateDeclaracionData {
mes: number;
tipo: 'normal' | 'complementaria';
periodicidad?: Periodicidad;
impuestos: Impuesto[];
/** Legacy: se infiere en backend si se envían obligacionesIds. */
impuestos?: Impuesto[];
/** Obligaciones fiscales que cubre esta declaración. */
obligacionesIds?: string[];
montoPago?: number;
pdfBase64: string;
pdfFilename: string;

View File

@@ -0,0 +1,47 @@
import { apiClient } from './client';
export interface DeclaracionLink {
id: number;
año: number;
mes: number;
tipo: 'normal' | 'complementaria';
pdfFilename: string | null;
}
export interface ObligacionPeriodo {
id: string;
nombre: string;
frecuencia: string | null;
fechaLimite: string | null;
categoria: string | null;
activa: boolean;
esRecomendada: boolean;
completada: boolean;
completadaAt: string | null;
completadaPor: string | null;
periodoCompletado: string | null;
periodStatus: 'pendiente' | 'completada' | 'atrasada';
periodoAplica: string;
declaracion: DeclaracionLink | null;
declaracionPresentada: boolean;
pagoPresentado: boolean;
requierePago: boolean;
}
export interface ObligacionesPorPeriodoResponse {
data: ObligacionPeriodo[];
periodo: string;
}
export function getObligacionesPorPeriodo(
contribuyenteId: string,
periodo: string,
atrasados = false,
): Promise<ObligacionesPorPeriodoResponse> {
const params = new URLSearchParams();
params.set('periodo', periodo);
params.set('atrasados', String(atrasados));
return apiClient
.get<ObligacionesPorPeriodoResponse>(`/contribuyentes/${contribuyenteId}/obligaciones/periodo?${params}`)
.then((r) => r.data);
}

View File

@@ -7,7 +7,8 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "next lint"
"lint": "next lint",
"typecheck": "tsc --noEmit"
},
"dependencies": {
"@horux/shared": "workspace:*",