# REGLAS DE NEGOCIO — Recarga.do API Reglas extraídas del código en producción (`/var/www/html/recargas/`) y del esquema real, con la corrección aplicada en la API v1 cuando el comportamiento existente pone dinero en riesgo. Cada regla indica su origen: **HECHO CONFIRMADO** (leído del código o de los datos), **INFERENCIA** (deducido del esquema) o **DECISIÓN** (criterio nuevo de la API v1). --- ## 1. IDENTIDAD Y AISLAMIENTO MULTI-TENANT | Regla | Origen | |---|---| | `empresa_id` es la unidad de aislamiento: saldo, catálogo, comisiones y transacciones cuelgan de él. | HECHO CONFIRMADO | | Un token de API pertenece a exactamente una empresa (`api_tokens.empresa_id`, FK con `ON DELETE CASCADE`). | HECHO CONFIRMADO | | **La identidad se deriva exclusivamente del token.** Si el cuerpo trae `empresa_id` y no coincide, se responde 403. | DECISIÓN (corrige C-01) | | Toda consulta de transacciones filtra por el `empresa_id` del token; una transacción ajena responde 404, no 403 (no se revela su existencia). | DECISIÓN | | La empresa debe tener `api_enabled = 1` y `estado ∈ (activa, vip)`. | HECHO CONFIRMADO | | `ambiente = 'development'` enruta a sandbox; `''` (8 registros con ENUM corrupto) se trata como `production`. | DECISIÓN | --- ## 2. MODELO DE SALDO El saldo vive en `empresa_balances` con tres componentes: | Campo | Significado | |---|---| | `saldo_actual` | Dinero que la empresa tiene, incluido lo retenido | | `saldo_disponible` | Dinero gastable ahora mismo | | `saldo_retenido` | Dinero comprometido por operaciones en vuelo | Invariante que la API v1 mantiene: ``` saldo_actual = saldo_disponible + saldo_retenido ``` > **Advertencia sobre el estado heredado:** el sistema legado nunca usó `saldo_retenido` > y movía `saldo_actual` y `saldo_disponible` en paralelo. Además `empresas.saldo_actual` > duplica el dato y **96 empresas ya divergen**. La API v1 opera únicamente sobre > `empresa_balances`. La conciliación de las 96 divergencias está pendiente y se > documenta en `MIGRATION.md`. ### Ciclo de vida del dinero en una recarga ``` RESERVAR CONFIRMAR LIBERAR disponible -= monto actual -= monto disponible += monto retenido += monto retenido -= monto retenido -= monto + asiento en movimientos_balance ``` **Regla dura:** ninguna recarga se solicita al proveedor sin que el importe esté ya reservado. Esto corrige C-05, donde el legado entregaba la recarga y solo después intentaba cobrar. --- ## 3. FLUJO DE UNA RECARGA (API v1) ``` 1. Autenticar token → empresa_id (nunca del cuerpo) 2. Rate limit por token 3. Idempotency-Key: INSERT 'in_progress' (unicidad garantizada por la BD) 4. Validar servicio activo y rango de monto ┌─ T1 · TRANSACCIÓN DE BASE DE DATOS ────────────────────────┐ │ 5. Validar límites diario / semanal / mensual │ │ 6. RESERVAR importe (UPDATE atómico condicional) │ │ 7. INSERT transacción estado 'procesando' │ └─ COMMIT ───────────────────────────────────────────────────┘ 8. Llamar al proveedor con xid derivado de la Idempotency-Key ┌─ T2 · TRANSACCIÓN DE BASE DE DATOS ────────────────────────┐ │ 9a. SUCCESS → confirmar + asiento contable + 'completada' │ │ 9b. FAILED → liberar reserva + 'fallida' │ │ 9c. UNKNOWN → mantener reserva + 'pendiente' │ └─ COMMIT ───────────────────────────────────────────────────┘ 10. Guardar la respuesta bajo la Idempotency-Key ``` La llamada de red **nunca** ocurre dentro de una transacción abierta: mantener una transacción abierta 30 s bloquearía la fila de balance para todo el comercio. ### Comparación con el flujo legado | Paso | Legado | API v1 | |---|---|---| | Identidad | del cuerpo del POST | del token validado | | Validación de saldo | lectura suelta, sin bloqueo | dentro de transacción | | Reserva | **no existe** | `UPDATE … WHERE saldo_disponible >= monto` | | Llamada al proveedor | hasta 180 s, TLS sin verificar | 30 s, TLS verificado | | Débito | después de entregar; si falla, solo un warning | reserva previa, confirmación posterior | | Asiento contable | ninguno (1 fila en 43,123 tx) | siempre, en la misma transacción | | Timeout del proveedor | marca `fallida` | marca `pendiente`, mantiene la reserva | | Reintento del cliente | ejecuta otra recarga real | devuelve la respuesta original | --- ## 4. IDEMPOTENCIA | Regla | Detalle | |---|---| | `Idempotency-Key` es **obligatoria** en todo endpoint que mueva dinero | 422 si falta | | Ámbito de la clave | `(empresa_id, idempotency_key)` — dos comercios pueden usar la misma cadena | | Vigencia | 24 h configurables (`IDEMPOTENCY_TTL_HOURS`) | | Misma clave + mismo cuerpo, ya completada | se devuelve la respuesta original con `Idempotent-Replay: true` | | Misma clave + mismo cuerpo, aún en curso | 409 `IDEMPOTENT_REQUEST_IN_PROGRESS` | | Misma clave + **otro** cuerpo | 422: la clave no puede reutilizarse | | Fallo inesperado del servidor | la clave se libera para permitir el reintento | | Caída del proceso a mitad de operación | la clave queda `in_progress` → 409. **Se prefiere bloquear a cobrar dos veces.** | El `xid` enviado al proveedor se deriva de la clave: `RD-` + primeros 24 hex de `sha256(empresa_id : idempotency_key)`. Así, un reintento llega a MidasRed con el mismo identificador y este puede deduplicarlo. --- ## 5. ESTADOS TRANSACCIONALES Se conserva el ENUM existente en base de datos y se expone un nombre estable al cliente: | BD (`transacciones.estado`) | API | Significado | |---|---|---| | `pendiente` | `PENDING` | Resultado desconocido. **El importe sigue retenido.** | | `procesando` | `PROCESSING` | En vuelo hacia el proveedor | | `completada` | `SUCCESS` | Entregada y cobrada | | `fallida` | `FAILED` | Rechazada; el importe fue devuelto | | `anulada` | `REVERSED` | Anulada tras haberse completado | **Regla crítica (§26 del encargo):** un timeout **nunca** produce `FAILED`. Un proveedor que no responde puede haber ejecutado la recarga; marcarla como fallida y devolver el dinero regala el producto. Se marca `PENDING` y se resuelve por consulta. ### Resolución de pendientes `RechargeService::reconciliar()` consulta `/api/topup/status` por `xid` y liquida: - proveedor confirma → confirmar reserva → `completada` - proveedor niega → liberar reserva → `fallida` - sigue sin saberse → permanece `pendiente` para el próximo barrido > **Pendiente de instalar:** el barrido periódico. Hay 367 transacciones históricas en > `pendiente` (RD$ 43,381.00) que nunca se resolvieron. Ver `MIGRATION.md`. --- ## 6. LÍMITES DE CONSUMO | Regla | Origen | |---|---| | Límites diario, semanal y mensual por empresa en `empresa_balances`. | HECHO CONFIRMADO | | Valor `0` significa **sin límite**. | INFERENCIA (consistente con los datos) | | Semana = desde el lunes (`WEEKDAY()`); mes = desde el día 1. | DECISIÓN | | El consumo cuenta estados `completada`, `procesando` y `pendiente`. | DECISIÓN | | Se validan dentro de la transacción que reserva, no antes por separado. | DECISIÓN (corrige la carrera) | | Defecto por empresa sin balance: 50,000 / 250,000 / 1,000,000. | HECHO CONFIRMADO | `pendiente` cuenta como consumido porque el importe está retenido: no hacerlo permitiría gastar dos veces el mismo dinero mientras se resuelve la operación. --- ## 7. VALIDACIÓN DE MONTOS | Regla | Origen | |---|---| | `servicios.monto_minimo` / `monto_maximo` acotan cada operación (0 = sin cota). | HECHO CONFIRMADO | | Máximo 2 decimales; se rechaza mayor precisión en vez de redondear en silencio. | DECISIÓN | | Monto estrictamente mayor que cero. | HECHO CONFIRMADO | | Moneda única: DOP. | INFERENCIA | --- ## 8. HORARIO DE OPERACIÓN `empresas.horario_operacion` guarda un JSON por día y existe `classes/HorarioValidator.php`, pero **la validación está comentada en producción desde 2025-12-16** para permitir venta 24/7 (`api/process-recharge.php:122-159`). **Decisión API v1:** no se reimplanta. Reactivarla cambiaría el comportamiento comercial vigente y es una decisión de negocio, no técnica. Queda registrada como `REQUIERE VALIDACIÓN` del área comercial. --- ## 9. COMISIONES Estructura descubierta: | Tabla | Filas | Contenido | |---|---|---| | `empresa_comisiones` | 2,759 | Comisión por empresa | | `empresa_producto_comision` | 2,174 | Comisión por empresa + producto (`comision_personalizada`, `tipo_comision`) | | `productos` | 37 | Comisión por nivel: distribuidor / subdistribuidor / punto de venta | | `esquemas_comision` | 3 | Esquemas base | | `comisiones_calculadas` | **0** | Vacía | | `historial_comisiones` | **0** | Vacía | | `plantillas_comisiones` | **0** | Vacía | `transacciones.comision_calculada` existe y por defecto vale `0.00`. > **NO CONFIRMADO:** dónde se calcula efectivamente la comisión. Las tablas destinadas a > almacenar el cálculo están vacías, y `movimientos_balance` solo tiene 122 asientos de > tipo `comision` frente a 43,123 transacciones. **La API v1 no calcula ni aplica > comisiones**: registra el importe bruto y deja `comision_calculada` intacta. Definir > esta regla exige validación con el área financiera antes de implementarla. --- ## 10. ANULACIONES Y REVERSOS | Regla | Origen | |---|---| | `servicios.acepta_anulacion` indica si el servicio admite reverso. | HECHO CONFIRMADO | | El reverso se solicita al proveedor por `xid` (`/api/topup/reverse`). | HECHO CONFIRMADO | | Solo puede anularse una transacción en estado `completada`. | INFERENCIA | | Al anular, el importe se acredita y se asienta como `reversion` en el libro mayor. | DECISIÓN | | Existen 467 transacciones anuladas históricas. | HECHO CONFIRMADO | > **Pendiente de implementar** en la API v1: el endpoint `POST /v1/transactions/{id}/reverse`. > Requiere confirmar con MidasRed la ventana temporal admitida para anular y el > comportamiento cuando el reverso queda en estado desconocido. --- ## 11. SANDBOX Empresas con `ambiente = 'development'` no llegan al proveedor real. El simulador selecciona el escenario según el último dígito del `identifier`, de forma determinista: | Termina en | Resultado simulado | |---|---| | `0` | `FAILED` / `INVALID_PHONE` | | `1` | `FAILED` / `TRANSACTION_REJECTED` | | `2` | `UNKNOWN` → `PENDING` (permite probar la retención de saldo) | | otro | `SUCCESS` | El sandbox **sí** mueve el saldo de la empresa de pruebas: así se validan reserva, confirmación y liberación con la mecánica real. --- ## 12. PRIORIDADES ANTE CONFLICTO Orden de precedencia aplicado en todo el diseño: 1. **No perder dinero** — ante la duda, retener el importe. 2. **No duplicar operaciones** — ante la duda, bloquear el reintento (409) antes que arriesgar un doble cobro. 3. **No romper producción** — el sistema existente sigue operando sin cambios. 4. Seguridad e integridad de datos. 5. Compatibilidad, simplicidad, rendimiento.