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

@@ -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" />