Skip to content

Instantly share code, notes, and snippets.

@jluisflo
Created December 18, 2025 05:53
Show Gist options
  • Select an option

  • Save jluisflo/5463b980405790b9bb9c609b64320798 to your computer and use it in GitHub Desktop.

Select an option

Save jluisflo/5463b980405790b9bb9c609b64320798 to your computer and use it in GitHub Desktop.
Arquitectura de Integración Equifax - Plataforma MPAY (Buy Now Pay Later)

Arquitectura de Integración Equifax - MPAY

Versión: 1.0 Última actualización: Diciembre 2024 Plataforma: MPAY (Mercandu Pay / n1co)


1. Resumen Ejecutivo

La integración con Equifax es el motor de evaluación crediticia de MPAY. Permite consultar el buró de crédito de El Salvador para evaluar solicitudes de crédito BNPL (Buy Now Pay Later).

Características Principales

  • Búsqueda de clientes en buró de crédito
  • Evaluación crediticia automatizada
  • Extracción de variables de decisión (score, edad, ingresos)
  • Asignación automática de montos de crédito
  • Gestión de consentimientos y documentos

2. Arquitectura General

┌─────────────────────────────────────────────────────────────────────────┐
│                           ARQUITECTURA MPAY + EQUIFAX                    │
├─────────────────────────────────────────────────────────────────────────┤
│                                                                          │
│   ┌──────────────┐     ┌──────────────────┐     ┌──────────────────┐   │
│   │  ONBOARDING  │     │   BACK-OFFICE    │     │   EQUIFAX API    │   │
│   │  (Next.js)   │────▶│   (Laravel)      │────▶│  (El Salvador)   │   │
│   │   :3000      │     │     :80          │     │                  │   │
│   └──────────────┘     └──────────────────┘     └──────────────────┘   │
│          │                     │                         │              │
│          │                     │                         │              │
│          │              ┌──────▼──────┐                  │              │
│          │              │  DATABASE   │                  │              │
│          └─────────────▶│   (MySQL)   │◀─────────────────┘              │
│                         └─────────────┘                                 │
│                                                                          │
│   ┌──────────────┐                                                      │
│   │  API LARAVEL │  ← NO se comunica con Equifax                        │
│   │    :8051     │    (solo consulta datos de equifax_requests)         │
│   └──────────────┘                                                      │
│                                                                          │
└─────────────────────────────────────────────────────────────────────────┘

Flujo de Datos

flowchart LR
    A[Onboarding Next.js] -->|HTTP API| B[Back-Office Laravel]
    B -->|Bearer Token| C[Equifax API]
    B -->|Read/Write| D[(MySQL)]

    E[API Laravel] -.->|Solo lectura| D

    style B fill:#4a90e2,color:#fff
    style C fill:#f5a623,color:#fff
Loading

3. Componentes del Sistema

3.1 Onboarding (Next.js)

Ubicación: mpay/onboarding/ Puerto: 3000 Responsabilidad: Interfaz de usuario para solicitud de crédito

onboarding/
├── src/
│   ├── app/
│   │   └── (steps)/           # Páginas del flujo
│   │       ├── welcome/
│   │       ├── login/
│   │       ├── otp/
│   │       ├── consent/
│   │       ├── general-data/
│   │       ├── work-data/
│   │       ├── documents/
│   │       ├── references/
│   │       └── result/
│   └── services/
│       └── api.ts             # Cliente HTTP al Back-Office

NO tiene comunicación directa con Equifax.


3.2 Back-Office (Laravel + Nova)

Ubicación: mpay/back-office/ Puerto: 80 Responsabilidad: TODA la integración con Equifax

back-office/
├── app/
│   ├── Services/
│   │   ├── ExternalServices/
│   │   │   └── EquifaxService.php        # 🔑 Cliente HTTP para Equifax
│   │   └── BurofaxRequest/
│   │       ├── EquifaxRequestService.php # 🔑 Orquestador del flujo
│   │       └── EquifaxResponseService.php
│   ├── Managers/
│   │   └── AmountDecisionManager.php     # Motor de reglas de decisión
│   ├── Http/Controllers/API/
│   │   ├── EquifaxRequest/
│   │   │   └── EquifaxRequestController.php
│   │   └── Customer/
│   │       └── CustomerStepEquifaxController.php
│   ├── Facades/
│   │   ├── ExternalServices/Equifax.php
│   │   └── BurofaxRequest/EquifaxRequest.php
│   ├── Models/
│   │   ├── EquifaxRequest.php
│   │   ├── DecisionRule.php
│   │   └── MockEquifaxValue.php
│   ├── Observers/
│   │   └── EquifaxRequestObserver.php    # Notificaciones automáticas
│   └── Enums/Equifax/
│       ├── EquifaxRequestStep.php        # 13 pasos del proceso
│       ├── EquifaxRequestDecision.php    # APPROVED, DENIED, etc.
│       └── EquifaxRequestRejectReason.php
├── config/
│   └── equifax.php                       # 🔑 Configuración de credenciales
└── routes/api/v1/
    └── equifax.php                       # Rutas del API

