[CONSEJO — Doc 07] Contrarian: El backend de PayMind tiene un requerimiento inusual: el motor de planilla debe ser reproducible — si corres el mismo cálculo el día 1 y el día 365, el resultado debe ser idéntico dado el mismo input. Las tablas de tasas regulatorias deben estar versionadas con fechas de vigencia, no sobreescritas. Si no, una auditoría de la CCSS que pida "recalcula la planilla del período X" no puede responderse. Executor: El archivo bancario es el output más crítico de la planilla — es el archivo que realmente transfiere el dinero a los empleados. Un error en el formato del archivo ACH/SINPE puede resultar en que 50 empleados no reciban su salario. El formato debe ser validado contra las especificaciones oficiales de cada banco antes de cada deploy, no solo antes del lanzamiento. Auditor Seg.: Los datos de planilla (salario de cada empleado) son los datos más sensibles que PayMind procesa. Un atacante que acceda a la planilla de una empresa puede saber exactamente cuánto gana cada persona. Row-level security + column-level encryption en salarios es obligatorio.
1. Arquitectura general
┌─────────────────────────────────────────────────────────────────┐
│ FRONTEND (Next.js) │
└────────────────────────────┬────────────────────────────────────┘
│ HTTPS + JWT httpOnly cookie
┌────────────────────────────▼────────────────────────────────────┐
│ FastAPI (API Gateway) │
│ /api/v1/paymind/* │
└──┬──────────────┬──────────────┬──────────────┬─────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
PayrollService HRService WorkforceService ReportService
(motor planilla)(empleados) (predictivo) (CCSS/IMSS/DIAN)
│ │ │ │
└──────────────┴──────────────┴──────────────┘
│
┌────────▼────────┐
│ PostgreSQL 16 │
│ (pm_* tables) │
└─────────────────┘
│
┌────────▼────────┐
│ Celery Workers │
│ Redis Streams │
└─────────────────┘
│
┌───────────────────┼──────────────────┐
▼ ▼ ▼
CCSS CR (web SAT MX (CFDI DIAN CO (FE
services CCSS) nómina IMSS) electrónica)
│
┌────▼────────────────────────────────┐
│ Redis Streams (eventos cross-módulo) │
│ ← StockMind OC_aprobada │
│ ← SalesMind pipeline_ganado │
│ ← BookMind forecast_actualizado │
└─────────────────────────────────────┘
2. API endpoints principales
2.1 Planilla
POST /api/v1/paymind/planilla/calcular-borrador # Calcula sin confirmar
GET /api/v1/paymind/planilla/borrador/{planilla_id} # Ver desglose línea por línea
POST /api/v1/paymind/planilla/confirmar # Confirma + genera archivo banco
GET /api/v1/paymind/planilla/{planilla_id}/archivo-banco # Descarga ACH/SINPE
GET /api/v1/paymind/planilla/{planilla_id}/recibos # Descarga todos los recibos PDF
GET /api/v1/paymind/planilla/historial # Planillas procesadas anteriores
POST /api/v1/paymind/planilla/calcular-borrador — request:
{
"tenant_id": "uuid",
"empresa_id": "uuid",
"periodo": {"inicio": "2032-08-01", "fin": "2032-08-15"},
"pais": "CR",
"novedades": [
{"empleado_id": "uuid", "tipo": "hora_extra", "horas": 8},
{"empleado_id": "uuid", "tipo": "ausencia", "dias": 2, "tipo_ausencia": "incapacidad"}
]
}
POST /api/v1/paymind/planilla/calcular-borrador — response:
{
"planilla_id": "pm-planilla-uuid",
"estado": "borrador",
"periodo": {"inicio": "2032-08-01", "fin": "2032-08-15"},
"resumen": {
"empleados_procesados": 23,
"total_salarios_brutos": 8234500.00,
"total_deducciones_empleados": 879082.00,
"total_cargas_patronales": 2195490.00,
"total_a_depositar": 7355418.00,
"moneda": "CRC"
},
"detalle_empleados": [
{
"empleado_id": "uuid",
"nombre": "Ana Ramírez",
"salario_bruto": 850000.00,
"deducciones": {
"ccss_empleado": 90745.00,
"isr": 32000.00,
"otros": 0.00
},
"salario_neto": 727255.00,
"cargas_patronales": {
"ccss_patronal": 226950.00,
"banco_popular": 8500.00
}
}
],
"tabla_regulatoria_version": "CR-2032-v1",
"generado_en": "2032-08-15T08:30:00Z"
}
2.2 Empleados
GET /api/v1/paymind/empleados # Lista con paginación
POST /api/v1/paymind/empleados # Crear nuevo empleado
GET /api/v1/paymind/empleados/{id} # Expediente completo
PATCH /api/v1/paymind/empleados/{id} # Actualizar datos
GET /api/v1/paymind/empleados/{id}/vacaciones # Vacaciones acumuladas
POST /api/v1/paymind/empleados/{id}/ausencia # Registrar ausencia
GET /api/v1/paymind/empleados/{id}/cesantia-proyectada # Cálculo de cesantía
2.3 Workforce planning
GET /api/v1/paymind/workforce/forecast # Forecast necesidades 90 días
GET /api/v1/paymind/workforce/costos # Proyección costo planilla 90 días
POST /api/v1/paymind/workforce/escenario # Simular contratación manual
GET /api/v1/paymind/workforce/senales # Señales de módulos hermanos
2.4 Reportes regulatorios
GET /api/v1/paymind/reportes/ccss/{mes} # Planilla CCSS CR (XML + PDF)
GET /api/v1/paymind/reportes/imss/{mes} # SUA MX
GET /api/v1/paymind/reportes/isr-laboral/{mes} # ISR retenido CR/MX/CO
POST /api/v1/paymind/reportes/ccss/{mes}/enviar # Envío directo a portal CCSS
3. Motor de planilla — servicio de cálculo
class PayrollService:
async def calcular_planilla(
self,
tenant_id: UUID,
empresa_id: UUID,
periodo: PeriodoPlanilla,
pais: str,
novedades: list[NovedadPlanilla]
) -> PlanillaBorrador:
# 1. Cargar empleados activos del período
empleados = await self.db.get_empleados_activos(empresa_id, periodo)
# 2. Cargar tabla regulatoria vigente para el período
tabla = await self.db.get_tabla_regulatoria(pais, periodo.fin)
# Tablas versionadas por fecha de vigencia — NUNCA sobreescritas
# 3. Calcular por empleado (determinístico)
detalle = []
for emp in empleados:
calculo = self.calcular_empleado(emp, tabla, novedades, periodo)
detalle.append(calculo)
# 4. Persistir como borrador (no confirmado)
planilla_id = await self.db.crear_borrador(tenant_id, empresa_id, periodo, detalle)
return PlanillaBorrador(planilla_id=planilla_id, detalle=detalle)
def calcular_empleado(
self,
empleado: Empleado,
tabla: TablaRegulatoriaVigente,
novedades: list[NovedadPlanilla],
periodo: PeriodoPlanilla
) -> CalculoEmpleado:
dias_trabajados = self._calcular_dias_trabajados(empleado, periodo, novedades)
salario_periodo = empleado.salario_mensual * (dias_trabajados / 30)
# Horas extra
horas_extra_novedad = next((n for n in novedades if n.empleado_id == empleado.id and n.tipo == 'hora_extra'), None)
valor_hora_extra = (empleado.salario_mensual / 30 / 8) * tabla.factor_hora_extra
total_horas_extra = (horas_extra_novedad.horas * valor_hora_extra) if horas_extra_novedad else 0
salario_bruto = salario_periodo + total_horas_extra
# Deducciones empleado
ccss_empleado = round(salario_bruto * tabla.tasa_ccss_empleado, 2)
isr = self._calcular_isr(salario_bruto, tabla.tramos_isr)
salario_neto = salario_bruto - ccss_empleado - isr
# Cargas patronales
ccss_patronal = round(salario_bruto * tabla.tasa_ccss_patronal, 2)
return CalculoEmpleado(
empleado_id=empleado.id,
salario_bruto=salario_bruto,
ccss_empleado=ccss_empleado,
isr=isr,
salario_neto=salario_neto,
ccss_patronal=ccss_patronal,
tabla_version=tabla.version # Auditoría: qué tabla se usó
)
4. Integración cross-módulo (workforce planning)
PayMind suscribe a eventos de los módulos hermanos para el motor de workforce:
# Consumer de Redis Streams — eventos de otros módulos
async def process_portfolio_event(event: dict):
match event['type']:
case 'sm:oc_aprobada':
# Orden de compra aprobada → más trabajo de bodega esperado
await workforce_service.actualizar_senal(
tenant_id=event['tenant_id'],
tipo='bodega',
delta=event['payload']['lineas_count'],
fecha_estimada=event['payload']['fecha_entrega']
)
case 'sl:pipeline_ganado':
# Negocio cerrado en SalesMind → más trabajo de ventas/despacho
await workforce_service.actualizar_senal(
tenant_id=event['tenant_id'],
tipo='ventas',
delta=event['payload']['monto_usd'],
fecha_estimada=event['payload']['fecha_cierre']
)
case 'bk:forecast_actualizado':
# Nuevo forecast de BookMind → actualizar presupuesto disponible
await workforce_service.actualizar_presupuesto_disponible(
tenant_id=event['tenant_id'],
saldo_p50_90d=event['payload']['saldo_p50_90d']
)
5. Integración BookMind GL (asientos automáticos)
Después de confirmar una planilla, PayMind genera automáticamente los asientos en BookMind:
async def generar_asientos_planilla(planilla: PlanillaConfirmada):
evento = {
'type': 'pm:planilla_confirmada',
'tenant_id': str(planilla.tenant_id),
'payload': {
'periodo': planilla.periodo.dict(),
'total_salarios_brutos': float(planilla.total_bruto),
'total_ccss_patronal': float(planilla.total_ccss_patronal),
'total_neto_depositar': float(planilla.total_neto),
'asientos': [
# Débito: Gasto de planilla (cuenta 5101)
{'cuenta': '5101', 'tipo': 'D', 'monto': float(planilla.total_bruto)},
# Débito: Cargas patronales (cuenta 5102)
{'cuenta': '5102', 'tipo': 'D', 'monto': float(planilla.total_ccss_patronal)},
# Crédito: Salarios por pagar (cuenta 2201)
{'cuenta': '2201', 'tipo': 'C', 'monto': float(planilla.total_neto)},
# Crédito: CCSS por pagar (cuenta 2301)
{'cuenta': '2301', 'tipo': 'C', 'monto': float(planilla.total_ccss_patronal + planilla.total_ccss_empleado)},
# Crédito: ISR retenido por pagar (cuenta 2302)
{'cuenta': '2302', 'tipo': 'C', 'monto': float(planilla.total_isr)},
]
}
}
await redis.xadd('bk:asientos_automaticos', evento)
6. Generación de archivos bancarios
class ArchivoBancoService:
def generar_sinpe_cr(self, planilla: PlanillaConfirmada) -> bytes:
# Formato SINPE Móvil / IBAN (BCCR especificación técnica)
lineas = [
f"01{planilla.empresa.iban_cr}{planilla.total_neto:015.2f}...", # Cabecera
]
for emp in planilla.detalle:
lineas.append(
f"02{emp.empleado.iban}{emp.salario_neto:015.2f}"
f"{emp.empleado.nombre:<40}"
)
lineas.append(f"09{len(planilla.detalle):010d}{planilla.total_neto:015.2f}") # Totalizador
return '\r\n'.join(lineas).encode('latin-1')
7. Workers Celery
| Worker | Tarea | Schedule |
|---|---|---|
workforce_worker |
Recalcular forecast de necesidades de headcount | Diario 6:00 AM |
regulatory_worker |
Verificar si hay nuevas tasas publicadas | Semanal + evento manual |
cesantia_worker |
Recalcular cesantías acumuladas de todos los empleados | Mensual (1er día) |
vacation_worker |
Actualizar días de vacaciones acumuladas | Quincenal |
gl_asientos_worker |
Enviar asientos de planilla confirmada a BookMind | Post-confirmación (evento) |
8. Testing
| Capa | Cobertura objetivo |
|---|---|
| Motor de planilla (cálculos regulatorios) | ≥99.5% — fixtures oficiales por país |
| Generación archivo bancario | ≥99% — validado contra especificaciones BCCR |
| Integración cross-módulo (Redis Streams) | ≥90% |
| API endpoints | ≥80% |
| Motor workforce predictivo | ≥85% |
Obligatorio antes de producción: test end-to-end con 3 empleados reales en sandbox (no datos de producción), verificando que el archivo banco generado es válido para el banco.
Ver también: Doc 06 (SRD Frontend — wizard de planilla) · Doc 08 (Modelo de Datos — schema pm_) · Doc 09 (Motor Core — algoritmos de cálculo) · Doc 10 (Seguridad — cifrado de salarios)*