MPAY (Mercandu Pay / n1co) es una plataforma Buy Now Pay Later (BNPL) completa para El Salvador y Honduras. Es un monorepo con 5 proyectos independientes que trabajan en conjunto mediante una arquitectura orientada a eventos.
| Aspecto | Detalle |
|---|---|
| Tipo | Plataforma BNPL (Compra Ahora, Paga Después) |
| Stack Principal | Laravel 10 (PHP 8.2), Next.js 14, Python 3.13 |
| Base de Datos | MySQL 8.0 + Redis |
| Países | El Salvador, Honduras |
| Arquitectura | Event-Driven + Clean Architecture |
┌─────────────────────────────────────────────────────────────────┐
│ ACTORES EXTERNOS │
└─────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ CLIENTE │ │ COMERCIO │ │ ADMINISTRADOR│ │ SISTEMAS │
│ (Usuario) │ │ (Merchant) │ │ (Admin) │ │ EXTERNOS │
└──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘
│ │ │ │
│ Solicita crédito │ Integra API │ Gestiona │
│ Realiza pagos │ Recibe webhooks │ operaciones │
│ │ │ │
▼ ▼ ▼ ▼
┌───────────────────────────────────────────────────────────────────────────────────────┐
│ │
│ PLATAFORMA MPAY │
│ │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ │
│ │ ONBOARDING │ │ API-LARAVEL │ │ BACK-OFFICE │ │DOCUMENTS-OCR│ │
│ │ (Next.js) │ │ (Laravel) │ │ (Nova) │ │ (Python) │ │
│ └─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘ │
│ │
└───────────────────────────────────────────────────────────────────────────────────────┘
│ │ │ │
▼ ▼ ▼ ▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ EQUIFAX │ │ TWILIO │ │ KEYCLOAK │ │GOOGLE GEMINI │
│ (Buro Créd.) │ │ (SMS) │ │ (SSO) │ │ (AI/OCR) │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘
| Actor | Rol | Interacción |
|---|---|---|
| Cliente (Usuario) | Solicitante de crédito BNPL | Solicita crédito, sube documentos, realiza pagos |
| Comercio (Merchant) | Tienda integrada | Crea órdenes vía API, recibe webhooks de estado |
| Administrador | Operador interno | Aprueba créditos, gestiona mora, ajusta órdenes |
| Equifax | Buró de crédito | Evalúa riesgo crediticio del cliente |
| Twilio/Tigo/ConceptoMóvil | SMS Gateway | Envía códigos OTP y recordatorios |
| Keycloak | Identity Provider | Autenticación SSO para administradores |
| Google Gemini AI | Servicio de IA | OCR inteligente de documentos DUI |
| H4B | Payment Gateway | Procesa pagos de clientes |
┌────────────────────────────────────────────────────────────────────────────────────────────────────┐
│ PLATAFORMA MPAY │
├────────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ FRONTEND LAYER │ │
│ │ │ │
│ │ ┌───────────────────────────────────┐ │ │
│ │ │ ONBOARDING │ Next.js 14 + React 18 + TypeScript │ │
│ │ │ Puerto: 3000 │ Flujo de solicitud de crédito (10 pasos) │ │
│ │ │ /onboarding/* │ Validación con Zod, UI con Radix + Tailwind │ │
│ │ └───────────────────────────────────┘ │ │
│ │ │ │ │
│ └──────────────────────┼──────────────────────────────────────────────────────────────────────┘ │
│ │ HTTP REST │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ BACKEND LAYER │ │
│ │ │ │
│ │ ┌───────────────────────────────────┐ ┌───────────────────────────────────┐ │ │
│ │ │ API-LARAVEL │ │ BACK-OFFICE │ │ │
│ │ │ Puerto: 8051 │ │ Puerto: 80 │ │ │
│ │ │ /api/v1/* │ │ /api/v1/equifax/* │ │ │
│ │ │ │ │ /nova/* │ │ │
│ │ │ • API principal para comercios │ │ • Panel administrativo Nova │ │ │
│ │ │ • OAuth2 (Laravel Passport) │ │ • Integración COMPLETA Equifax │ │ │
│ │ │ • Gestión de órdenes/pagos │ │ • Motor de reglas de decisión │ │ │
│ │ │ • Laravel Octane (Swoole) │ │ • Keycloak SSO │ │ │
│ │ │ • Laravel Horizon (Queues) │ │ • Inertia.js + Vue 3 │ │ │
│ │ └───────────────────────────────────┘ └───────────────────────────────────┘ │ │
│ │ │ │ │ │
│ │ └──────────────┬─────────────────────┘ │ │
│ │ │ │ │
│ └─────────────────────────────────────┼────────────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ MICROSERVICES │ │
│ │ │ │
│ │ ┌───────────────────────────────────┐ │ │
│ │ │ DOCUMENTS-OCR │ Python 3.13 + FastAPI │ │
│ │ │ Puerto: 8000 │ Google Gemini AI (gemini-2.5-flash) │ │
│ │ │ /extract/dui │ Extracción automática de datos DUI │ │
│ │ └───────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ┌─────────────────────────────────────┼────────────────────────────────────────────────────────┐ │
│ │ DATA LAYER │ │
│ │ ▼ │ │
│ │ ┌───────────────────────────────────┐ ┌───────────────────────────────────┐ │ │
│ │ │ MySQL 8.0 │ │ Redis │ │ │
│ │ │ Puerto: 3306 │ │ Puerto: 6379 │ │ │
│ │ │ │ │ │ │ │
│ │ │ • 50+ tablas │ │ • Cache │ │ │
│ │ │ • 88 migraciones │ │ • Queues (Horizon) │ │ │
│ │ │ • Esquema compartido │ │ • Sesiones │ │ │
│ │ │ (database-laravel) │ │ │ │ │
│ │ └───────────────────────────────────┘ └───────────────────────────────────┘ │ │
│ │ │ │
│ │ ┌───────────────────────────────────┐ │ │
│ │ │ Google Cloud Storage (GCS) │ Almacenamiento de documentos (DUI, selfies) │ │
│ │ └───────────────────────────────────┘ │ │
│ │ │ │
│ └──────────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
└────────────────────────────────────────────────────────────────────────────────────────────────────┘
| Container | Tecnología | Puerto | Responsabilidad |
|---|---|---|---|
| onboarding | Next.js 14 + React 18 | 3000 | App de solicitud de crédito (10 pasos) |
| api-laravel | Laravel 10 + Octane | 8051 | API principal para comercios (OAuth2) |
| back-office | Laravel 10 + Nova 4 | 80 | Panel admin + Integración Equifax |
| documents-ocr | Python 3.13 + FastAPI | 8000 | OCR de DUI con Gemini AI |
| database-laravel | Laravel Package | N/A | Migraciones compartidas (88 migrations) |
| MySQL | MySQL 8.0 | 3306 | Base de datos relacional (50+ tablas) |
| Redis | Redis Alpine | 6379 | Cache + Queues + Sessions |
| GCS | Google Cloud Storage | N/A | Almacenamiento de documentos |
┌─────────────────────────────────────┐
│ ONBOARDING │
│ (Next.js 14) │
│ Puerto: 3000 │
└──────────────┬──────────────────────┘
│
│ HTTP REST
│ /api/v1/equifax/*
▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ BACK-OFFICE │
│ (Laravel 10 + Nova 4) │
│ Puerto: 80 │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ Dependencias Internas: Dependencias Externas: │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ database-laravel │◄─────────────┤ Equifax API │ │
│ │ (paquete VCS GitLab) │ │ (OAuth2 + REST) │ │
│ └─────────────────────────┘ │ - Búsqueda persona │ │
│ │ - Pre-calificación │ │
│ ┌─────────────────────────┐ │ - Evaluación crediticia │ │
│ │ nova-components/ │ └─────────────────────────┘ │
│ │ (4 componentes custom) │ │
│ │ - ConditionalBuilder │ ┌─────────────────────────┐ │
│ │ - DecisionRuleField │ │ Keycloak │ │
│ │ - PermissionPicker │ │ (SSO para admins) │ │
│ │ - Picker │ └─────────────────────────┘ │
│ └─────────────────────────┘ │
│ ┌─────────────────────────┐ │
│ Servicios: │ Documents-OCR │ │
│ ├─ EquifaxService │ (POST /extract/dui) │ │
│ ├─ DocumentOCR └─────────────────────────┘ │
│ ├─ MercanduCoreService │
│ └─ MixpanelService ┌─────────────────────────┐ │
│ │ Mercandu Core API │ │
│ │ (plataforma principal) │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
│
│ Comparte esquema DB
▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ API-LARAVEL │
│ (Laravel 10 + Octane) │
│ Puerto: 8051 │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ Dependencias Internas: Dependencias Externas: │
│ ┌─────────────────────────┐ ┌─────────────────────────┐ │
│ │ database-laravel │ │ Twilio │ │
│ │ (paquete VCS GitLab) │ │ (SMS Global) │ │
│ └─────────────────────────┘ └─────────────────────────┘ │
│ │
│ APIs Expuestas: ┌─────────────────────────┐ │
│ ├─ POST /api/v1/customers │ Tigo │ │
│ ├─ POST /api/v1/orders │ (SMS SV/HN) │ │
│ ├─ POST /api/v1/payments └─────────────────────────┘ │
│ └─ GET /api/v1/products │
│ ┌─────────────────────────┐ │
│ Managers: │ Concepto Móvil │ │
│ ├─ AmortizationManager │ (SMS multi-país) │ │
│ ├─ OrderManager └─────────────────────────┘ │
│ ├─ PaymentManager │
│ └─ AmountDecisionManager ┌─────────────────────────┐ │
│ │ H4B │ │
│ Jobs (Horizon): │ (Payment Gateway) │ │
│ ├─ ProcessingPayments └─────────────────────────┘ │
│ ├─ CalculateInstallmentsInDue │
│ ├─ CalculateInstallmentsInArrears ┌─────────────────────────┐ │
│ └─ SendPaymentReminders │ AWS S3 / GCS │ │
│ │ (Almacenamiento) │ │
│ Webhooks Salientes: └─────────────────────────┘ │
│ ├─ order_state_changed │
│ └─ installment_state_changed ┌─────────────────────────┐ │
│ │ Keycloak │ │
│ │ (OAuth2) │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
│
│ Comparte esquema DB
▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ DATABASE-LARAVEL │
│ (Paquete Composer) │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ Contenido: │
│ ├─ 88 migraciones │
│ ├─ 50+ tablas │
│ ├─ 4 vistas SQL │
│ └─ ServiceProvider Laravel │
│ │
│ Dominios: │
│ ├─ Customer (customers, documents, personal_information) │
│ ├─ Order (orders, installments, repayments, adjustments) │
│ ├─ Payment (payments, processing_payments, capital_registers) │
│ ├─ Product (products, periods, amounts, levels) │
│ ├─ Credit (equifax_requests, decision_rules, loans) │
│ ├─ OAuth (oauth_clients, oauth_access_tokens) │
│ └─ Admin (users, roles, permissions, disbursements) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ DOCUMENTS-OCR │
│ (Python 3.13 + FastAPI) │
│ Puerto: 8000 │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ Endpoints: Dependencias Externas: │
│ ├─ GET /health ┌─────────────────────────┐ │
│ └─ POST /extract/dui │ Google Gemini AI │ │
│ │ (gemini-2.5-flash) │ │
│ Datos Extraídos del DUI: │ - Vision OCR │ │
│ ├─ number (número DUI) └─────────────────────────┘ │
│ ├─ surname (apellidos) │
│ ├─ given_names (nombres) ┌─────────────────────────┐ │
│ ├─ issuance_date │ Google Cloud Storage │ │
│ └─ expiration_date │ (lectura de imágenes) │ │
│ └─────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
| Servicio | Depende de | Es consumido por |
|---|---|---|
| onboarding | back-office API, Mixpanel | Usuarios finales |
| api-laravel | database-laravel, MySQL, Redis, Twilio, Tigo, H4B, Keycloak, GCS | Comercios (OAuth2) |
| back-office | database-laravel, MySQL, Redis, Equifax, documents-ocr, Keycloak, Mercandu Core | Administradores, onboarding |
| documents-ocr | Google Gemini AI, GCS | back-office |
| database-laravel | MySQL | api-laravel, back-office |
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ BOUNDED CONTEXTS │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ IDENTITY & │ │ CREDIT │ │ ORDER │ │ PAYMENT │ │
│ │ ONBOARDING │ │ EVALUATION │ │ MANAGEMENT │ │ PROCESSING │ │
│ │ │ │ │ │ │ │ │ │
│ │ • Customer │ │ • EquifaxRequest│ │ • Order │ │ • Payment │ │
│ │ • Document │ │ • DecisionRule │ │ • Installment │ │ • ProcessingPmt │ │
│ │ • Verification │ │ • AmountCustomer│ │ • OrderRepayment│ │ • CapitalRegister│ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ COLLECTION │ │ PRODUCT │ │ COMMERCE │ │ ADMINISTRATION │ │
│ │ & ARREARS │ │ CATALOG │ │ INTEGRATION │ │ │ │
│ │ │ │ │ │ │ │ │ │
│ │ • Jobs de mora │ │ • Product │ │ • Client(OAuth) │ │ • User │ │
│ │ • Recordatorios │ │ • Period │ │ • Webhooks │ │ • Disbursement │ │
│ │ • Notificaciones│ │ • Amount/Level │ │ • H4B Gateway │ │ • SourceCustomer│ │
│ └─────────────────┘ └─────────────────┘ └─────────────────┘ └─────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ FLUJO: CUSTOMER ONBOARDING │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ACTOR: Cliente SISTEMA: onboarding + back-office │
│ │
│ ┌──────────┐ ┌────────────────────────────────────────────────────────────────────────┐ │
│ │ COMANDO │───▶│ EVENTOS │ │
│ └──────────┘ └────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 📝 CreateCustomer │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ CustomerCreated │────▶│ GenerateOTP │ │ │
│ │ │ (Observer) │ │ (Job) │ │ │
│ │ └─────────────────┘ └────────┬────────┘ │ │
│ │ │ │ │
│ │ 📝 ValidatePhone ▼ │ │
│ │ │ ┌─────────────────────┐ │ │
│ │ ▼ │ OTPSent │ │ │
│ │ ┌─────────────────┐ │ (Twilio/Tigo/CM) │ │ │
│ │ │ PhoneValidation │◀─┴─────────────────────┘ │ │
│ │ │ Requested │ │ │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ 📝 VerifyPhone │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ PhoneVerified │ number_verified = true │ │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ 📝 UploadDocuments │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ DocumentUploaded│────▶│ ExtractDUIData │────▶│ DUIDataExtracted│ │ │
│ │ │ (to GCS) │ │ (documents-ocr) │ │ (Gemini AI) │ │ │
│ │ └─────────────────┘ └─────────────────┘ └────────┬────────┘ │ │
│ │ │ │ │
│ │ 📝 SavePersonalInfo │ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────────┐ │ │
│ │ │ PersonalInfo │ │ Auto-fill from OCR: │ │ │
│ │ │ Saved │◀────│ - number, surname, given_names │ │ │
│ │ └────────┬────────┘ │ - issuance_date, expiration_date│ │ │
│ │ │ └─────────────────────────────────┘ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ OnboardingReady │ Listo para evaluación Equifax │ │
│ │ │ ForEvaluation │ │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ PASOS DEL ONBOARDING (Next.js): │
│ 1. Welcome → 2. Login (DUI+tel) → 3. OTP → 4. Consent → 5. General Data │
│ 6. Work Data → 7. Documents → 8. References → 9. Evaluation → 10. Result │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ FLUJO: CREDIT EVALUATION (EQUIFAX) │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ACTOR: Sistema (back-office) EXTERNO: Equifax API │
│ │
│ Estados (EquifaxRequestStep): │
│ ┌────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ CREATED → SEARCH → CONSENT → SAVE_PERSONAL_DATA → SAVE_WORK_DATA → SAVE_DOCUMENTS │ │
│ │ → CREATE_REQUEST → CREATE_CONSENT → SAVE_PERSONAL_DATA_EQUIFAX │ │
│ │ → SAVE_DOCUMENTS_EQUIFAX → EVALUATION → [APPEAL] │ │
│ └────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 📝 SearchPersonInEquifax │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────────────────┐ │ │
│ │ │ PersonFound │────▶│ Equifax API: /searchPerson │ │ │
│ │ │ (step=SEARCH) │ │ Busca por DUI en buró de crédito │ │ │
│ │ └────────┬────────┘ └─────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ 📝 RegisterConsent │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────────────────┐ │ │
│ │ │ ConsentSaved │────▶│ Firma digital del cliente │ │ │
│ │ │ (step=CONSENT) │ │ Almacena en GCS + Equifax │ │ │
│ │ └────────┬────────┘ └─────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ 📝 SaveDataToEquifax │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────────────────┐ │ │
│ │ │ DataSentTo │────▶│ Envía a Equifax: │ │ │
│ │ │ Equifax │ │ • Datos personales │ │ │
│ │ │ (steps 7-10) │ │ • Datos laborales │ │ │
│ │ └────────┬────────┘ │ • Documentos (DUI, selfie) │ │ │
│ │ │ └─────────────────────────────────────────┘ │ │
│ │ 📝 RunEvaluation │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────────────────┐ │ │
│ │ │ EvaluationRun │────▶│ Equifax API: /evaluation │ │ │
│ │ │ (step=EVAL) │ │ Retorna: score, variables de decisión │ │ │
│ │ └────────┬────────┘ └─────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ 📝 ApplyDecisionRules │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────────────────┐ │ │
│ │ │ DecisionMade │────▶│ Motor de reglas (Symfony Expression) │ │ │
│ │ │ │ │ Evalúa: score, ingresos, deuda, etc. │ │ │
│ │ └────────┬────────┘ └─────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ├──────────────────────┬──────────────────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ CreditApproved │ │ CreditDenied │ │ CreditInProcess │ │ │
│ │ │ decision= │ │ decision= │ │ decision= │ │ │
│ │ │ APPROVED │ │ DENIED │ │ IN_PROCESS │ │ │
│ │ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │ │
│ │ │ │ │ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ AmountCustomer │ │ CreditReject │ │ ManualReview │ │ │
│ │ │ Created │ │ Notification │ │ Required │ │ │
│ │ │ (límite asign.) │ │ (Email) │ │ │ │ │
│ │ └─────────────────┘ └─────────────────┘ └─────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ DECISIONES POSIBLES: │
│ • APPROVED - Crédito aprobado, se asigna AmountCustomer │
│ • DENIED - Crédito rechazado │
│ • IN_PROCESS - Requiere revisión manual │
│ • EXPIRED - Solicitud expirada │
│ • REJECTED - Rechazado manualmente por admin │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ FLUJO: ORDER CREATION │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ACTOR: Comercio (via OAuth2) SISTEMA: api-laravel │
│ │
│ Estados de Order (OrderState): │
│ ┌────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ ACTIVE ←→ PROCESSING_PAYMENT → PAID │ │
│ │ ↓ │ │
│ │ DUE → IN_ARREARS │ │
│ │ ↓ │ │
│ │ CANCEL │ │
│ └────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 📝 PreviewOrder (opcional) │ │
│ │ │ POST /api/v1/orders/preview │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────────────────┐ │ │
│ │ │ AmortizationCalc│────▶│ AmortizationManager: │ │ │
│ │ │ ulated │ │ • Calcula cuotas (principal+interés+IVA)│ │ │
│ │ │ │ │ • Tipos fee: COMPOUND, AMOUNT, % │ │ │
│ │ └─────────────────┘ └─────────────────────────────────────────┘ │ │
│ │ │ │
│ │ 📝 CreateOrder │ │
│ │ │ POST /api/v1/orders │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────────────────┐ │ │
│ │ │ OrderCreating │────▶│ Observer: OrderObserver::creating │ │ │
│ │ │ │ │ • Asigna recurrence_number │ │ │
│ │ └────────┬────────┘ └─────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ OrderCreated │ Observer: OrderObserver::created │ │
│ │ │ status=ACTIVE │ │ │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ ├─────────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ CapitalRegister │ amount = -disbursement_amount │ Installments │ │ │
│ │ │ Created │ (registro de desembolso negativo) │ Generated │ │ │
│ │ │ (negativo) │ │ (N cuotas) │ │ │
│ │ └─────────────────┘ └────────┬────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ FirstInstallment│ │ │
│ │ │ Activated │ │ │
│ │ │ status=ACTIVE │ │ │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ 📤 WebhookDispatched │ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ order_created │ → Comercio webhook_url │ CreateOrder │ │ │
│ │ │ (webhook) │ Payload: customer_uuid, order_uuid, │ Notification │ │ │
│ │ │ │ order_status, period_uuid │ (Email cliente) │ │ │
│ │ └─────────────────┘ └─────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ESTRUCTURA DE INSTALLMENT: │
│ ┌────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ number | amount | principal | interest | vat_interest | debt | fee | status | expired_at│ │
│ │ 1 │ $50.00 │ $45.00 │ $4.50 │ $0.50 │ $0 │ $0 │ ACTIVE │ 2024-02-15│ │
│ │ 2 │ $50.00 │ $45.50 │ $4.00 │ $0.50 │ $0 │ $0 │ PENDING│ 2024-03-15│ │
│ │ 3 │ $50.00 │ $46.00 │ $3.50 │ $0.50 │ $0 │ $0 │ PENDING│ 2024-04-15│ │
│ └────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ FLUJO: PAYMENT PROCESSING │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ACTOR: Cliente / Gateway H4B SISTEMA: api-laravel │
│ │
│ Estados de Payment (PaymentState): │
│ ┌────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ PENDING → CONFIRMED → SUCCESS │ │
│ └────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ 📝 CreatePayment │ │
│ │ │ POST /customers/{uuid}/orders/{id}/pay │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ PaymentCreated │ status = PENDING │ │
│ │ │ │ Crea ProcessingPayment en cola │ │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ ⏰ Job: ProcessingPayments (cada minuto) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────────────────────────────┐ │ │
│ │ │ GatewayQueried │────▶│ Consulta estado en H4B/STRIPE │ │ │
│ │ │ │ │ Si confirmado → status = CONFIRMED │ │ │
│ │ └────────┬────────┘ └─────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ PaymentConfirmed│ status = SUCCESS │ │
│ │ │ │ Observer: PaymentObserver::updated │ │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ ├─────────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ CapitalRegister │ amount = +confirmed_amount │ AllocateDebtTo │ │ │
│ │ │ Created │ (registro de ingreso positivo) │ Installment │ │ │
│ │ │ (positivo) │ │ (Job async) │ │ │
│ │ └─────────────────┘ └────────┬────────┘ │ │
│ │ │ │ │
│ │ LÓGICA DE DISTRIBUCIÓN DE PAGO: │ │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │ │
│ │ │ Orden de aplicación: │ │ │ │
│ │ │ 1. Deuda acumulada (debt + vat_debt) │ │ │ │
│ │ │ 2. Cargos por mora (fee + vat_fee) │ │ │ │
│ │ │ 3. Intereses (interest + vat_interest) │ │ │ │
│ │ │ 4. Principal │ │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ OrderRepayment │ │ │
│ │ │ Created │ │ │
│ │ │ (por cada cuota)│ │ │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ Si cuota totalmente pagada: ┌─────────────────┐ │ │
│ │ │ InstallmentPaid │ │ │
│ │ │ status=PAID │ │ │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ Observer: InstallmentObserver::updated │ │ │
│ │ │ │ │ │
│ │ ├─────────────────────────────────────────────────────────┤ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ NextInstallment │ Activa siguiente cuota │ installment_ │ │ │
│ │ │ Activated │ (status PENDING → ACTIVE) │ state_changed │ │ │
│ │ └─────────────────┘ │ (webhook) │ │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ │ Si TODAS las cuotas pagadas: │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ OrderFullyPaid │────▶│ UpgradeLevel │────▶│ order_state_ │ │ │
│ │ │ status=PAID │ │ (Job async) │ │ changed │ │ │
│ │ │ │ │ Sube nivel │ │ (webhook) │ │ │
│ │ └────────┬────────┘ │ crediticio │ └─────────────────┘ │ │
│ │ │ └─────────────────┘ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ ApprovedPayment │ Notificación Email al cliente │ │
│ │ │ Notification │ │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ FLUJO: COLLECTION & ARREARS MANAGEMENT │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ACTOR: Sistema (Jobs programados) SISTEMA: api-laravel │
│ │
│ Estados de Installment (InstallmentState): │
│ ┌────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ PENDING → ACTIVE → DUE → IN_ARREARS │ │
│ │ ↘ ↘ │ │
│ │ PAID PAID (con mora) │ │
│ └────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ │ │
│ │ ⏰ RECORDATORIOS PRE-VENCIMIENTO │ │
│ │ │ │
│ │ Job: SendPaymentReminders (diario, 9 AM) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ PaymentReminder │ SMS + Email al cliente │ │
│ │ │ Sent │ Días antes: 5, 3, 1, 0 (día de vencimiento) │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ │ ═══════════════════════════════════════════════════════════════════════════════ │ │
│ │ │ │
│ │ ⏰ MARCADO DE CUOTAS VENCIDAS │ │
│ │ │ │
│ │ Job: CalculateInstallmentsInDue (diario, 2 AM) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ Criterio: expired_at <= yesterday() │ │
│ │ │ InstallmentDue │ status: ACTIVE → DUE │ │
│ │ │ │ │ │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ ├─────────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ Order.status │ status: ACTIVE → DUE │ NextInstallment │ │ │
│ │ │ = DUE │ │ Activated │ │ │
│ │ └─────────────────┘ └────────┬────────┘ │ │
│ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ installment_ │ Webhook al comercio │ InstallmentStatus│ │ │
│ │ │ state_changed │ │ ChangeNotif │ │ │
│ │ │ (webhook) │ │ (Email admin) │ │ │
│ │ └─────────────────┘ └─────────────────┘ │ │
│ │ │ │
│ │ ═══════════════════════════════════════════════════════════════════════════════ │ │
│ │ │ │
│ │ ⏰ PERÍODO DE GRACIA │ │
│ │ │ │
│ │ Job: SendGracePeriodReminder (diario, 10 AM) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ GracePeriod │ SMS + Email al cliente │ │
│ │ │ ReminderSent │ "Tienes X días para pagar sin mora adicional" │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ │ ═══════════════════════════════════════════════════════════════════════════════ │ │
│ │ │ │
│ │ ⏰ MARCADO DE CUOTAS EN MORA │ │
│ │ │ │
│ │ Job: CalculateInstallmentsInArrears (diario, 6 AM) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ Criterio: vencida > grace_period días │ │
│ │ │ InstallmentIn │ status: DUE → IN_ARREARS │ │
│ │ │ Arrears │ │ │
│ │ └────────┬────────┘ │ │
│ │ │ │ │
│ │ ├─────────────────────────────────────────────────────────┐ │ │
│ │ │ │ │ │
│ │ ▼ ▼ │ │
│ │ ┌─────────────────┐ ┌─────────────────┐ │ │
│ │ │ Order.status │ status: DUE → IN_ARREARS │ AllocateDebtTo │ │ │
│ │ │ = IN_ARREARS │ │ Installment │ │ │
│ │ └─────────────────┘ │ (batch job) │ │ │
│ │ └────────┬────────┘ │ │
│ │ ▼ │ │ │
│ │ ┌─────────────────┐ ▼ │ │
│ │ │ installment_ │ ┌─────────────────┐ │ │
│ │ │ state_changed │ │ DebtCalculated │ │ │
│ │ │ (webhook) │ │ fee + vat_fee │ │ │
│ │ └─────────────────┘ │ asignados │ │ │
│ │ └─────────────────┘ │ │
│ │ ═══════════════════════════════════════════════════════════════════════════════ │ │
│ │ │ │
│ │ ⏰ RECORDATORIOS DE MORA │ │
│ │ │ │
│ │ Job: SendLatePaymentReminder (diario, 11 AM) │ │
│ │ │ │ │
│ │ ▼ │ │
│ │ ┌─────────────────┐ │ │
│ │ │ LatePayment │ SMS + Email al cliente │ │
│ │ │ ReminderSent │ "Tu cuota está vencida. Monto total: $X" │ │
│ │ └─────────────────┘ │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
│ CRONOGRAMA DE JOBS: │
│ ┌────────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ 2:00 AM │ CalculateInstallmentsInDue │ Marca cuotas como DUE │ │
│ │ 6:00 AM │ CalculateInstallmentsInArrears │ Marca cuotas como IN_ARREARS │ │
│ │ 9:00 AM │ SendPaymentReminders │ Recordatorios pre-vencimiento │ │
│ │ 10:00 AM │ SendGracePeriodReminder │ Recordatorios período de gracia │ │
│ │ 11:00 AM │ SendLatePaymentReminder │ Recordatorios de mora │ │
│ │ * * * * │ ProcessingPayments │ Cada minuto - procesa pagos pendientes │ │
│ │ * * * * │ ProcessingOrderAdjustment │ Cada minuto - procesa ajustes │ │
│ └────────────────────────────────────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
| Evento | Productor | Consumidor(es) | Payload |
|---|---|---|---|
CustomerCreated |
CustomerObserver | - | customer_id, recurrence_number |
PhoneVerified |
CustomerService | - | customer_id, phone_number |
DocumentUploaded |
DocumentService | documents-ocr | document_id, gcs_uri |
DUIDataExtracted |
documents-ocr | back-office | number, surname, names, dates |
EquifaxEvaluationCompleted |
EquifaxService | DecisionRulesEngine | score, decision_variables |
CreditApproved |
DecisionRulesEngine | AmountCustomerService | customer_id, amount, level |
CreditDenied |
DecisionRulesEngine | NotificationService | customer_id, reason |
OrderCreated |
OrderObserver | CapitalRegisterService, NotificationService | order_id, amount |
InstallmentsGenerated |
AmortizationManager | - | order_id, installments[] |
PaymentReceived |
PaymentObserver | AllocateDebtJob | payment_id, amount |
InstallmentPaid |
InstallmentObserver | NextInstallmentActivator | installment_id |
OrderFullyPaid |
OrderObserver | UpgradeLevelJob | order_id, customer_id |
InstallmentBecameDue |
CalculateInstallmentsInDue | NotificationService | installment_id |
InstallmentBecameInArrears |
CalculateInstallmentsInArrears | DebtCalculator | installment_id |
| Webhook | Trigger | Payload |
|---|---|---|
order_created |
OrderObserver::created | { event, timestamp, customer_uuid, order_uuid, order_status, period_uuid } |
order_state_changed |
OrderObserver::updated | { event, timestamp, customer_uuid, order_uuid, order_status, period_uuid } |
installment_state_changed |
InstallmentObserver::updated | { event, timestamp, customer_uuid, order_uuid, installment: { status, amount, number, expired_at } } |
| Notificación | Canal | Trigger |
|---|---|---|
PhoneValidation |
SMS | Solicitud de verificación |
PaymentReminder |
SMS + Email | Job diario (5, 3, 1, 0 días antes) |
GracePeriodReminder |
SMS + Email | Job diario (durante gracia) |
LatePaymentReminder |
SMS + Email | Job diario (en mora) |
ApprovedPayment |
Pago exitoso | |
CreditApprovedNotification |
Crédito aprobado | |
CreditRejectNotification |
Crédito rechazado |
┌─────────────────────────────────────────────────────────────────────────────────────────────────┐
│ MAPA DE INTEGRACIONES EXTERNAS │
├─────────────────────────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ EQUIFAX │ │ KEYCLOAK │ │ TWILIO │ │ TIGO │ │
│ │ (Crédito) │ │ (SSO) │ │ (SMS) │ │ (SMS SV/HN) │ │
│ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ └──────┬───────┘ │
│ │ │ │ │ │
│ │ OAuth2 + REST │ OIDC │ REST API │ REST API (XML) │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌─────────────────────────────────────────────────────────────────────────────────────┐ │
│ │ PLATAFORMA MPAY │ │
│ │ │ │
│ │ back-office ◄── Equifax, Keycloak │ │
│ │ api-laravel ◄── Twilio, Tigo, Concepto Móvil, H4B, Keycloak │ │
│ │ documents-ocr ◄── Google Gemini AI, GCS │ │
│ │ │ │
│ └─────────────────────────────────────────────────────────────────────────────────────┘ │
│ │ │ │ │ │
│ ▼ ▼ ▼ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │CONCEPTO MÓVIL│ │ H4B │ │ GOOGLE GCS │ │GOOGLE GEMINI │ │
│ │ (SMS multi) │ │ (Gateway) │ │ (Storage) │ │ (AI/OCR) │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ TOTALES: │
│ • 3 servicios SMS (Twilio, Tigo, Concepto Móvil) │
│ • 1 buró de crédito (Equifax) │
│ • 1 identity provider (Keycloak) │
│ • 1 payment gateway (H4B) │
│ • 1 servicio IA (Google Gemini) │
│ • 2 cloud storage (GCS, AWS S3) │
│ • 2 analytics (Mixpanel, Klaviyo - deshabilitado) │
│ │
└─────────────────────────────────────────────────────────────────────────────────────────────────┘
- Separación clara de responsabilidades: Cada servicio tiene un propósito bien definido
- Event-driven: Observers y Jobs mantienen el sistema desacoplado
- Esquema compartido: database-laravel garantiza consistencia entre servicios
- Microservicio especializado: documents-ocr con Gemini AI para OCR inteligente
- Multi-canal de notificaciones: SMS + Email con fallbacks por país
- Equifax solo en back-office: Toda la integración crediticia está centralizada
- Paquete privado GitLab: database-laravel requiere acceso a GitLab
- Nova licenciado: back-office requiere licencia Laravel Nova ($199/año)
- Sin message broker externo: Redis cubre las necesidades actuales pero podría limitar escalabilidad futura
| Métrica | Valor |
|---|---|
| Servicios | 5 |
| Tablas MySQL | 50+ |
| Migraciones | 88 |
| Jobs programados | 7 |
| Notificaciones | 23 |
| Integraciones externas | 10+ |
| Webhooks salientes | 3 |
Documento generado el 2025-12-19 Análisis realizado con Claude Code (Opus 4.5)