Nómina
Cómo se calcula el pago del personal y qué se congela al registrarlo.
La aplicación no transfiere dinero. Calcula, desglosa y deja constancia de que un pago se realizó fuera del sistema.
Cómo se calcula
1. Tarifa aplicable
tarifa = empleado.hourlyRateOverride ?? configuración.defaultHourlyRateUn hourlyRateOverride de cero es un valor legítimo, no una ausencia: el
operador ?? sólo cae al valor global cuando el campo es null.
2. Congelado
La tarifa se copia a ServiceAssignment.hourlyRateSnapshot al asignar el
empleado. A partir de ahí, ese servicio se paga siempre a esa tarifa.
María cobraba ₡5.000/h en agosto y pasa a ₡5.500/h en octubre. Los servicios de agosto se siguen calculando a ₡5.000/h, para siempre.
3. Tiempo trabajado
Suma de las sesiones de ese empleado en ese servicio:
minutos = Σ (endedAt − startedAt)Una sesión abierta cuenta hasta el instante actual, para que el panel muestre el tiempo en curso.
4. Redondeo
minutos_facturables = aplicarPolítica(minutos, política)| Política | Efecto |
|---|---|
EXACT (actual) | Sin cambios |
ROUND_15_MINUTES | Al cuarto de hora más cercano |
ROUND_30_MINUTES | A la media hora más cercana |
Es al múltiplo más cercano, no siempre hacia arriba: redondear siempre hacia arriba favorecería sistemáticamente a una de las partes, y esa decisión Royal Clean aún no la ha tomado (PENDING_BUSINESS_RULES.md #1).
5. Importe
importe = redondeoAlCéntimo(tarifa × minutos_facturables ÷ 60)Todo en enteros de céntimos. El redondeo ocurre una sola vez, al final.
Ejemplo
3 h 10 min a ₡5.000/h:
500 000 céntimos/h × 190 min ÷ 60 = 1 583 333,33
→ 1 583 333 céntimos
= ₡15.833,33Nómina de un período
El redondeo se aplica por servicio, no sobre el total del período. Así el desglose siempre suma exactamente el total mostrado, sin diferencias de un céntimo que nadie sabe explicar.
Ejemplo de desglose:
| Fecha | Propiedad | Tiempo | Tarifa | Subtotal |
|---|---|---|---|---|
| 01/09/2026 | Villa Azul | 3 h 10 m | ₡5.000/h | ₡15.833,33 |
| 05/09/2026 | Casa Tamarindo | 4 h 05 m | ₡5.000/h | ₡20.416,67 |
| 10/09/2026 | Condo Playa | 2 h 30 m | ₡5.000/h | ₡12.500 |
| Total | 9 h 45 m | ₡48.750 |
Registrar un pago
Al marcar un período como pagado, todo ocurre en una transacción:
flowchart TD
A[Marcar como pagado] --> B{¿Trabajo en curso<br/>en el período?}
B -->|Sí| C[Rechazar: el monto<br/>seguiría creciendo]
B -->|No| D[Calcular el desglose]
D --> E{¿Queda algo<br/>por pagar?}
E -->|No| F[Rechazar: ya está pagado]
E -->|Sí| G[Crear PayrollPayment]
G --> H[Congelar cada línea<br/>en PayrollPaymentItem]
H --> I[Escribir auditoría]Qué se congela
Cada PayrollPaymentItem guarda:
- El nombre de la propiedad como texto, no una referencia. El recibo sobrevive a que la propiedad se renombre o se archive.
- La fecha del servicio.
- Los minutos ya redondeados.
- La tarifa aplicada.
- El importe.
El PayrollPayment guarda además la política de redondeo vigente, para que
un recibo antiguo siempre pueda explicarse.
Por qué congelar
Si el recibo se recalculara al abrirlo, corregir unas horas de septiembre cambiaría retroactivamente un pago de septiembre que ya se hizo. El registro dejaría de ser un registro.
Con el desglose congelado, corregir horas más adelante no altera lo que dice
el recibo. El sistema además avisa en pantalla cuando esa corrección afecta a un
servicio ya liquidado, y lo anota en auditoría con la acción
PAYROLL_AFFECTED_AFTER_PAYMENT.
Hay un test que lo comprueba: se registra un pago, luego se duplican las horas y
se sube la tarifa, y se verifica que el recibo sigue diciendo lo mismo
(tests/integration/payroll.test.ts).
Qué no se paga
- Servicios cancelados. Si tenían tiempo trabajado, el servicio queda marcado
con
requiresAdminReviewpara que un administrador decida. - Trabajo en curso. Hay que cerrar la participación primero.
- Servicios ya incluidos en un pago liquidado.
Revertir un pago
Con motivo obligatorio. El registro se elimina —no se marca anulado— para que el trabajo vuelva a quedar disponible para futuras liquidaciones. La auditoría conserva el rastro completo: quién lo registró, por cuánto, qué servicios incluía y quién lo revirtió.
Multi-empleado
Cada empleado tiene sus propias sesiones y su propia tarifa congelada. Nunca se reparte una duración global del servicio entre varias personas.
| Empleado | Sesiones | Tarifa | Importe |
|---|---|---|---|
| María | 11:00 – 13:30 | ₡5.000/h | ₡12.500 |
| Carlos | 11:10 – 12:40 | ₡5.000/h | ₡7.500 |
| Costo laboral del servicio | ₡20.000 |
Lo que el personal ve
Su historial muestra fecha, propiedad, duración y estado de las tareas.
Nunca ve su tarifa, lo que gana, ni ningún importe. Esto se garantiza en la
capa de consultas: los select del espacio del empleado sencillamente no piden
esos campos.
Reglas pendientes
Horas extra, recargos por domingo, feriado o nocturnidad, y la política
definitiva de redondeo están sin confirmar. El cálculo está centralizado en
calculateServiceLaborCost(), que recibe un desglose por empleado y no un total
opaco, precisamente para poder segmentarlo más adelante sin reescribir a sus
llamadores.
Detalle en PENDING_BUSINESS_RULES.md.