Update: nueva version Horux Despachos
This commit is contained in:
208
packages/shared/src/constants/despacho-plans.ts
Normal file
208
packages/shared/src/constants/despacho-plans.ts
Normal file
@@ -0,0 +1,208 @@
|
||||
export const DESPACHO_PLANS = {
|
||||
trial: {
|
||||
name: 'Trial',
|
||||
maxRfcs: 3,
|
||||
maxUsers: 1,
|
||||
maxCfdisPorContribuyente: 1_000_000,
|
||||
timbresIncluidosMes: 20,
|
||||
dbMode: 'MANAGED' as const,
|
||||
permiteServidorBackup: false,
|
||||
features: [
|
||||
'dashboard', 'cfdi_basic', 'iva_isr', 'reportes', 'alertas',
|
||||
'calendario', 'conciliacion', 'documentos', 'facturacion',
|
||||
'forecasting', 'xml_sat',
|
||||
],
|
||||
},
|
||||
mi_empresa: {
|
||||
name: 'Mi Empresa',
|
||||
maxRfcs: 1,
|
||||
maxUsers: 3,
|
||||
maxCfdisPorContribuyente: 1_000_000,
|
||||
timbresIncluidosMes: 50,
|
||||
dbMode: 'MANAGED' as const,
|
||||
permiteServidorBackup: false,
|
||||
features: [
|
||||
'dashboard', 'cfdi_basic', 'iva_isr', 'reportes', 'alertas',
|
||||
'calendario', 'conciliacion', 'documentos', 'facturacion',
|
||||
'forecasting', 'xml_sat',
|
||||
],
|
||||
},
|
||||
mi_empresa_plus: {
|
||||
name: 'Mi Empresa +',
|
||||
maxRfcs: 1,
|
||||
maxUsers: 3,
|
||||
maxCfdisPorContribuyente: 1_000_000,
|
||||
timbresIncluidosMes: 50,
|
||||
dbMode: 'MANAGED' as const,
|
||||
permiteServidorBackup: false,
|
||||
features: [
|
||||
'dashboard', 'cfdi_basic', 'iva_isr', 'reportes', 'alertas',
|
||||
'calendario', 'conciliacion', 'documentos', 'facturacion',
|
||||
'forecasting', 'xml_sat', 'api', 'ia_lolita',
|
||||
],
|
||||
},
|
||||
business_control: {
|
||||
name: 'Business Control',
|
||||
maxRfcs: 100,
|
||||
maxUsers: -1,
|
||||
maxCfdisPorContribuyente: 1_000_000,
|
||||
timbresIncluidosMes: 0,
|
||||
dbMode: 'BYO' as const,
|
||||
permiteServidorBackup: true,
|
||||
features: [
|
||||
'dashboard', 'cfdi_basic', 'iva_isr', 'reportes', 'alertas',
|
||||
'calendario', 'conciliacion', 'documentos', 'facturacion',
|
||||
'forecasting', 'xml_sat', 'api',
|
||||
],
|
||||
},
|
||||
// Custom — gratis, sin fecha fin, solo asignable por Admin Global.
|
||||
// Reusa el enum `custom` (legacy "precio variable" tenía 0 tenants).
|
||||
// Comportamiento idéntico a Mi Empresa (1 RFC, MANAGED, sin API ni Lolita)
|
||||
// pero NO se incluye en DESPACHO_PLAN_PRICES — no genera Subscription ni
|
||||
// cobro MP. Ningún cron lo expira (sin trialEndsAt, sin currentPeriodEnd).
|
||||
// Oculto del catálogo user-facing, visible solo en `/clientes` admin.
|
||||
custom: {
|
||||
name: 'Custom',
|
||||
maxRfcs: 1,
|
||||
maxUsers: 3,
|
||||
maxCfdisPorContribuyente: 1_000_000,
|
||||
timbresIncluidosMes: 50,
|
||||
dbMode: 'MANAGED' as const,
|
||||
permiteServidorBackup: false,
|
||||
features: [
|
||||
'dashboard', 'cfdi_basic', 'iva_isr', 'reportes', 'alertas',
|
||||
'calendario', 'conciliacion', 'documentos', 'facturacion',
|
||||
'forecasting', 'xml_sat',
|
||||
],
|
||||
},
|
||||
// Identificador interno: business_cloud (no se renombra el enum por
|
||||
// backward compat de suscripciones existentes). Nombre display: "Enterprise".
|
||||
business_cloud: {
|
||||
name: 'Enterprise',
|
||||
maxRfcs: 100,
|
||||
maxUsers: -1,
|
||||
maxCfdisPorContribuyente: 3_000_000,
|
||||
timbresIncluidosMes: 0,
|
||||
dbMode: 'BYO' as const,
|
||||
permiteServidorBackup: true,
|
||||
features: [
|
||||
'dashboard', 'cfdi_basic', 'iva_isr', 'reportes', 'alertas',
|
||||
'calendario', 'conciliacion', 'documentos', 'facturacion',
|
||||
'forecasting', 'xml_sat', 'api',
|
||||
],
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type DespachoPlan = keyof typeof DESPACHO_PLANS;
|
||||
|
||||
/**
|
||||
* Precios MXN (IVA incluido) para planes despacho pagables.
|
||||
*
|
||||
* - `monthly`: precio mensual. Solo aplica a planes con `permiteMonthly=true`
|
||||
* (Mi Empresa, Mi Empresa+). Para Business Control y Enterprise es null.
|
||||
* - `firstYear` / `renewal`: precios anuales. `firstYear` es lo que se cobra
|
||||
* al contratar; `renewal` lo que MP cobra automáticamente en renovaciones.
|
||||
* Cuando ambos son iguales no hay dualidad.
|
||||
*
|
||||
* Política de descuento (D1, 2026-04-26): Mi Empresa y Mi Empresa+ tienen
|
||||
* descuento del ~17% al pagar el año adelantado — el cobro anual es
|
||||
* equivalente a 10 meses (en lugar de 12).
|
||||
*
|
||||
* mi_empresa: $580/mes → $5,800/año (10 meses)
|
||||
* mi_empresa_plus: $900/mes → $9,000/año (10 meses)
|
||||
*/
|
||||
export const DESPACHO_PLAN_PRICES = {
|
||||
mi_empresa: {
|
||||
monthly: 580,
|
||||
firstYear: 5_800,
|
||||
renewal: 5_800,
|
||||
permiteMonthly: true,
|
||||
},
|
||||
mi_empresa_plus: {
|
||||
monthly: 900,
|
||||
firstYear: 9_000,
|
||||
renewal: 9_000,
|
||||
permiteMonthly: true,
|
||||
},
|
||||
business_control: {
|
||||
monthly: null,
|
||||
firstYear: 25_850,
|
||||
renewal: 25_850,
|
||||
permiteMonthly: false,
|
||||
},
|
||||
business_cloud: { // display: Enterprise
|
||||
monthly: null,
|
||||
firstYear: 43_000,
|
||||
renewal: 43_000,
|
||||
permiteMonthly: false,
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type DespachoPaidPlan = keyof typeof DESPACHO_PLAN_PRICES;
|
||||
export type DespachoPricePhase = 'firstYear' | 'renewal';
|
||||
export type DespachoFrequency = 'monthly' | 'annual';
|
||||
|
||||
/**
|
||||
* Resuelve el precio MXN para un (plan, frequency, phase). Para monthly
|
||||
* la fase se ignora (no hay dualidad mensual). Para annual aplica
|
||||
* firstYear o renewal según corresponda. Throws si el plan no permite
|
||||
* la frecuencia solicitada.
|
||||
*/
|
||||
export function getPrecioDespacho(
|
||||
plan: DespachoPaidPlan,
|
||||
frequency: DespachoFrequency,
|
||||
phase: DespachoPricePhase = 'renewal',
|
||||
): number {
|
||||
const cfg = DESPACHO_PLAN_PRICES[plan];
|
||||
if (frequency === 'monthly') {
|
||||
if (!cfg.permiteMonthly || cfg.monthly == null) {
|
||||
throw new Error(`El plan ${plan} no permite frecuencia mensual`);
|
||||
}
|
||||
return cfg.monthly;
|
||||
}
|
||||
return cfg[phase];
|
||||
}
|
||||
|
||||
/** True si el plan acepta frecuencia mensual (Mi Empresa y Mi Empresa+). */
|
||||
export function permiteFrecuenciaMensual(plan: string): boolean {
|
||||
if (plan in DESPACHO_PLAN_PRICES) {
|
||||
return DESPACHO_PLAN_PRICES[plan as DespachoPaidPlan].permiteMonthly;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Costo mensual MXN por contribuyente extra que excede `maxRfcs` del
|
||||
* plan. Solo aplica a business_control y business_cloud (Enterprise).
|
||||
* Mi Empresa tiene límite duro de 1 RFC; no permite extras.
|
||||
*/
|
||||
export const DESPACHO_OVERAGE_PRICE_MENSUAL = 45;
|
||||
|
||||
/** True si el plan cobra distinto en el primer año vs renovaciones (anual). */
|
||||
export function despachoPlanTieneDualidad(plan: DespachoPaidPlan): boolean {
|
||||
const p = DESPACHO_PLAN_PRICES[plan];
|
||||
return p.firstYear !== p.renewal;
|
||||
}
|
||||
|
||||
export function getDespachoPlanLimits(plan: DespachoPlan) {
|
||||
return DESPACHO_PLANS[plan];
|
||||
}
|
||||
|
||||
export function hasDespachoFeature(plan: DespachoPlan, feature: string): boolean {
|
||||
return (DESPACHO_PLANS[plan]?.features as readonly string[])?.includes(feature) ?? false;
|
||||
}
|
||||
|
||||
export function isDespachoTenant(tenantRfc: string | null | undefined): boolean {
|
||||
return typeof tenantRfc === 'string' && tenantRfc.toUpperCase().startsWith('DESPACHO_');
|
||||
}
|
||||
|
||||
/** True si el plan es uno pagable de despacho (excluye trial). */
|
||||
export function isDespachoPaidPlan(plan: string): plan is DespachoPaidPlan {
|
||||
return plan === 'business_control' || plan === 'business_cloud'
|
||||
|| plan === 'mi_empresa' || plan === 'mi_empresa_plus';
|
||||
}
|
||||
|
||||
/** Planes que permiten cobrar overage por contribuyente extra. */
|
||||
export function permiteOverage(plan: string): boolean {
|
||||
return plan === 'business_control' || plan === 'business_cloud';
|
||||
}
|
||||
46
packages/shared/src/constants/plans.ts
Normal file
46
packages/shared/src/constants/plans.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
export const PLANS = {
|
||||
starter: {
|
||||
name: 'Starter',
|
||||
cfdiLimit: 0,
|
||||
usersLimit: 1,
|
||||
features: ['dashboard', 'cfdi_basic', 'iva_isr'],
|
||||
},
|
||||
business: {
|
||||
name: 'Business',
|
||||
cfdiLimit: 50,
|
||||
usersLimit: 3,
|
||||
features: ['dashboard', 'cfdi_basic', 'iva_isr', 'reportes', 'alertas', 'calendario', 'conciliacion', 'forecasting', 'xml_sat', 'documentos'],
|
||||
},
|
||||
business_ia: {
|
||||
name: 'Business + IA',
|
||||
cfdiLimit: 50,
|
||||
usersLimit: 3,
|
||||
features: ['dashboard', 'cfdi_basic', 'iva_isr', 'reportes', 'alertas', 'calendario', 'conciliacion', 'forecasting', 'xml_sat', 'documentos', 'ia_lolita'],
|
||||
},
|
||||
custom: {
|
||||
name: 'Custom',
|
||||
cfdiLimit: 50,
|
||||
usersLimit: 3,
|
||||
features: ['dashboard', 'cfdi_basic', 'iva_isr', 'reportes', 'alertas', 'calendario', 'conciliacion', 'forecasting', 'xml_sat', 'documentos', 'ia_lolita'],
|
||||
},
|
||||
enterprise: {
|
||||
name: 'Enterprise',
|
||||
cfdiLimit: 100,
|
||||
usersLimit: -1,
|
||||
features: ['dashboard', 'cfdi_basic', 'iva_isr', 'reportes', 'alertas', 'calendario', 'conciliacion', 'forecasting', 'xml_sat', 'documentos', 'api', 'ia_lolita'],
|
||||
},
|
||||
} as const;
|
||||
|
||||
export type Plan = keyof typeof PLANS;
|
||||
|
||||
export function getPlanLimits(plan: Plan) {
|
||||
return PLANS[plan];
|
||||
}
|
||||
|
||||
export function hasFeature(plan: Plan, feature: string): boolean {
|
||||
// Defensive: un plan desconocido (típicamente un valor que no pertenece
|
||||
// al catálogo Horux 360 — p.ej. un plan despacho usado por error)
|
||||
// retorna false en vez de crashear con "Cannot read properties of undefined".
|
||||
// El caller debe usar el catálogo correcto según la vertical del tenant.
|
||||
return (PLANS[plan]?.features as readonly string[] | undefined)?.includes(feature) ?? false;
|
||||
}
|
||||
76
packages/shared/src/constants/roles.ts
Normal file
76
packages/shared/src/constants/roles.ts
Normal file
@@ -0,0 +1,76 @@
|
||||
import type { Role, PlatformRole } from '../types/auth';
|
||||
|
||||
export const GLOBAL_ADMIN_RFC = 'HTS240708LJA';
|
||||
|
||||
/** Roles que son superset (implican todos los demás platform roles). */
|
||||
const SUPERSET_ROLES: PlatformRole[] = ['platform_admin', 'platform_ti'];
|
||||
|
||||
/**
|
||||
* "Admin global" = staff interno de Horux 360 con rol `platform_admin` o `platform_ti`.
|
||||
*
|
||||
* Primero checa `platformRoles` del user (nueva fuente de verdad post-migración).
|
||||
* Si no hay platformRoles (JWT viejo, sesión pre-deploy), cae al check legacy
|
||||
* por RFC + rol owner del tenant HTS240708LJA.
|
||||
*
|
||||
* Código nuevo debería preferir `hasPlatformRoleFromArray(platformRoles, 'platform_admin')`.
|
||||
*/
|
||||
export function isGlobalAdminRfc(
|
||||
tenantRfc: string | undefined,
|
||||
role: string | undefined,
|
||||
platformRoles?: PlatformRole[] | null,
|
||||
): boolean {
|
||||
// Nueva fuente: platform_admin o platform_ti en user_platform_roles
|
||||
if (platformRoles && SUPERSET_ROLES.some(r => platformRoles.includes(r))) return true;
|
||||
// Fallback legacy: owner del tenant con RFC admin global
|
||||
return role === 'owner' && tenantRfc === GLOBAL_ADMIN_RFC;
|
||||
}
|
||||
|
||||
/** ¿El user tiene algún rol de plataforma (staff interno)? */
|
||||
export function isPlatformStaffFromRoles(platformRoles?: PlatformRole[] | null): boolean {
|
||||
return !!platformRoles && platformRoles.length > 0;
|
||||
}
|
||||
|
||||
/** ¿El user tiene el rol de plataforma indicado? admin y ti implican todos. */
|
||||
export function hasPlatformRoleFromArray(
|
||||
platformRoles: PlatformRole[] | undefined | null,
|
||||
role: PlatformRole,
|
||||
): boolean {
|
||||
if (!platformRoles) return false;
|
||||
if (SUPERSET_ROLES.some(r => platformRoles.includes(r))) return true;
|
||||
return platformRoles.includes(role);
|
||||
}
|
||||
|
||||
export const ROLES = {
|
||||
owner: {
|
||||
name: 'Dueño',
|
||||
permissions: ['read', 'write', 'delete', 'manage_users', 'manage_settings'],
|
||||
},
|
||||
cfo: {
|
||||
name: 'CFO',
|
||||
permissions: ['read', 'write', 'delete', 'manage_users', 'manage_settings'],
|
||||
},
|
||||
contador: {
|
||||
name: 'Contador',
|
||||
permissions: ['read', 'write', 'resolve_alertas'],
|
||||
},
|
||||
auxiliar: {
|
||||
name: 'Auxiliar',
|
||||
permissions: ['read', 'write', 'resolve_alertas'],
|
||||
},
|
||||
visor: {
|
||||
name: 'Visor',
|
||||
permissions: ['read'],
|
||||
},
|
||||
supervisor: {
|
||||
name: 'Supervisor',
|
||||
permissions: ['read', 'write', 'resolve_alertas', 'manage_carteras'],
|
||||
},
|
||||
cliente: {
|
||||
name: 'Cliente',
|
||||
permissions: ['read'],
|
||||
},
|
||||
} as const;
|
||||
|
||||
export function hasPermission(role: Role, permission: string): boolean {
|
||||
return ROLES[role].permissions.includes(permission as any);
|
||||
}
|
||||
Reference in New Issue
Block a user