3.3 API Laravel

Ubicación: mpay/api-laravel/ Puerto: 8051 Responsabilidad: API principal para comercios (NO integra con Equifax)

Solo tiene:

  • Enums de Equifax (para estados)
  • Modelo EquifaxRequest (solo lectura)

NO tiene EquifaxService ni comunicación con Equifax.


4. Sistema de Autenticación con Equifax

4.1 Flujo OAuth Actual

sequenceDiagram
    participant BO as Back-Office
    participant OKTA as Okta (IdP)
    participant EFX as Equifax API

    BO->>OKTA: POST /oauth2/.../v1/token
    Note right of BO: grant_type: password<br/>username: {user}<br/>password: {pass}<br/>Authorization: Basic {public_key}

    OKTA-->>BO: { access_token: "eyJ..." }

    BO->>EFX: GET /efx-api-precalificacion-cam/searchPerson
    Note right of BO: Authorization: Bearer {access_token}

    EFX-->>BO: { data: {...} }
Loading

4.2 Configuración

Archivo: back-office/config/equifax.php

return [
    'credentials' => [
        'grant_type' => env('EQUIFAX_GRANT_TYPE', 'password'),
        'username' => env('EQUIFAX_USERNAME'),
        'password' => env('EQUIFAX_PASSWORD'),
        'public_key' => env('EQUIFAX_PUBLIC_KEY'),  // Base64(client_id:client_secret)
    ],
    'api_credit_request_url' => env('EQUIDFAX_API_CREDIT_REQUEST_URL'),
    'organization_name' => env('EQUIFAX_ORGANIZATION_NAME', 'SV_MERCANDU'),
    'time_expired_request' => env('EQUIFAX_TIME_EXPIRED_REQUEST', 24),
    'time_expired_request_with_appeal' => env('EQUIFAX_TIME_EXPIRED_REQUEST_WITH_APPEAL', 72),
];

4.3 Variables de Entorno

# Autenticación OAuth (Okta)
EQUIFAX_GRANT_TYPE=password
[email protected]
EQUIFAX_PASSWORD=contraseña_segura
EQUIFAX_PUBLIC_KEY=Y2xpZW50X2lkOmNsaWVudF9zZWNyZXQ=

# API
EQUIDFAX_API_CREDIT_REQUEST_URL=https://www.equifax.com.sv
EQUIFAX_ORGANIZATION_NAME=SV_MERCANDU

# Tiempos
EQUIFAX_TIME_EXPIRED_REQUEST=24
EQUIFAX_TIME_EXPIRED_REQUEST_WITH_APPEAL=72

# Mock (solo desarrollo)
MERCANDU_PAY_MOCK_EQUIFAX_API=true

5. Endpoints de Equifax API

5.1 Autenticación

Método URL Descripción
POST https://equifax-icg-can-aws.okta.com/oauth2/aus17wy1rokxXPuw85d7/v1/token Obtener access token

5.2 Servicios de Negocio

Base URL: https://www.equifax.com.sv

Método Endpoint Descripción
GET /efx-api-precalificacion-cam/searchPerson Buscar persona por DUI
GET /efx-api-precalificacion-cam/obtenerPrecalificacion Obtener precalificación
POST /efx-api-precalificacion-cam/createRequest/{personId} Crear solicitud
POST /efx-api-consent-anywhere-pwa/consent/person/save Registrar consentimiento
PUT /efx-api-precalificacion-cam/saveRequest Guardar datos personales
POST /efx-api-precalificacion-cam/document Subir documentos
POST /efx-api-precalificacion-cam/evaluation?gstSol={id} Ejecutar evaluación
GET /efx-api-precalificacion-cam/precalificacion/consentPreview/{personId} Descargar PDF consentimiento
GET /efx-api-precalificacion-cam/tiketEvaluation/{requestId} Descargar resumen ejecutivo

6. Endpoints Internos (Back-Office API)

