Versión: 1.0 Fecha: Enero 2026 Propósito: Documentación técnica completa de la integración entre N1co Issuing y MercanduPay (MPAY) para funcionalidad Buy Now Pay Later (BNPL)
- Arquitectura General
- Componentes del Sistema
- Flujos de Comunicación
- API Endpoints
- Webhooks y Eventos
- WebView Integration
- Modelos de Datos
- Estados y Transiciones
- Implementar Nuevos Endpoints
- Referencias de Código
┌─────────────────────────────────────────────────────────────────────────┐
│ ARQUITECTURA N1CO ↔ MPAY (BNPL) │
└─────────────────────────────────────────────────────────────────────────┘
┌──────────────┐ ┌──────────────┐ ┌──────────────┐ ┌──────────┐
│ N1co App │─────▶│ N1co App │─────▶│ N1co Finance│─────▶│ MPAY │
│ (Flutter) │ │ Gateway │ │ Backend │ │ API │
│ Frontend │◀─────│ (GraphQL) │◀─────│ (REST API) │◀─────│ │
└──────────────┘ └──────────────┘ └──────────────┘ └──────────┘
│ │ │
│ │ │
└────────────WebView (MPAY KYC)──────────────┴─────────────────────┘
Embedded in App
| Patrón | Ubicación | Propósito |
|---|---|---|
| Clean Architecture | Finance Backend | Separación de concerns en capas |
| CQRS | Application Layer | Separar Commands (escritura) de Queries (lectura) |
| Facade Pattern | FinancingService |
Simplificar interfaz compleja de MPAY |
| Adapter Pattern | MpayHttpClient |
Adaptar API REST de MPAY al dominio N1co |
| Repository Pattern | Infrastructure | Abstracción de persistencia |
| Anti-Corruption Layer | FinancingService |
Proteger dominio N1co de cambios en MPAY |
| Event-Driven | MediatR | Domain Events para notificaciones |
┌─────────────────────────────────────────────────────────────┐
│ WebUI Layer (Controllers) │
│ - FinancingController.cs │
│ - Endpoints REST públicos y webhooks │
├─────────────────────────────────────────────────────────────┤
│ Application Layer (CQRS) │
│ - Commands: CreateFinancingCommand, PayWithCardCommand │
│ - Queries: GetPeriodsQuery, GetFinancingDetailQuery │
│ - Handlers: MediatR request handlers │
├─────────────────────────────────────────────────────────────┤
│ Domain Layer (Core) │
│ - Interfaces: IFinancingService, IRepositories │
│ - Entities: FinancingProfile, FinancingDetail │
│ - Domain Events: FinancingCreditApprovedEvent │
├─────────────────────────────────────────────────────────────┤
│ Infrastructure Layer │
│ - Services: FinancingService, MpayHttpClient │
│ - Repositories: EF Core implementations │
│ - External APIs: HTTP clients │
├─────────────────────────────────────────────────────────────┤
│ Database (PostgreSQL) │
│ - FinancingProfiles, FinancingDetails │
└─────────────────────────────────────────────────────────────┘
Ubicación: n1co-app/lib/app/ui/pages/bnpl/
| Componente | Responsabilidad |
|---|---|
BnplMainPage |
Pantalla principal de BNPL |
BnplCalculatorScreen |
Calculadora de cuotas |
BnplCheckoutScreen |
Checkout de financiamiento |
BnplDetailPage |
Detalle de financiamiento |
BnplWebviewPage |
WebView embebido para KYC de MPAY |
CreateFinancingCubit |
Estado de creación de financiamiento |
PayFinancingWithCardCubit |
Estado de pago con tarjeta |
Ubicación: n1co-app-gateway/
// GraphQL Queries
FinancingQueries
├─ getFinancingPeriods(amount: Decimal!)
├─ getFinancingDetail(financingId: String!)
└─ getCustomerFinancing(page: Int, perPage: Int)
// GraphQL Mutations
FinancingMutations
├─ createFinancing(input: CreateFinancingInput!)
├─ payFinancingWithCard(input: PayFinancingInput!)
└─ getOrCreateFinancingCustomer()Comunicación: HTTP REST hacia Finance Backend con Bearer Token
Ubicación: src/WebUI/Controllers/FinancingController.cs
Endpoints Públicos (Requieren JWT):
POST /api/financing/customers // Onboarding
POST /api/financing/create // Crear financiamiento
POST /api/financing/pay-with-card // Pagar con tarjeta
GET /api/financing/periods // Obtener períodos
POST /api/financing/credit-preview // Simular cuotas
GET /api/financing // Listar financiamientos
GET /api/financing/paginated // Listar paginado
GET /api/financing/detail // Detalle de financiamientoWebhooks (Anónimos o ApiKey):
POST /api/financing/callback/status-events // [AllowAnonymous]
POST /api/financing/callback/credit-approval // [AllowAnonymous]
POST /api/v1/payments/{ref}/confirm // [ApiKeyAuth]
POST /customers/{uuid}/orders/{uuid}/pay // [ApiKeyAuth] MirrorUbicación: src/Infrastructure/Services/MercanduPay/FinancingService.cs (~1200 líneas)
Métodos Principales:
Task<ApiResponse<CustomerResult>> GetOrCreateCustomerAsync(Account)
Task<FinancingCreationResponse> CreateFinancingAsync(FinancingCreationRequest)
Task<CustomerFinancingListResponse> GetCustomerFinancingAsync(Account, page, perPage)
Task<FinancingDetailResponse> GetFinancingDetailAsync(Guid financingId)
Task<InternalProductsDataDto> GetPeriodsAsync(Account, decimal amount)
Task<CreditInstallmentsPreviewDto> FetchCreditInstallmentsPreview(...)
Task<PayFinancingResponse> PayFinancingAsync(PayFinancingRequest)
Task<CustomerStatsDto> GetCustomerStatsAsync(Account)Responsabilidades:
- Orquestar llamadas a MPAY
- Aplicar reglas de negocio (ej: 80% del monto aprobado)
- Transformar DTOs MPAY ↔ Dominio N1co
- Gestionar estado en BD local
- Anti-Corruption Layer
Ubicación: src/Infrastructure/Services/MercanduPay/Client/MpayHttpClient.cs
Características:
- OAuth2 Client Credentials con caché de tokens
- Deserialización case-insensitive
- Logging exhaustivo
- Manejo robusto de errores
Métodos:
Task<string> Login() // OAuth2 token
Task<string> CreateCustomer(MercanduCustomerData)
Task<MercanduCustomerResponse?> GetCustomerByDui(string)
Task<MercanduProductsResponse?> GetCreditProductsAvailable(uuid, amount)
Task<MercanduPreviewOrderResponse?> GetPreviewCredit(...)
Task<MercanduOrderResponse?> CreateFinancing(FinancingCreationRequest)
Task<FinancingListApiResponse?> GetCustomerFinancing(uuid, productId, page, perPage)
Task<FinancingDetailApiResponse?> GetFinancingDetail(uuid, orderId, productId)
Task<CustomerStatsApiResponse?> GetCustomerStats(uuid)
Task<PayFinancingApiResponse?> PayFinancing(...)
Task<ConfirmPaymentResponse?> ConfirmPayment(ref, amount, txnNumber)Base URL: https://api.mercandu-dev.com (configurado en appsettings.json)
Autenticación: OAuth2 Client Credentials
- ClientId y ClientSecret en configuración
- Token cacheado por (expires_in - 60) segundos
| Mecanismo | Dirección | Tipo | Propósito |
|---|---|---|---|
| API REST | N1co → MPAY | Síncrono | CRUD de clientes, órdenes, pagos |
| Webhooks | MPAY → N1co | Asíncrono | Notificación de cambios de estado |
| WebView | User ↔ MPAY | Embebido | KYC y verificación de identidad |
┌─────────┐ ┌─────────┐ ┌──────────┐ ┌──────┐
│ User │ │ App │ │ Finance │ │ MPAY │
└────┬────┘ └────┬────┘ └────┬─────┘ └───┬──┘
│ │ │ │
│ Abre BNPL │ │ │
├──────────────▶│ │ │
│ │ GET /api/financing/customers │
│ ├──────────────▶│ │
│ │ │ GetOrCreateCustomer
│ │ ├───┐ │
│ │ │ │ │
│ │ │◀──┘ │
│ │ │ CreateCustomer │
│ │ ├──────────────▶│
│ │ │ │
│ │ │ customerUuid │
│ │ │◀──────────────┤
│ │ │ GetCreditProductsAvailable
│ │ ├──────────────▶│
│ │ │ │
│ │ │ Products/Error│
│ │ │◀──────────────┤
│ │ │ DetermineStep │
│ │ ├───┐ │
│ │ │ │ │
│ │ │◀──┘ │
│ │ │ Save Profile │
│ │ ├───┐ │
│ │ │ │ │
│ │ │◀──┘ │
│ │ Step + OnboardingUrl (if KYC) │
│ │◀──────────────┤ │
│ │ │ │
│ Show Status │ │ │
│◀──────────────┤ │ │
│ │ │ │
Si Step = "Kyc":
│ Open WebView │ │ │
├──────────────▶│ │ │
│ │ Navigate to OnboardingUrl │
│ ├──────────────────────────────▶│
│ │ │ │
│ Complete KYC in MPAY WebView │ │
│◀──────────────────────────────────────────────┤
│ │ │ │
│ │ │ Webhook: assigned_amount
│ │ │◀──────────────┤
│ │ │ Update Profile
│ │ ├───┐ │
│ │ │◀──┘ │
Determinación de Step:
// src/Infrastructure/Services/MercanduPay/FinancingService.cs:202-215
private static string DetermineStep(MercanduProductsResponse financingInfo)
{
var product = financingInfo?.Products?.FirstOrDefault();
// Si hay crédito aprobado
if (product?.ApprovedCreditAmount != null)
return FinancingProfileSteps.Home; // ✅
// Si NO hay crédito, analizar error
return financingInfo?.Error?.Code switch
{
"burofax_process" => FinancingProfileSteps.Kyc, // ⏳ Necesita KYC
"burofax_request_appeal" => FinancingProfileSteps.KycFailed, // ❌ Rechazado
_ => FinancingProfileSteps.KycFailed // ❌ Otros errores
};
}┌─────────┐ ┌─────────┐ ┌──────────┐ ┌──────┐
│ User │ │ App │ │ Finance │ │ MPAY │
└────┬────┘ └────┬────┘ └────┬─────┘ └───┬──┘
│ │ │ │
│ Select amount │ │ │
│ & period │ │ │
├──────────────▶│ │ │
│ │ │ │
│ Confirm │ │ │
├──────────────▶│ │ │
│ │ POST /api/financing/create │
│ ├──────────────▶│ │
│ │ │ CreateFinancingCommand
│ │ ├───┐ │
│ │ │ │ Validate │
│ │ │◀──┘ │
│ │ │ CreateFinancing
│ │ ├──────────────▶│
│ │ │ │
│ │ │ mpayOrderId │
│ │ │◀──────────────┤
│ │ │ Create CashIn │
│ │ ├───┐ │
│ │ │ │ (credit) │
│ │ │◀──┘ │
│ │ │ Save Detail │
│ │ ├───┐ │
│ │ │◀──┘ │
│ │ │ Assign Tags │
│ │ ├───┐ │
│ │ │◀──┘ │
│ │ FinancingId │ │
│ │◀──────────────┤ │
│ Success │ │ │
│◀──────────────┤ │ │
Pasos Detallados:
-
Validación:
- Verificar límites de cuenta
- Verificar perfil de financiamiento existe
- Validar que Step = "Home"
-
Crear Orden en MPAY:
var orderResponse = await mPayHttpClient.CreateFinancing(request); // request contiene: ProductId, Amount, PeriodId, CustomerId (UUID MPAY)
-
Acreditar Fondos (CashIn):
- Sistema crea transacción interna
- Acredita monto a la cuenta del usuario
- Usuario puede usar fondos inmediatamente
-
Persistir Localmente:
var financingDetail = new FinancingDetail { Id = Guid.NewGuid(), // Internal ID AccountId = request.AccountId, ExternalFinancingId = orderResponse.MpayOrderId, // MPAY UUID ExternalProductId = request.ProductId, Amount = request.Amount, Status = "ACTIVE", PaymentLink = "" };
-
Asignar Tags:
- Tag "FirstInstallmentTopUp" si es primer financiamiento
┌─────────┐ ┌─────────┐ ┌──────────┐ ┌──────────┐ ┌──────┐
│ User │ │ App │ │ Finance │ │ Payment │ │ MPAY │
│ │ │ │ │ │ │ Gateway │ │ │
└────┬────┘ └────┬────┘ └────┬─────┘ └────┬─────┘ └───┬──┘
│ │ │ │ │
│ Pay with card │ │ │ │
├──────────────▶│ │ │ │
│ │ POST /api/financing/pay-with-card │
│ ├──────────────▶│ │ │
│ │ │ PayFinancingWithCardCommand │
│ │ ├───┐ │ │
│ │ │ │ Validate │ │
│ │ │ │ - Identity│ │
│ │ │ │ - Amount │ │
│ │ │ │ - Card │ │
│ │ │ │ - Balance │ │
│ │ │◀──┘ │ │
│ │ │ ProcessIssuingPayment │
│ │ ├──────────────▶│ │
│ │ │ │ Charge card │
│ │ │ ├───┐ │
│ │ │ │◀──┘ │
│ │ │ ReferenceId │ │
│ │ │◀──────────────┤ │
│ │ │ │ │
│ │ │ PayFinancing(ref) │
│ │ ├──────────────────────────────▶│
│ │ │ │ │
│ │ │ │ Process │
│ │ │ │ ├───┐
│ │ │ │ │◀──┘
│ │ │ MPayRefNumber │
│ │ │◀──────────────────────────────┤
│ │ │ Save Transaction │
│ │ ├───┐ │ │
│ │ │◀──┘ (Completed) │
│ │ Success │ │ │
│ │◀──────────────┤ │ │
│ Updated │ │ │ │
│◀──────────────┤ │ │ │
Validaciones Críticas:
// src/Application/Financing/Commands/PayFinancingWithCardCommand.cs
1. Verificar identidad del usuario (Auth0)
2. Verificar propiedad del financiamiento (AccountId match)
3. Obtener estado actualizado de MPAY
4. Validar estado permitido: "UpToDate" o "Overdue"
5. Validar monto ≤ TotalPendingAmount
6. Obtener tarjeta digital (IsVirtual && IsCurrent)
7. Validar tarjeta no bloqueada
8. Validar saldo suficiente (Inswitch)- PRIMERO: Cargar tarjeta con Payment Gateway
- DESPUÉS: Notificar a MPAY
- Si MPAY falla: Estado crítico → Requiere intervención manual (tarjeta ya fue cargada)
TODO Identificado: Implementar lógica de reversión si MPAY falla después de cargar tarjeta.
┌──────┐ ┌──────────┐ ┌─────────────┐ ┌─────────┐
│ MPAY │ │ Finance │ │ Domain │ │ User │
│ │ │ Backend │ │ Events │ │ │
└───┬──┘ └────┬─────┘ └──────┬──────┘ └────┬────┘
│ │ │ │
│ Installment │ │ │
│ becomes DUE │ │ │
├─┐ │ │ │
│ │ │ │ │
│◀┘ │ │ │
│ │ │ │
│ POST /api/financing/callback/status-events │
├────────────▶│ │ │
│ │ FinancingCallbackCommand │
│ ├───┐ │ │
│ │ │ Deserialize │ │
│ │◀──┘ │ │
│ │ GetAccount(customerUuid) │
│ ├───┐ │ │
│ │◀──┘ │ │
│ │ DetermineTagAction│ │
│ ├───┐ │ │
│ │ │ DUE/IN_ARREARS│ │
│ │ │ → Assign Tag │ │
│ │◀──┘ │ │
│ │ AssignTag("OVERDUE_CUSTOMER") │
│ ├───────────────────┐ │
│ │ │ │
│ │◀──────────────────┘ │
│ 200 OK │ │ │
│◀────────────┤ │ │
│ │ │ │
│ │ (Tag afecta funcionalidad │
│ │ en otras partes del sistema) │
Estados que asignan tag:
DUE(Vencido)IN_ARREARS(Atraso)
Estados que remueven tag:
ACTIVE(Al día)PAID(Pagado)
┌──────┐ ┌──────────┐ ┌─────────────┐ ┌──────────┐ ┌─────────┐
│ MPAY │ │ Finance │ │ Domain │ │ Push │ │ User │
│ │ │ Backend │ │ Events │ │ Notif │ │ Device │
└───┬──┘ └────┬─────┘ └──────┬──────┘ └────┬─────┘ └────┬────┘
│ │ │ │ │
│ KYC Approved│ │ │ │
│ (Equifax) │ │ │ │
├─┐ │ │ │ │
│◀┘ │ │ │ │
│ │ │ │ │
│ POST /callback/status-events │ │
│ Event: assigned_amount_created │ │
│ Status: applied, Type: equifax │ │
├────────────▶│ │ │ │
│ │ HandleAssignedAmountCreated │ │
│ ├───┐ │ │ │
│ │◀──┘ │ │ │
│ │ GetAccount │ │ │
│ ├───┐ │ │ │
│ │◀──┘ │ │ │
│ │ ProcessFinancingApproval │ │
│ ├───┐ │ │ │
│ │ │ Update Profile.Step = "Home" │ │
│ │ │ AddDomainEvent(...) │ │
│ │◀──┘ │ │ │
│ │ SaveChanges() │ │ │
│ ├───────────────────▶│ │ │
│ │ │ Publish FinancingCreditApprovedEvent
│ │ ├────────────────▶│ │
│ │ │ │ Create Notification
│ │ │ ├───┐ │
│ │ │ │◀──┘ │
│ │ │ │ Send Push (FCM) │
│ │ │ ├────────────────▶│
│ │ │ │ │
│ 200 OK │ │ │ │
│◀────────────┤ │ │ │
│ │ │ │ Notification │
│ │ │ │ Displayed │
│ │ │ │ │◀┐
│ │ │ │ │ │
│ │ │ │ │ │
Cadena de Eventos:
assigned_amount_created/updated(webhook)FinancingCreditApprovedEvent(domain event)NotificationCreatedEvent(domain event)- Firebase Cloud Messaging → Device
Base URL: https://api.mercandu-dev.com
| Método | Endpoint | Propósito | Archivo |
|---|---|---|---|
POST |
/oauth/token |
Autenticación OAuth2 | MpayHttpClient.cs:84 |
POST |
/api/v1/customers |
Crear cliente | MpayHttpClient.cs:152 |
GET |
/api/v1/customers/by-document?value={dui}&type=dui |
Buscar por DUI | MpayHttpClient.cs:185 |
GET |
/api/v2/products?customer={uuid}&amount={amount} |
Productos disponibles | MpayHttpClient.cs:221 |
POST |
/api/v1/orders/preview |
Simular cuotas | MpayHttpClient.cs:263 |
POST |
/api/v1/orders |
Crear orden | MpayHttpClient.cs:330 |
GET |
/api/v1/customers/{uuid}/orders?product={id}&page={n}&per_page={n} |
Lista órdenes | MpayHttpClient.cs:360 |
GET |
/api/v1/customers/{uuid}/orders/{oid}?product={id} |
Detalle orden | MpayHttpClient.cs:399 |
GET |
/api/v1/customers/{uuid}/stats |
Estadísticas | MpayHttpClient.cs:437 |
POST |
/api/v1/customers/{uuid}/orders/{oid}/pay |
Pagar cuota | MpayHttpClient.cs:475 |
POST |
/api/v1/payments/{ref}/confirm |
Confirmar pago | MpayHttpClient.cs:509 |
Autenticación:
Authorization: Bearer {token_from_oauth}Ejemplo de Request - CreateCustomer:
POST /api/v1/customers
Content-Type: application/json
Authorization: Bearer eyJhbGc...
{
"name": "Juan",
"last_name": "Perez",
"email": "juan.perez@example.com",
"phone_number": "+50377998832",
"document_value": "12345678-9",
"document_type": "dui"
}Ejemplo de Response - GetCreditProductsAvailable (Aprobado):
{
"status": "success",
"products": [
{
"uuid": "prod-uuid-123",
"name": "Crédito Personal",
"approvedCreditAmount": 500.00,
"periods": [
{"id": "1", "name": "15 días", "days": 15},
{"id": "2", "name": "30 días", "days": 30}
]
}
]
}Ejemplo de Response - GetCreditProductsAvailable (Necesita KYC):
{
"status": "error",
"error": {
"code": "burofax_process",
"message": "Requiere verificación",
"onboardingUrl": "https://mpay.com/kyc/session-xyz"
}
}Ejemplo de Response - GetCreditProductsAvailable (Rechazado):
{
"status": "error",
"error": {
"code": "burofax_request_appeal",
"message": "Solicitud no aprobada"
}
}Base URL de N1co Finance Backend: Configurado en MPAY dashboard
| Endpoint | Método | Auth | Evento |
|---|---|---|---|
/api/financing/callback/status-events |
POST | Anonymous | Cambios de estado |
/api/financing/callback/credit-approval |
POST | Anonymous | Aprobación de crédito |
Propósito: Notificar cambios en estado de cuotas de una orden.
Payload:
{
"event": "installment_state_changed",
"timestamp": "2026-01-15T10:30:00-06:00",
"data": {
"customer_uuid": "customer-uuid-123",
"order_uuid": "order-uuid-456",
"order_status": "DUE",
"period_uuid": "period-uuid-789",
"installment": {
"status": "DUE",
"amount": 50.00,
"total_amount": 150.00,
"total_pending_amount": 100.00,
"number": 2,
"expired_at": "2026-01-14T23:59:59Z"
}
}
}Acciones Desencadenadas:
order_status |
Acción |
|---|---|
DUE |
Asignar tag OVERDUE_CUSTOMER |
IN_ARREARS |
Asignar tag OVERDUE_CUSTOMER |
ACTIVE |
Remover tag OVERDUE_CUSTOMER |
PAID |
Remover tag OVERDUE_CUSTOMER |
Código:
// src/Application/Financing/Commands/FinancingCallbackCommand.cs:146-196
private async Task<ApiResponse<string>> HandleInstallmentStateChanged(...)
{
var eventPayload = DeserializeInstallmentPayload(payload);
var account = await GetAccountByCustomerUuid(eventPayload.Data.CustomerUuid);
var tagAction = eventPayload.Data.OrderStatus switch
{
"DUE" or "IN_ARREARS" => TagAction.Assign,
"ACTIVE" or "PAID" => TagAction.Remove,
_ => TagAction.None
};
if (tagAction != TagAction.None)
{
await ProcessTagAction(account, tagAction);
}
}Propósito: Notificar cuando se asigna un nuevo monto de crédito al cliente.
Payload:
{
"event": "assigned_amount_created",
"timestamp": "2026-01-15T12:00:00-06:00",
"data": {
"customer_uuid": "customer-uuid-123",
"type": "equifax",
"status": "applied",
"amount": 1000.00
}
}Tipos de Asignación:
automatic: Aprobación automática por algoritmosource_customer_data: Basado en datos del clienteequifax: Aprobación por consulta a bureau de créditomanual: Aprobación manual por operador
Estados:
applied: Monto aprobado y aplicado al cliente ✅ignored: Monto ignorado (cliente ya tiene uno mayor)⚠️ rejected: Monto rechazado ❌
Acciones según Status:
| Status | Acción |
|---|---|
applied |
✅ Actualizar FinancingProfile.Step = "Home" ✅ Enviar notificación push ✅ Publicar FinancingCreditApprovedEvent |
ignored |
ℹ️ Solo loguear (cliente mantiene monto actual) |
rejected |
❌ Solo loguear (KycFailed) |
Propósito: Notificar cambios en estado de un monto asignado previamente.
Payload:
{
"event": "assigned_amount_updated",
"timestamp": "2026-01-15T14:00:00-06:00",
"data": {
"customer_uuid": "customer-uuid-123",
"type": "manual",
"status": "applied",
"previous_status": "rejected",
"amount": 800.00
}
}Transiciones Importantes:
| Transición | Acción |
|---|---|
rejected → applied |
Aprobado en apelación → Notificar usuario |
applied → rejected |
|
ignored → rejected |
Sin acción (sin cambio para usuario) |
Ubicación: n1co-app/lib/app/ui/pages/bnpl/bnpl_webview_page.dart
Propósito: Embebir proceso de KYC de MPAY dentro de la app.
Parámetros:
BnplWebviewPage({
required String url, // OnboardingUrl de MPAY
required String closeUrl, // URL que indica cierre exitoso
})Configuración:
- URL de cierre: Firebase Remote Config
CLOSE_WEB_VIEW_MPAY_REMOTE_CONFIG_KEY - Fallback:
AppConstants.CLOSE_WEB_VIEW_MPAY_PARAM
Permisos Requeridos:
- 📷 Cámara (para captura de documentos)
- 📂 File Picker (galería/cámara para adjuntar documentos)
Flujo:
1. Backend retorna Step = "Kyc" + OnboardingUrl
2. App navega a BnplWebviewPage(url: OnboardingUrl)
3. WebView carga proceso de MPAY
4. Usuario completa verificación (selfie, documentos, etc.)
5. MPAY redirige a closeUrl al finalizar
6. WebView detecta closeUrl y cierra
7. App retorna a pantalla anterior
8. Backend recibe webhook de MPAY (async)
9. Próxima vez que usuario consulta → Step = "Home"
Código:
void _closeAndSendResult(String url) {
if (url.startsWith(closeUrl)) {
Navigator.of(context).pop(true);
}
}
WebView(
initialUrl: widget.url,
javascriptMode: JavascriptMode.unrestricted,
onPageStarted: (url) {
_closeAndSendResult(url);
},
)Ubicación: src/Domain/Entities/FinancingProfile.cs
public class FinancingProfile
{
public Guid Id { get; set; }
public Guid AccountId { get; set; } // FK → Account
public string SourceSystem { get; set; } // "Mercandu"
public string ExternalCustomerId { get; set; } // UUID del cliente en MPAY
public string Step { get; set; } // Home | Kyc | KycFailed | Error
public bool HasApprovedFinancingAmount { get; set; }
// Navigation
public Account Account { get; set; }
}Estados (FinancingProfileSteps):
public const string Home = "Home"; // ✅ Cliente con crédito aprobado
public const string Kyc = "KYC"; // ⏳ Necesita completar KYC
public const string KycFailed = "KYC_FAILED"; // ❌ KYC rechazado / no aprobado
public const string Error = "ERROR"; // ⚠️ Error generalUbicación: src/Domain/Entities/FinancingDetail.cs
public class FinancingDetail
{
public Guid Id { get; set; } // Internal ID (N1co)
public Guid AccountId { get; set; } // FK → Account
public string ExternalFinancingId { get; set; } // UUID de orden en MPAY
public string ReferenceId { get; set; } // Referencia interna
public string ExternalProductId { get; set; } // UUID del producto MPAY
public decimal Amount { get; set; }
public string Status { get; set; } // ACTIVE | PAID | DUE | etc.
public string PaymentLink { get; set; }
// Timestamps
public DateTime CreatedAt { get; set; }
public DateTime? UpdatedAt { get; set; }
// Navigation
public Account Account { get; set; }
}public class MercanduProductsResponse
{
public string Status { get; set; } // "success" | "error"
public List<Product> Products { get; set; }
public ErrorInfo? Error { get; set; }
}
public class Product
{
public string Uuid { get; set; }
public string Name { get; set; }
public decimal? ApprovedCreditAmount { get; set; }
public List<Period> Periods { get; set; }
}
public class ErrorInfo
{
public string Code { get; set; } // "burofax_process", etc.
public string Message { get; set; }
public string? OnboardingUrl { get; set; }
}public class MercanduOrderResponse
{
public string Status { get; set; }
public string Message { get; set; }
public OrderData Data { get; set; }
}
public class OrderData
{
public Guid MpayOrderId { get; set; }
public string Status { get; set; } // ACTIVE | PROCESSING_PAYMENT | PAID | CANCEL
public string? PaymentUrl { get; set; }
} ┌────────────────┐
│ NEW USER │
└────────┬───────┘
│
│ GET /customers
▼
┌────────────────┐
│ GetCredit │
│ Products │
└────────┬───────┘
│
┌────────────┼────────────┐
│ │ │
ApprovedCredit > 0 burofax_ Other
│ process errors
▼ │ │
┌──────────┐ ▼ ▼
│ HOME │ ┌─────────┐ ┌──────────┐
│ (Ready) │ │ KYC │ │ KYC_FAILED│
└──────────┘ │(Pending)│ │ (Rejected)│
└────┬────┘ └──────────┘
│
Complete
KYC in
WebView
│
┌────────────┼────────────┐
│ │ │
Approved Rejected Needs Appeal
│ │ │
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│ HOME │ │KYC_FAILED│ │KYC_FAILED│
└──────────┘ └──────────┘ └──────────┘
Can create Cannot Cannot
financing create create
Archivo: src/Infrastructure/Services/MercanduPay/Constants/MpayProductErrorCodes.cs
| Código MPAY | Descripción | Step N1co | OnboardingUrl |
|---|---|---|---|
ApprovedCreditAmount > 0 |
Crédito aprobado | Home |
No |
burofax_process |
Requiere verificación | Kyc |
Sí |
burofax_request_appeal |
Rechazado, puede apelar | KycFailed |
No |
insufficient_credit |
Crédito insuficiente | KycFailed |
No |
maximum_active_orders |
Máx. órdenes activas | KycFailed |
No |
| Cualquier otro | Error general | KycFailed |
No |
Mapeo MPAY → N1co:
| Estado MPAY | Estado N1co | Permite Pago | Descripción |
|---|---|---|---|
ACTIVE |
UpToDate |
✅ Sí | Al día, sin atrasos |
DUE |
Overdue |
✅ Sí | Cuota vencida, puede pagar |
IN_ARREARS |
Overdue |
✅ Sí | Múltiples cuotas vencidas |
PROCESSING_PAYMENT |
Pending |
❌ No | Pago en proceso |
PAID |
Paid |
❌ No | Totalmente pagado |
CANCEL |
Canceled |
❌ No | Cancelado |
Archivo: src/Application/Financing/Commands/PayFinancingWithCardCommand.cs:87-96
var allowedStatuses = new[] { "UpToDate", "Overdue" };
if (!allowedStatuses.Contains(financingDetailResponse.Status))
{
return ApiResponse<PayFinancingWithCardResponse>.CreateError(
"El financiamiento no está en un estado válido para realizar pagos",
ResponseCodes.ErrorResponse
);
}Escenario: MPAY lanza endpoint /api/v1/customers/{uuid}/transactions
Archivo: src/Infrastructure/Services/MercanduPay/Client/IMPayHttpClient.cs
public interface IMPayHttpClient
{
// ... métodos existentes
Task<CustomerTransactionsResponse?> GetCustomerTransactions(
string customerUuid,
int page = 1,
int perPage = 10);
}Archivo: src/Infrastructure/Services/MercanduPay/Client/MpayHttpClient.cs
public async Task<CustomerTransactionsResponse?> GetCustomerTransactions(
string customerUuid,
int page = 1,
int perPage = 10)
{
logger.LogInformation(
"MpayHttpClient Getting transactions for customer: {CustomerUuid}, page: {Page}",
customerUuid, page);
try
{
var token = await Login();
var url = $"api/v1/customers/{customerUuid}/transactions?page={page}&per_page={perPage}";
var response = await GetHttpResponseAsync(url, HttpMethod.Get, token);
if (string.IsNullOrEmpty(response))
{
logger.LogWarning("MpayHttpClient Empty response for GetCustomerTransactions");
return null;
}
var options = new JsonSerializerOptions
{
PropertyNameCaseInsensitive = true
};
return JsonSerializer.Deserialize<CustomerTransactionsResponse>(response, options);
}
catch (Exception ex)
{
logger.LogCritical(ex,
"MpayHttpClient Error getting transactions for customer: {CustomerUuid}",
customerUuid);
return null;
}
}Archivo: src/Infrastructure/Services/MercanduPay/Model/CustomerTransactionsResponse.cs
public class CustomerTransactionsResponse
{
public string Status { get; set; }
public List<Transaction> Data { get; set; }
public PaginationMeta Meta { get; set; }
}
public class Transaction
{
public string Uuid { get; set; }
public decimal Amount { get; set; }
public string Type { get; set; }
public string Status { get; set; }
public DateTime CreatedAt { get; set; }
}Archivo: src/Application/Common/Interfaces/IFinancingService.cs
public interface IFinancingService
{
// ... métodos existentes
Task<CustomerTransactionsDto> GetCustomerTransactionsAsync(
Account account,
int page = 1,
int perPage = 10);
}Archivo: src/Infrastructure/Services/MercanduPay/FinancingService.cs
public async Task<CustomerTransactionsDto> GetCustomerTransactionsAsync(
Account account,
int page = 1,
int perPage = 10)
{
logger.LogInformation(
"FinancingService Getting transactions for account: {AccountId}",
account.Id);
try
{
// 1. Obtener perfil de financiamiento
var profile = await financingProfileRepository
.GetByAccountIdAndSourceAsync(account.Id, SourceSystem);
if (profile == null)
{
logger.LogWarning(
"FinancingService No profile found for account: {AccountId}",
account.Id);
return new CustomerTransactionsDto { IsSuccess = false };
}
// 2. Llamar a MPAY
var response = await mPayHttpClient.GetCustomerTransactions(
profile.ExternalCustomerId,
page,
perPage);
if (response == null || !response.Data.Any())
{
return new CustomerTransactionsDto
{
IsSuccess = true,
Transactions = new List<TransactionDto>()
};
}
// 3. Transformar a dominio (aplicar reglas de negocio si aplica)
var transactions = response.Data.Select(t => new TransactionDto
{
Id = t.Uuid,
Amount = t.Amount,
Type = t.Type,
Status = t.Status,
Date = t.CreatedAt
}).ToList();
return new CustomerTransactionsDto
{
IsSuccess = true,
Transactions = transactions,
TotalPages = response.Meta.TotalPages,
CurrentPage = page
};
}
catch (Exception ex)
{
logger.LogCritical(ex,
"FinancingService Error getting transactions for account: {AccountId}",
account.Id);
return new CustomerTransactionsDto { IsSuccess = false };
}
}Archivo: src/Application/Financing/Queries/GetCustomerTransactionsQuery.cs
[Authorize]
public class GetCustomerTransactionsQuery : IRequest<ApiResponse<CustomerTransactionsDto>>
{
public int Page { get; set; } = 1;
public int PerPage { get; set; } = 10;
public class Handler : IRequestHandler<GetCustomerTransactionsQuery,
ApiResponse<CustomerTransactionsDto>>
{
private readonly IFinancingService _financingService;
private readonly IUserService _userService;
private readonly ILogger<Handler> _logger;
public Handler(
IFinancingService financingService,
IUserService userService,
ILogger<Handler> logger)
{
_financingService = financingService;
_userService = userService;
_logger = logger;
}
public async Task<ApiResponse<CustomerTransactionsDto>> Handle(
GetCustomerTransactionsQuery request,
CancellationToken cancellationToken)
{
_logger.LogInformation(
"GetCustomerTransactionsQuery Page: {Page}, PerPage: {PerPage}",
request.Page, request.PerPage);
var account = await _userService.GetAccountFromAuthenticatedUser();
if (account == null)
{
return ApiResponse<CustomerTransactionsDto>.CreateError(
"Usuario no encontrado",
ResponseCodes.UserNotFoundError);
}
var result = await _financingService.GetCustomerTransactionsAsync(
account,
request.Page,
request.PerPage);
if (!result.IsSuccess)
{
return ApiResponse<CustomerTransactionsDto>.CreateError(
"Error obteniendo transacciones",
ResponseCodes.ErrorResponse);
}
return ApiResponse<CustomerTransactionsDto>.CreateSuccess(
result,
"Transacciones obtenidas exitosamente",
ResponseCodes.SuccessfulResponse);
}
}
}Archivo: src/WebUI/Controllers/FinancingController.cs
[HttpGet("transactions")]
public async Task<ApiResponse<CustomerTransactionsDto>> GetCustomerTransactions(
[FromQuery] int page = 1,
[FromQuery] int perPage = 10)
{
var query = new GetCustomerTransactionsQuery
{
Page = page,
PerPage = perPage
};
return await Mediator.Send(query);
}Archivo: n1co-app-gateway/src/Application/Common/Interfaces/IFinanceService.cs
Task<FinanceServiceResponse<CustomerTransactionsDto>> GetCustomerTransactions(
int page = 1,
int perPage = 10);Archivo: n1co-app-gateway/src/Infrastructure/Services/FinanceService.cs
public async Task<FinanceServiceResponse<CustomerTransactionsDto>> GetCustomerTransactions(
int page = 1,
int perPage = 10)
{
var response = await _httpClient.GetAsync(
$"financing/transactions?page={page}&perPage={perPage}");
return await response.ParseHttpRequestResponse<CustomerTransactionsDto>();
}Archivo: n1co-app-gateway/src/WebUI/GraphQL/Queries/Financing/FinancingQueries.cs
public async Task<FinanceServiceResponse<CustomerTransactionsDto>> GetCustomerTransactions(
[Service] IMediator mediator,
int page = 1,
int perPage = 10)
{
var query = new GetCustomerTransactionsQuery { Page = page, PerPage = perPage };
return await mediator.Send(query);
}GraphQL Query:
const String getCustomerTransactionsQuery = '''
query GetCustomerTransactions(\$page: Int!, \$perPage: Int!) {
getCustomerTransactions(page: \$page, perPage: \$perPage) {
data {
transactions {
id
amount
type
status
date
}
totalPages
currentPage
}
isSuccess
message
}
}
''';Use Case:
class GetCustomerTransactionsUseCase {
final IFinancingRepository _repository;
Future<CustomerTransactionsModel> call({
required int page,
required int perPage,
}) async {
final dto = await _repository.getCustomerTransactions(page, perPage);
return CustomerTransactionsMapper().call(dto);
}
}Cubit:
class CustomerTransactionsCubit extends Cubit<CustomerTransactionsUiState> {
final GetCustomerTransactionsUseCase _getTransactionsUseCase;
CustomerTransactionsCubit(this._getTransactionsUseCase)
: super(CustomerTransactionsUiState.initial());
Future<void> loadTransactions({int page = 1}) async {
emit(state.copyWith(isLoading: true));
try {
final transactions = await _getTransactionsUseCase.call(
page: page,
perPage: 10,
);
emit(state.copyWith(
transactions: transactions,
isLoading: false,
));
} catch (e) {
emit(state.copyWith(
error: e.toString(),
isLoading: false,
));
}
}
}| Componente | Ruta |
|---|---|
| Controller | src/WebUI/Controllers/FinancingController.cs |
| Facade Service | src/Infrastructure/Services/MercanduPay/FinancingService.cs |
| HTTP Client | src/Infrastructure/Services/MercanduPay/Client/MpayHttpClient.cs |
| Webhook Handler | src/Application/Financing/Commands/FinancingCallbackCommand.cs |
| Create Financing | src/Application/Financing/Commands/CreateFinancingCommand.cs |
| Pay With Card | src/Application/Financing/Commands/PayFinancingWithCardCommand.cs |
| Domain Events | src/Domain/Events/FinancingCreditApprovedEvent.cs |
| Error Codes | src/Infrastructure/Services/MercanduPay/Constants/MpayProductErrorCodes.cs |
| Config | src/WebUI/appsettings.json (líneas 247-250) |
| WebView (Flutter) | n1co-app/lib/app/ui/pages/bnpl/bnpl_webview_page.dart |
appsettings.json:
{
"MercanduPay": {
"ApiBaseUrl": "https://api.mercandu-dev.com",
"ClientId": "[SECRET]",
"ClientSecret": "[SECRET]"
}
}GlobalSettings (FINANCING_CONFIG):
{
"initialAmount": 100.00,
"minimumAmount": 50.00,
"financePercentage": 80,
"stepAmountSize": 10.00
}Unit/Integration Tests:
tests/Application.IntegrationTests/FinancingService/MpayHttpClientTests.cs(~1500 líneas)tests/Application.IntegrationTests/FinancingService/FinancingServiceTests.cstests/Application.IntegrationTests/Financing/Commands/FinancingCallbackCommandTests.cs
Para preguntas sobre esta integración:
- Documentación MPAY: Contactar a MercanduPay
- Issues de Código: Crear issue en repositorio interno
- Arquitectura: Equipo de Backend N1co
| Versión | Fecha | Cambios |
|---|---|---|
| 1.0 | Enero 2026 | Documentación inicial completa |
IMPORTANTE: Este documento contiene información sensible sobre la integración.
- ✅ Usar en ambientes de desarrollo/staging
- ❌ NO compartir fuera del equipo
- ❌ NO commitear credentials reales
- ✅ Mantener actualizado con cambios en MPAY API
Generado por: Análisis de codebase N1co Issuing Última actualización: Enero 2026