# ARQUITECTURA — Recarga.do API v1 ## Decisión de fondo La API v1 **no envuelve** el código legado: se construye al lado, comparte la base de datos `recargas_2` y reimplementa el camino del dinero de forma correcta. El sistema existente (`/var/www/html/recargas/`) sigue operando sin ningún cambio. Se descartó reutilizar `models/Transaction.php` y `classes/LimitValidator.php` porque su defecto no es de implementación sino de diseño: debitan con un ciclo leer-calcular-escribir sin transacción (hallazgo C-02). Envolverlos habría heredado el problema. --- ## Stack | Decisión | Elección | Motivo | |---|---|---| | Lenguaje | PHP 8.4 | Es el stack del sistema y del servidor; no introduce operación nueva | | Framework | **Ninguno** | El proyecto no usa Composer. Un router y un middleware pipeline son ~300 líneas; un framework añadiría superficie sin resolver el problema real, que es transaccional | | Base de datos | MySQL 5.6 existente | Fuente de verdad. Sin CTE, sin JSON, índices ≤ 767 bytes | | Dependencias externas | **Cero** | Regla §57: cada dependencia debe justificarse | | Cache / colas | **No** | No hay evidencia de que hagan falta. Se añadirán cuando un dato lo demuestre | --- ## Estructura ``` /var/www/recarga.do/api/ ├── public/ ← ÚNICO directorio servido por Apache │ ├── index.php front controller │ └── .htaccess rewrite + cabeceras de seguridad ├── app/ │ ├── bootstrap.php autoload PSR-4 propio + entorno │ ├── Core/ Env, Config, Db, Logger, Router, Request, Response, App │ ├── Http/ │ │ ├── Controllers/ System, Balance, Catalog, Recharge, Transaction │ │ └── Middleware/ Authenticate, RateLimit, Idempotency │ ├── Services/ BalanceService, RechargeService, LimitService │ ├── Integrations/ │ │ └── MidasRed/ MidasRedClient, ProviderResult │ └── Exceptions/ ApiException ├── config/ app.php, database.php ├── routes/api.php ├── database/migrations/ ├── storage/logs/ └── tests/ ``` **El código vive fuera del DocumentRoot.** Solo `public/` es alcanzable por HTTP; el resto no es servible ni siquiera si `.htaccess` deja de aplicarse. Es la corrección estructural del hallazgo C-06, donde `config/config.php` con las credenciales de producción estaba dentro del webroot. Los secretos residen en `/etc/recarga.do/api.env` (modo `0640`, `root:www-data`). --- ## Flujo de una petición ``` Apache → public/index.php → App::boot() ├─ Request::capture() (request_id único) ├─ Router::match() └─ pipeline de middleware ├─ Authenticate → identidad desde el token ├─ RateLimit → contador atómico por token └─ Idempotency → solo en endpoints financieros └─ Controller → Service → Repository / Integration ``` El middleware es una cadena de clausuras: cada capa decide si continúa. La idempotencia va **después** de la autenticación porque la clave se indexa por `empresa_id` y no debe consumirse en peticiones sin credenciales. --- ## Separación de responsabilidades | Capa | Responsabilidad | Prohibido | |---|---|---| | Controller | Validar entrada, componer la respuesta | Tocar la BD o el proveedor | | Service | Reglas de negocio y límites transaccionales | Conocer HTTP | | Integration | Hablar con el proveedor y normalizar su respuesta | Conocer la BD o el saldo | | Core/Db | Acceso a datos y transacciones | Reglas de negocio | `MidasRedClient` no sabe qué es un balance. `BalanceService` no sabe que existe MidasRed. `RechargeService` los coordina y es el único que abre transacciones. --- ## Las cuatro decisiones que sostienen el diseño ### 1. La identidad viene del token, nunca del cuerpo `Authenticate` resuelve `empresa_id` desde `api_tokens`. Si el cuerpo incluye un `empresa_id` distinto, se responde 403 y se registra el intento. Corrige C-01. ### 2. El dinero se reserva antes de gastarse `BalanceService::reservar()` es un `UPDATE … WHERE saldo_disponible >= :monto`. La condición vive en el `WHERE`, no en PHP: si afecta 0 filas, no había saldo. Ninguna recarga se solicita al proveedor sin reserva previa. Corrige C-02 y C-05. ### 3. La red nunca ocurre dentro de una transacción Dos transacciones cortas (reservar / liquidar) con la llamada al proveedor en medio. Mantener una transacción abierta durante 30 s bloquearía la fila de balance del comercio entero. ### 4. Desconocido no es fallido Un timeout produce `PENDING` con el importe retenido, nunca `FAILED`. Marcar fallida una recarga que sí se entregó regala el producto. Corrige el flujo de recuperación del legado. --- ## Escalabilidad Monolito modular. Los límites entre `Services/`, `Integrations/` y `Http/` están trazados para poder extraer módulos sin reescribir: el candidato natural a separarse primero es `Integrations/`, cuando exista un segundo proveedor además de MidasRed. No se adoptaron microservicios: con un solo proveedor y una sola base de datos, añadirían latencia y modos de fallo distribuidos sin resolver ningún problema actual. **Estado horizontal:** la API no guarda estado en el proceso. Rate limiting e idempotencia viven en base de datos, de modo que N instancias detrás de un balanceador se comportan igual que una. La única excepción es el token del proveedor, cacheado en memoria por petición. --- ## Observabilidad - **Logs estructurados** en JSON, una línea por evento, en `storage/logs/api-YYYY-MM-DD.log`. `Logger::redact()` enmascara todo campo cuyo nombre sugiera credencial. - **`request_id`** único por petición, presente en el log, en la respuesta y en el header `X-Request-Id`. Es el hilo que conecta cliente, aplicación, base de datos y proveedor. - **Auditoría** en `api_audit_log`: quién, qué, cuándo, desde dónde, con qué resultado. --- ## Compatibilidad con el sistema existente | Aspecto | Efecto | |---|---| | Archivos de `/var/www/html/recargas/` | Ninguno. No se modificó nada | | Tablas existentes | Ninguna alterada. Solo lectura y escritura por las vías previstas | | Tablas nuevas | 3, todas aditivas: `api_idempotency_keys`, `api_rate_counters`, `api_audit_log` | | Portal, cron, webhooks | Sin cambios | | Tokens de cliente | Se reutiliza `api_tokens`; se aceptan secretos legados y con hash | Las transacciones creadas por la API v1 aparecen en `transacciones` con el mismo formato que las del portal, por lo que los reportes y conciliaciones existentes las ven sin cambios.