Base URL: http://back-office/api/v1

// Autenticación requerida: auth:mercandu-pay

GET  /customer-equifax-step          // Obtener paso actual del cliente
POST /save-signature                 // Guardar firma digital
POST /save-personal-information      // Guardar datos personales
POST /save-work-information          // Guardar datos laborales
POST /save-documents                 // Subir DUI + selfie
POST /save-references                // Guardar referencias
POST /process-approval               // Ejecutar evaluación completa
POST /save-appeal                    // Subir documentos de apelación

GET  /amortization                   // Calcular tabla de amortización
GET  /periods-active                 // Períodos disponibles

7. Flujo de Evaluación Crediticia

7.1 Diagrama de Flujo Completo

flowchart TD
    A[Usuario inicia en Onboarding] --> B[Login + OTP]
    B --> C[Acepta consentimiento]
    C --> D[Ingresa datos personales]
    D --> E[Ingresa datos laborales]
    E --> F[Sube documentos DUI]
    F --> G[Ingresa referencias]
    G --> H[POST /process-approval]

    subgraph "Back-Office → Equifax"
        H --> I[search: Buscar en buró]
        I --> J{¿Existe?}
        J -->|No| K[DENIED: customer_not_found]
        K --> L[Habilitar apelación]

        J -->|Sí| M[createRequest]
        M --> N[createConsent]
        N --> O[savePersonalData]
        O --> P[saveDocuments]
        P --> Q[evaluation]
    end

    subgraph "Decisión Local"
        Q --> R[Extraer variables]
        R --> S{Validar reglas}
        S -->|Falla edad| T1[DENIED: minimum/maximum_age]
        S -->|Falla ingreso| T2[DENIED: minimum_salary]
        S -->|Falla score| T3[DENIED: minimum_score]
        S -->|Falla rating| T4[DENIED: invalid_bureau_rating]
        S -->|Pasa todo| U[AmountDecisionManager]
        U --> V[Asignar monto]
        V --> W[APPROVED]
    end

    W --> X[Notificar cliente]
    T1 & T2 & T3 & T4 --> Y[Notificar rechazo]
    L --> Y

    style W fill:#90EE90
    style K fill:#FFB6C1
    style T1 fill:#FFB6C1
    style T2 fill:#FFB6C1
    style T3 fill:#FFB6C1
    style T4 fill:#FFB6C1
Loading

7.2 Estados del Proceso (EquifaxRequestStep)

# Step Descripción
1 CREATED Solicitud creada
2 CONSENT Esperando firma
3 SAVE_PERSONAL_DATA Guardando datos personales
4 SAVE_WORK_DATA Guardando datos laborales
5 SAVE_DOCUMENTS Subiendo documentos
6 REFERENCES Guardando referencias
7 SEARCH Buscando en Equifax
8 CREATE_REQUEST Creando solicitud en Equifax
9 CREATE_CONSENT Registrando consentimiento
10 SAVE_PERSONAL_DATA_EQUIFAX Enviando datos a Equifax
11 SAVE_DOCUMENTS_EQUIFAX Enviando documentos a Equifax
12 EVALUATION Ejecutando evaluación
13 APPEAL Proceso de apelación

7.3 Decisiones Finales (EquifaxRequestDecision)

Estado Descripción
APPROVED Crédito aprobado
DENIED Crédito rechazado
IN_PROCESS En proceso de evaluación
EXPIRED Solicitud expirada
REJECTED Rechazado manualmente

7.4 Razones de Rechazo

Razón Descripción Permite Apelación
CUSTOMER_NOT_FOUND No existe en buró ✅ Sí
MINIMUM_AGE Menor de edad mínima ❌ No
MAXIMUM_AGE Mayor de edad máxima ❌ No
MINIMUM_SALARY Ingreso insuficiente ✅ Sí
MINIMUM_SCORE Score muy bajo ✅ Sí
INVALID_BANKING_BUREAU_RATING Calificación bancaria inválida ✅ Sí
MANUAL_REJECTION Rechazado manualmente ✅ Sí

8. Variables de Decisión

La evaluación de Equifax retorna estas variables que se usan para decisiones locales:

$decisionVariables = [
    'banking_bureau_rating' => 'A',      // Calificación bancaria (A-F)
    'age' => 35,                          // Edad del cliente
    'income' => 1500.00,                  // Ingreso mensual
    'score' => 650,                       // Score crediticio
];

Reglas de Validación (Configurables en Nova Settings)

