Skip to content

Instantly share code, notes, and snippets.

@jluisflo
Last active January 20, 2026 02:49
Show Gist options
  • Select an option

  • Save jluisflo/2da0504b557f8ca56142d043222f6aa1 to your computer and use it in GitHub Desktop.

Select an option

Save jluisflo/2da0504b557f8ca56142d043222f6aa1 to your computer and use it in GitHub Desktop.
N1co Issuing ↔ MPAY Integration Guide - Complete technical documentation for Buy Now Pay Later (BNPL) integration

Guía de Integración: N1co Issuing ↔ MPAY (Buy Now Pay Later)

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)


📋 Tabla de Contenidos

  1. Arquitectura General
  2. Componentes del Sistema
  3. Flujos de Comunicación
  4. API Endpoints
  5. Webhooks y Eventos
  6. WebView Integration
  7. Modelos de Datos
  8. Estados y Transiciones
  9. Implementar Nuevos Endpoints
  10. Referencias de Código

🏗️ Arquitectura General

Vista de 10,000 Pies

┌─────────────────────────────────────────────────────────────────────────┐
│                    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

Patrones Arquitectónicos

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

Estructura de Capas (Finance Backend)

┌─────────────────────────────────────────────────────────────┐
│  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                      │
└─────────────────────────────────────────────────────────────┘

🔧 Componentes del Sistema

1. Frontend (Flutter App)

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

2. Gateway (GraphQL)

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

3. Finance Backend (Core)

3.1 FinancingController

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 financiamiento

Webhooks (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] Mirror

3.2 FinancingService (Facade + ACL)

Ubicació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

3.3 MpayHttpClient (Adapter)

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)

4. MPAY API (External Service)

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

🔄 Flujos de Comunicación

Mecanismos de Integración

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

Flujo 1: Onboarding de Cliente

┌─────────┐     ┌─────────┐     ┌──────────┐     ┌──────┐
│  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
    };
}

Flujo 2: Creación de Financiamiento

┌─────────┐     ┌─────────┐     ┌──────────┐     ┌──────┐
│  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:

  1. Validación:

    • Verificar límites de cuenta
    • Verificar perfil de financiamiento existe
    • Validar que Step = "Home"
  2. Crear Orden en MPAY:

    var orderResponse = await mPayHttpClient.CreateFinancing(request);
    // request contiene: ProductId, Amount, PeriodId, CustomerId (UUID MPAY)
  3. Acreditar Fondos (CashIn):

    • Sistema crea transacción interna
    • Acredita monto a la cuenta del usuario
    • Usuario puede usar fondos inmediatamente
  4. 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 = ""
    };
  5. Asignar Tags:

    • Tag "FirstInstallmentTopUp" si es primer financiamiento

Flujo 3: Pago de Cuota con Tarjeta

┌─────────┐     ┌─────────┐     ┌──────────┐     ┌──────────┐     ┌──────┐
│  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)

⚠️ CRÍTICO - Orden de Operaciones:

  1. PRIMERO: Cargar tarjeta con Payment Gateway
  2. DESPUÉS: Notificar a MPAY
  3. 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.

Flujo 4: Webhook de Estado (Cuotas Vencidas)

┌──────┐     ┌──────────┐     ┌─────────────┐     ┌─────────┐
│ 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)

Flujo 5: Webhook de Aprobación de Crédito

┌──────┐     ┌──────────┐     ┌─────────────┐     ┌──────────┐     ┌─────────┐
│ 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:

  1. assigned_amount_created/updated (webhook)
  2. FinancingCreditApprovedEvent (domain event)
  3. NotificationCreatedEvent (domain event)
  4. Firebase Cloud Messaging → Device

🌐 API Endpoints

Endpoints N1co → MPAY

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"
  }
}

🔔 Webhooks y Eventos

Endpoints MPAY → N1co

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

Eventos de Webhook

1. installment_state_changed

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);
    }
}

2. assigned_amount_created

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 algoritmo
  • source_customer_data: Basado en datos del cliente
  • equifax: Aprobación por consulta a bureau de crédito
  • manual: 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 (⚠️ GAP: Debería actualizar a KycFailed)

3. assigned_amount_updated

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
rejectedapplied Aprobado en apelación → Notificar usuario
appliedrejected ⚠️ Revertir (TODO: No implementado)
ignoredrejected Sin acción (sin cambio para usuario)

📱 WebView Integration

BnplWebviewPage (Flutter)

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);
  },
)

💾 Modelos de Datos

Entidades de Dominio

FinancingProfile

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 general

FinancingDetail

Ubicació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; }
}

DTOs de MPAY

MercanduProductsResponse

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; }
}

MercanduOrderResponse

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; }
}

🔄 Estados y Transiciones

Diagrama de Estados del Cliente

                    ┌────────────────┐
                    │   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

Códigos de Error MPAY → Step N1co

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
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

Estados de Financiamiento (Orden)

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
    );
}

🔨 Implementar Nuevos Endpoints

Cuando MPAY Provee Nuevo Endpoint

Escenario: MPAY lanza endpoint /api/v1/customers/{uuid}/transactions

Paso 1: Agregar a IMPayHttpClient

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);
}

Paso 2: Implementar en MpayHttpClient

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;
    }
}

Paso 3: Crear DTOs

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; }
}

Paso 4: Agregar a IFinancingService

Archivo: src/Application/Common/Interfaces/IFinancingService.cs

public interface IFinancingService
{
    // ... métodos existentes

    Task<CustomerTransactionsDto> GetCustomerTransactionsAsync(
        Account account,
        int page = 1,
        int perPage = 10);
}

Paso 5: Implementar en FinancingService

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 };
    }
}

Paso 6: Crear Query en Application Layer

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);
        }
    }
}

Paso 7: Exponer en Controller

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);
}

Paso 8: (Opcional) Exponer en Gateway GraphQL

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);
}

Paso 9: Frontend (Flutter)

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,
      ));
    }
  }
}

📚 Referencias de Código

Archivos Clave

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

Configuración

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
}

Tests

Unit/Integration Tests:

  • tests/Application.IntegrationTests/FinancingService/MpayHttpClientTests.cs (~1500 líneas)
  • tests/Application.IntegrationTests/FinancingService/FinancingServiceTests.cs
  • tests/Application.IntegrationTests/Financing/Commands/FinancingCallbackCommandTests.cs

📞 Contacto y Soporte

Para preguntas sobre esta integración:

  1. Documentación MPAY: Contactar a MercanduPay
  2. Issues de Código: Crear issue en repositorio interno
  3. Arquitectura: Equipo de Backend N1co

📝 Changelog

Versión Fecha Cambios
1.0 Enero 2026 Documentación inicial completa

🔐 Seguridad

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment