'use client'; import { useState } from 'react'; import { Header } from '@/components/layouts/header'; import { Card, CardContent, CardHeader, CardTitle, Button } from '@horux/shared-ui'; import { useAuthStore } from '@/stores/auth-store'; import { isGlobalAdminRfc, DESPACHO_OVERAGE_PRICE_MENSUAL } from '@horux/shared'; import { Tags, ShieldAlert, Info, AlertTriangle, Check, Loader2, Pencil, X } from 'lucide-react'; import { useQuery, useMutation, useQueryClient } from '@tanstack/react-query'; import { apiClient } from '@/lib/api/client'; interface DespachoPlanLimits { plan: string; nombre: string; monthly: number | null; firstYear: number | null; renewal: number | null; permiteMonthly: boolean; maxRfcs: number; maxUsers: number; timbresIncluidosMes: number; dbMode: 'BYO' | 'MANAGED'; permiteServidorBackup: boolean; permiteSatIncremental: boolean; } const PLAN_ORDER = ['trial', 'mi_empresa', 'mi_empresa_plus', 'business_control', 'business_cloud', 'custom']; function fmtCurrency(n: number | null): string { if (n == null) return '—'; return `$${n.toLocaleString('es-MX')}`; } async function listDespachoCatalogo(): Promise { const res = await apiClient.get<{ data: DespachoPlanLimits[] }>('/planes/despacho'); return res.data.data; } async function updateDespachoCatalogo(plan: string, patch: Partial): Promise { const res = await apiClient.patch(`/planes/despacho/${plan}`, patch); return res.data; } export default function PreciosSuscripcionPage() { const { user } = useAuthStore(); const queryClient = useQueryClient(); const isGlobalAdmin = isGlobalAdminRfc(user?.tenantRfc, user?.role, user?.platformRoles); const [editingPlan, setEditingPlan] = useState(null); const [draft, setDraft] = useState>({}); const { data: plans = [], isLoading } = useQuery({ queryKey: ['despacho-catalogo'], queryFn: listDespachoCatalogo, enabled: isGlobalAdmin, }); const updateMutation = useMutation({ mutationFn: ({ plan, patch }: { plan: string; patch: Partial }) => updateDespachoCatalogo(plan, patch), onSuccess: () => { queryClient.invalidateQueries({ queryKey: ['despacho-catalogo'] }); setEditingPlan(null); setDraft({}); }, }); if (!isGlobalAdmin) { return ( <>

Acceso restringido

Solo admin global puede consultar el catálogo de precios.

); } const startEdit = (p: DespachoPlanLimits) => { setEditingPlan(p.plan); setDraft({ nombre: p.nombre, monthly: p.monthly, firstYear: p.firstYear, renewal: p.renewal, permiteMonthly: p.permiteMonthly, maxRfcs: p.maxRfcs, maxUsers: p.maxUsers, timbresIncluidosMes: p.timbresIncluidosMes, dbMode: p.dbMode, permiteServidorBackup: p.permiteServidorBackup, permiteSatIncremental: p.permiteSatIncremental, }); }; const cancelEdit = () => { setEditingPlan(null); setDraft({}); }; const saveEdit = (plan: string) => { updateMutation.mutate({ plan, patch: draft }); }; const sorted = [...plans].sort((a, b) => PLAN_ORDER.indexOf(a.plan) - PLAN_ORDER.indexOf(b.plan)); return ( <>
Los planes despacho se almacenan en la tabla despacho_plan_prices. Puedes editar precios y limits desde aquí — los cambios aplican a contrataciones nuevas y renovaciones; las suscripciones vigentes conservan su precio. Las features de cada plan siguen versionadas en código.
Planes despacho {isLoading ? (
Cargando catálogo…
) : (
{sorted.map((p) => { const editing = editingPlan === p.plan; return ( ); })}
Plan Mensual Anual 1° Renovación RFCs Usuarios Timbres DB Backup SAT Inc
{editing ? ( setDraft({ ...draft, nombre: e.target.value })} /> ) : ( <>{p.nombre} ({p.plan}) )} {editing ? ( setDraft({ ...draft, monthly: e.target.value === '' ? null : Number(e.target.value) })} /> ) : ( fmtCurrency(p.monthly) )} {editing ? ( setDraft({ ...draft, firstYear: e.target.value === '' ? null : Number(e.target.value) })} /> ) : ( fmtCurrency(p.firstYear) )} {editing ? ( setDraft({ ...draft, renewal: e.target.value === '' ? null : Number(e.target.value) })} /> ) : ( fmtCurrency(p.renewal) )} {editing ? ( setDraft({ ...draft, maxRfcs: Number(e.target.value) })} /> ) : ( p.maxRfcs )} {editing ? ( setDraft({ ...draft, maxUsers: Number(e.target.value) })} /> ) : ( p.maxUsers === -1 ? '∞' : p.maxUsers )} {editing ? ( setDraft({ ...draft, timbresIncluidosMes: Number(e.target.value) })} /> ) : ( p.timbresIncluidosMes )} {editing ? ( ) : ( {p.dbMode} )} {editing ? ( setDraft({ ...draft, permiteServidorBackup: e.target.checked })} /> ) : ( p.permiteServidorBackup ? : )} {editing ? ( setDraft({ ...draft, permiteSatIncremental: e.target.checked })} /> ) : ( p.permiteSatIncremental ? : )} {editing ? (
) : ( )}
)}
{updateMutation.isError && ( Error guardando: {String((updateMutation.error as any)?.response?.data?.message || updateMutation.error)} )}

Cobro adicional por RFC extra:{' '} ${DESPACHO_OVERAGE_PRICE_MENSUAL}/mes por cada contribuyente que exceda el límite. Solo aplica a Business Control y Enterprise; los planes Mi Empresa tienen límite duro de 1 RFC.

maxUsers = -1 significa ilimitado. trial y custom no tienen precio fijo (trial es gratis 30 días; custom se asigna con monto variable al provisionar).

SAT Inc habilita 3 syncs SAT extra al día (11:00, 15:00, 19:00) además del daily de las 03:00. Ventana de 8h por sync, deduplicado por UUID. Latencia típica de un CFDI ~1-2h en horario laboral vs ~24h con solo el daily.

); }