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:
@@ -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" />
|
||||
|
||||
Reference in New Issue
Block a user