Setting Descripción Ejemplo
minimum_age Edad mínima permitida 18
maximum_age Edad máxima permitida 65
minimum_salary Ingreso mínimo 300
minimum_score Score mínimo 500
invalid_banking_bureau_rating Ratings no permitidos "D,E,F"
burofax_request_months_of_waiting Meses entre solicitudes 6

9. Motor de Reglas de Decisión

9.1 DecisionRules

Las reglas se almacenan en la tabla decision_rules y usan Symfony ExpressionLanguage:

// Ejemplo de regla en BD:
// rule: "$score >= 700 && $income >= 1000"
// amount_id: 5 (asociado a monto de $500)

// Evaluación:
$expression = "$score >= 700 && $income >= 1000";
$evaluated = "650 >= 700 && 1500 >= 1000";  // false → siguiente regla

9.2 AmountDecisionManager

class AmountDecisionManager
{
    public function assignedAmount()
    {
        // 1. Obtener regla que aplica
        $decisionRule = $this->getDecisionRule();

        // 2. Asignar monto al cliente
        AmountCustomerService::addAmountToCustomer(
            customer: $this->equifaxRequest->customer,
            amount: $decisionRule->amount->value,
            // ...
        );

        // 3. Actualizar EquifaxRequest
        $this->equifaxRequest->update([
            'amount_assigned' => $decisionRule->amount->value,
            'final_decision' => EquifaxRequestDecision::APPROVED->value,
        ]);
    }
}

10. Modelo de Datos

10.1 Tabla Principal: equifax_requests

CREATE TABLE equifax_requests (
    id BIGINT PRIMARY KEY,
    uuid VARCHAR(36) UNIQUE,
    customer_id BIGINT,
    product_id BIGINT,
    oauth_client_id BIGINT,
    currency_id BIGINT,

    -- Estado del proceso
    step VARCHAR(50),                    -- EquifaxRequestStep
    final_decision VARCHAR(50),          -- EquifaxRequestDecision
    rejection_reason VARCHAR(50),        -- EquifaxRequestRejectReason

    -- Respuestas de Equifax
    search_response JSON,
    prequalification_response JSON,
    create_request_response JSON,
    consent_response JSON,
    save_personal_data_response JSON,
    evaluation JSON,
    decision_variables JSON,

    -- Datos del proceso
    work_information JSON,
    references JSON,
    amortization JSON,
    signature VARCHAR(255),
    consent VARCHAR(255),
    executive_summary VARCHAR(255),

    -- Monto asignado
    amount_assigned DECIMAL(10,2),
    amount_id BIGINT,
    decision_rule_id BIGINT,
    decision_rule_applied TEXT,

    -- Apelación
    appeal_needed BOOLEAN DEFAULT FALSE,
    appeal_completed BOOLEAN DEFAULT FALSE,

    -- Timestamps
    expired_at TIMESTAMP,
    finished_at TIMESTAMP,
    created_at TIMESTAMP,
    updated_at TIMESTAMP
);

10.2 Tablas Relacionadas

-- Reglas de decisión
decision_rules (id, rule, amount_id, is_active)

-- Montos disponibles
amounts (id, value, currency_id, product_id)

-- Valores mock para testing
mock_equifax_values (id, key, value)

-- Entidades de configuración Equifax
burofax_entities (id, slug, service, type, data)

11. Notificaciones

11.1 Al Cliente

Notificación Trigger
CreditApprovedNotification Crédito aprobado
CreditRejectNotification Crédito rechazado
AppealNotification Apelación recibida

11.2 Al Owner (Administrador)

Notificación Trigger
AmountApproved Monto asignado
AmountDenied Solicitud rechazada
ErrorRunningEvaluation Error en evaluación

11.3 Observer

// EquifaxRequestObserver.php
public function updated(EquifaxRequest $equifaxRequest)
{
    if ($equifaxRequest->isDirty('final_decision')) {
        if ($equifaxRequest->final_decision == 'approved') {
            $equifaxRequest->customer->notify(new CreditApprovedNotification());
            Notification::route('mail', config('mail.to.alert_address'))
                ->notify(new AmountApproved());
        }

        if ($equifaxRequest->final_decision == 'denied') {
            $equifaxRequest->customer->notify(new CreditRejectNotification());
            Notification::route('mail', config('mail.to.alert_address'))
                ->notify(new AmountDenied());
        }
    }
}

12. Modo Mock (Testing)

12.1 Activación

# .env
MERCANDU_PAY_MOCK_EQUIFAX_API=true

