Versión: 1.0 Última actualización: Diciembre 2024 Plataforma: MPAY (Mercandu Pay / n1co)
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).
- 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
┌─────────────────────────────────────────────────────────────────────────┐
│ 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) │
│ └──────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────────────┘
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
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.
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
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.
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: {...} }
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),
];# 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| Método | URL | Descripción |
|---|---|---|
POST |
https://equifax-icg-can-aws.okta.com/oauth2/aus17wy1rokxXPuw85d7/v1/token |
Obtener access token |
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 |
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 disponiblesflowchart 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
| # | 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 |
| Estado | Descripción |
|---|---|
APPROVED |
Crédito aprobado |
DENIED |
Crédito rechazado |
IN_PROCESS |
En proceso de evaluación |
EXPIRED |
Solicitud expirada |
REJECTED |
Rechazado manualmente |
| 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í |
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
];| 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 |
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 reglaclass 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,
]);
}
}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
);-- 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)| Notificación | Trigger |
|---|---|
CreditApprovedNotification |
Crédito aprobado |
CreditRejectNotification |
Crédito rechazado |
AppealNotification |
Apelación recibida |
| Notificación | Trigger |
|---|---|
AmountApproved |
Monto asignado |
AmountDenied |
Solicitud rechazada |
ErrorRunningEvaluation |
Error en evaluación |
// 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());
}
}
}# .env
MERCANDU_PAY_MOCK_EQUIFAX_API=trueTabla 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" |
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
}// Evento al aprobar
$this->klaviyoService->createEvent(
customer: $customer,
event: 'approved_amount',
properties: ['amount' => $amount],
customerProperties: [
'mercandu_pay_active' => true,
'available_amount_mercandu_pay' => $availableAmount,
]
);// Eventos de conversión
$this->mixPanel->sendEventApproved($product, $client);
$this->mixPanel->sendEventDenied($rejectionReason, $product, $client);
$this->mixPanel->sendEventNotFoundUser($product, $client);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
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()
);
}
}| 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 |
- Errores de autenticación OAuth
- Errores en cada paso de Equifax
- Notificaciones enviadas
- Decisiones de crédito
- Tiempo de respuesta de Equifax API
- Tasa de aprobación/rechazo
- Razones de rechazo más comunes
- Errores de conexión
| 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 |
- Solo afecta:
EquifaxService::getAccessToken() - No afecta: Endpoints de negocio (usan Bearer token igual)
- Verificar: Valor de
usuarioEvaluadorencreateRequest()
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
| Versión | Fecha | Cambios |
|---|---|---|
| 1.0 | Dic 2024 | Documento inicial |