12.2 Valores Configurables

Tabla mock_equifax_values:

Key Descripción Ejemplo
not_found_user Simular usuario no encontrado "0" o "1"
banking_bureau_rating Rating simulado "A"
age Edad simulada "25"
income Ingreso simulado "1500"
score Score simulado "650"
no_connection_to_equifax Simular error de conexión "0" o "1"

12.3 Implementación

public function search(string $documentValue)
{
    if (config('mercandu-pay.mock_equifax_api')) {
        Http::fake([
            'https://www.equifax.com.sv/efx-api-precalificacion-cam/searchPerson*'
                => Http::response([
                    'exist' => true,
                    'found' => true,
                    'person' => ['gstPerId' => 143921],
                ], 200),
        ]);
    }

    // ... resto del código
}

13. Integración con Servicios Externos

13.1 Klaviyo (Marketing)

// Evento al aprobar
$this->klaviyoService->createEvent(
    customer: $customer,
    event: 'approved_amount',
    properties: ['amount' => $amount],
    customerProperties: [
        'mercandu_pay_active' => true,
        'available_amount_mercandu_pay' => $availableAmount,
    ]
);

13.2 Mixpanel (Analytics)

// Eventos de conversión
$this->mixPanel->sendEventApproved($product, $client);
$this->mixPanel->sendEventDenied($rejectionReason, $product, $client);
$this->mixPanel->sendEventNotFoundUser($product, $client);

14. Manejo de Errores

14.1 Excepciones Específicas

Exceptions/Burofax/
├── InitException.php
├── SearchException.php
├── CreateRequestException.php
├── ConsentException.php
├── SavePersonalInformationException.php
├── SaveDocumentsException.php
├── EvaluationException.php
├── AnalyzeEvaluationException.php
├── CantMakeBurofaxRequest.php
├── EquifaxRequestAlreadyProcessedException.php
├── EquifaxRequestNotFoundException.php
├── AppealNotAllowedException.php
└── RejectReasons/
    ├── CustomerNotFound.php
    ├── MinimumAge.php
    ├── MaximumAge.php
    ├── MinimumSalary.php
    ├── MinimumScore.php
    └── InvalidBankingBureauRating.php

14.2 Manejo en el Servicio

public function search(): void
{
    try {
        // ... lógica de búsqueda
    } catch (RejectReason $e) {
        throw $e;  // Re-lanzar para manejo específico
    } catch (\Exception $e) {
        report($e);
        throw new SearchException(
            'No se pudo realizar la búsqueda en burofax.',
            $e->getMessage()
        );
    }
}

15. Consideraciones de Seguridad

Aspecto Implementación
Credenciales Variables de entorno, nunca en código
Transmisión HTTPS obligatorio
Token storage En memoria por request
Documentos Almacenados en storage seguro (S3/GCS)
Logs Respuestas sensibles no se loguean completas

16. Monitoreo y Logs

16.1 Puntos de Logging

  • Errores de autenticación OAuth
  • Errores en cada paso de Equifax
  • Notificaciones enviadas
  • Decisiones de crédito

16.2 Métricas Recomendadas

  • Tiempo de respuesta de Equifax API
  • Tasa de aprobación/rechazo
  • Razones de rechazo más comunes
  • Errores de conexión

17. Migración Futura: Nuevo Sistema de Autenticación

17.1 Cambio Planeado

Aspecto Actual Nuevo
URL Auth Okta (equifax-icg-can-aws.okta.com) API LATAM (api.latam.equifax.com)
Grant Type password client_credentials
Credenciales username + password client_id + client_secret
Scope No requerido Requerido

17.2 Impacto

  • Solo afecta: EquifaxService::getAccessToken()
  • No afecta: Endpoints de negocio (usan Bearer token igual)
  • Verificar: Valor de usuarioEvaluador en createRequest()

18. Referencias

18.1 Archivos Clave

back-office/
├── config/equifax.php                              # Configuración
├── app/Services/ExternalServices/EquifaxService.php # Cliente HTTP
├── app/Services/BurofaxRequest/EquifaxRequestService.php # Orquestador
├── app/Managers/AmountDecisionManager.php          # Motor de decisión
├── app/Http/Controllers/API/EquifaxRequest/        # Controladores
├── app/Observers/EquifaxRequestObserver.php        # Notificaciones
└── routes/api/v1/equifax.php                       # Rutas

18.2 Documentación Externa


Changelog

Versión Fecha Cambios
1.0 Dic 2024 Documento inicial
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment