# Plan — Integración Novasis Pay en el ERP (cobros, links y QR de cobradores)

> Fecha: 2026-08-17
> Alcance: puntos priorizados **1, 2 y 3**. Reembolsos se ven aparte con el proveedor.
> Repos involucrados: `novasispy-backend-api` (ERP backend), `facvoice-erp` (ERP frontend),
> `novasis-pay` (gateway), `novasis-pay-checkout` (checkout hospedado), app móvil de cobradores.

---

## Avance (2026-08-17)

- ✅ **QR embebido configurable por empresa** (`qr_embebido`): config ERP → `uiConfig` de la
  checkout session → checkout SPA lo dibuja o redirige. Flag `false` por defecto.
- ✅ **Mínimo por proveedor**: el catálogo del gateway expone `minAmount` (Dpago `PYG:1000`,
  dLocal `PYG:10000`); el modal de Nuevo Cobro lo valida según el provider ruteado.
- ✅ **Métodos de pago por proveedor**: `GET /providers/:provider/payment-methods` (gateway) +
  proxy en el ERP; alimenta el selector de QR/medio. Dpago devuelve sus 13 platformIds.
- ✅ **Tema 1 — modalidad "QR/directo" vs "link de pago" en Nuevo Cobro**:
  - Backend ERP: `novasis_pay_cobro` con `modo`/`platform_id`/`redirect_url`/`qr_data`
    (migración aplicada); `cobros.service` confirma en el acto en modo `directo` y devuelve
    `qr`/`redirect_url`; `confirmPaymentIntent()` en el client.
  - Frontend ERP: toggle de modalidad + selector de medio + pantalla de resultado que dibuja el
    QR (`qrcode`) o cae al redirect del proveedor (típico en pruebas).
  - Gateway: `POST /payment-intents/:id/confirm` (ya existía) + endpoint de métodos.
- ✅ **Fix 400 "customerId does not belong to this merchant"**: `novasis_pay_customer_ref` ahora
  scopeado por `merchant_id` (migración aplicada). Al cambiar de comercio ya no se reusa un `cus_`
  de otro comercio; las filas viejas (merchant NULL) no matchean y se recrea el customer.
- ⏳ **Pendiente para operar de verdad**: (a) credenciales del ERP deben apuntar a un comercio del
  gateway con Dpago activo; (b) QR embebido real requiere Dpago en **producción** (en sandbox
  `qrInformation` viene null → se usa el redirect). Ver *Estado actual verificado*.
- 🔜 **En curso**: descriptor de capacidades por proveedor (ver *Arquitectura*) para dejar de
  hardcodear reglas por proveedor en el ERP.

**Próximo:** descriptor de capacidades → luego Base (vínculo UUID) + Punto 1 (auto-registro).

---

## Contexto y hallazgos clave

Casi toda la infraestructura ya existe en el ERP. El trabajo es **conectar Novasis Pay a los
rieles existentes**, no construir contabilidad paralela.

- **Webhook del ERP** (`src/novasis-pay/novasis-pay-webhooks.service.ts`): ya es
  **idempotente** (`novasis_pay_webhook_event_in` con `event_id` único), **HMAC-verificado**
  (`t=<unix>,v1=<hmac>`, tolerancia 300s), multi-tenant por `merchant_id`, actualiza el estado
  del cobro y aplica a pedidos de ecommerce. → El punto 1 está **medio hecho**; falta la parte
  de documento + contabilidad.
- **Recibos de cobro** (`recibos_cobro` + `recibo_cobro_facturas` + `recibo_cobro_medios_pago`):
  sistema maduro con **modo MULTI**, vínculo a `factura_cab_id` **+ `cuota_id`**, medios de pago
  con `referencia` y `cuenta_tesoreria_id`, y **cobranza móvil ya incorporada** (`cobrador_id`,
  `geo_lat/lng`, `rendicion_id`, `origen`). → Destino natural de todo pago Novasis Pay.
- **Contabilidad**: `IntegracionService.integrarReciboMulti(reciboId, empresaId)` **ya genera el
  asiento** de un recibo. Existe además **`integrarPagoBancard`** — precedente exacto de "pago de
  pasarela → contabilidad". → No inventamos asientos: reusamos.

**Traducción:** registrar el pago = crear un `recibo_cobro` + llamar al integrador contable
existente. Ese es el corazón del punto 1.

---

## Arquitectura: el gateway es la capa de abstracción de proveedores

**Regla de oro:** el ERP (y todo cliente: checkout, app móvil, ecommerce) habla **un solo contrato
canónico** con el gateway. El gateway, vía **adapters**, traduce esos datos a lo que exige cada
proveedor. El cliente **nunca** conoce reglas de Dpago/dLocal/etc.

- **Datos** → canónicos en el gateway (superset: `Customer` con email/name/docType/docNumber/
  phone/country). Cada adapter toma lo que necesita (Dpago → `customer{email,identification,name,
  phone}`; dLocal → su `payer`).
- **Lógica específica de proveedor** → siempre en su **adapter** (gateway). Nunca en el ERP ni en
  el checkout. dLocal (100% funcional) no se toca al sumar proveedores.
- **Diferencias declaradas** → en un **descriptor de capacidades por proveedor**, para que los
  clientes se adapten solos y el gateway valide.

### Descriptor de capacidades (extender `GET /providers`)

Por proveedor, además de lo que ya hay (`minAmount`, `paymentMethods`, `modes`, `available`):

| Campo | Dpago | dLocal |
|---|---|---|
| `minAmount` (✅) | `{PYG:1000}` | `{PYG:10000}` |
| `paymentMethods` (✅) | 13 platformIds | card/transfer |
| `requiresPlatformId` | `true` | `false` |
| `requiredCustomerFields` | `[email,name,doc,phone]` | `[]` |
| `supportsDirect` / `supportsHosted` | `true` / `true` | `false` / `true` |
| `features` | `[qr]` | `[refund]` |

**Efecto:**
1. El ERP arma el formulario **dinámicamente**: selector de método solo si `requiresPlatformId`;
   exige los campos de comprador que declara el proveedor; valida el mínimo. Sin reglas
   hardcodeadas por proveedor (hoy están hardcodeadas: mínimo dLocal, "directo exige comprador
   completo" → migrar a este descriptor).
2. El gateway **valida** el request canónico contra el proveedor ruteado y devuelve error
   estructurado y claro si falta algo (el cliente solo reacciona al error).
3. Agregar el proveedor #3 = **un adapter + una entrada en el catálogo**. Cero cambios en
   ERP/checkout/app móvil.

---

## Estado actual verificado (2026-08-17)

Revisión en vivo del ERP + gateway local:

- **La UI de generación de link YA EXISTE y funciona**: pantalla `Novasis Pay → COBROS`, botón
  **"Nuevo cobro"** → modal con Monto, Referencia interna, Descripción, Comprador y **"Generar
  link de pago"**. Ya hay cobros listados con estados (Aprobado / Pendiente), referencia,
  documento y descripción. → **El punto 2 (generar link) está casi hecho a nivel UI + backend
  hosted.** Falta el vínculo tipado por UUID (hoy solo referencia + descripción libres) y el
  "enviar link desde la factura".

- **⚠️ BLOQUEANTE de configuración para probar de punta a punta**: el ERP
  (`novasis_pay_config`) apunta a `http://localhost:3010/v1` con
  `merchant_id = mer_pa4c2rh8g4mqyd8vq28qqjct` y `pk_test_p0dnb34s8...`, pero **ese comercio no
  existe en ese gateway**. El gateway local tiene:
  - `Demo Checkout SA` (`mer_01kzv3twk5aa6rxzekans9ykwj`)
  - `Demo Dpago SA` (`mer_01kzv5rr69fv95fta4argwfbps`) — **con Dpago activo**, clave pública
    `pk_test_7b6gyxkt96p4...`.
  El chip "Conectado" es solo el flag `activo`, no una prueba real. Con las credenciales actuales,
  "Generar link de pago" fallará la auth contra el gateway.

  **Fix (config, no código):** en `Novasis Pay → Cuentas`, cargar las credenciales reales de un
  comercio del gateway con Dpago activo (ej. `Demo Dpago SA`): su `secret_key` (`sk_test_…`
  completo — si no lo tienen, rotar/crear una API key nueva desde el admin del gateway),
  `publishable_key` `pk_test_7b6gyxkt96p4...` y `merchant_id` `mer_01kzv5rr69fv95fta4argwfbps`.
  Guardar y usar **PROBAR CONEXIÓN** para validar de verdad.

- **Caveat de modo test con Dpago**: la transacción DIRECTA (`/transactions`, con `platformId`)
  **solo opera en prod**. En `test`, para que el comprador **complete** el pago hay que usar el
  **link de pago** (`/links`, funciona en dev) o pasar a credenciales prod. Generar el cobro/link
  sí funciona en test; lo que no completa en test es el método directo (QR/tarjeta).

---

## Base (habilita 1, 2 y 3): vínculo tipado por UUID en el cobro

Hoy `novasis_pay_cobro` solo tiene `external_reference` (texto libre). Se agrega vínculo
**tipado y opcional**:

- `documento_tipo` (`factura` | `recibo` | `cuota` | `pedido` | `null`)
- `documento_id UUID?`
- `cuota_ids UUID[]` (un pago puede saldar una o varias cuotas)
- `recibo_id UUID?` (se completa al registrar el pago; guard de idempotencia)

Si no hay vínculo → sigue siendo un cobro **suelto** con `external_reference` (el "Nuevo Cobro"
actual). Migración aditiva (`ADD COLUMN IF NOT EXISTS`) + DTO + service. No rompe nada existente.

---

## Punto 1 — Auto-registro + asiento contable en `aprobado`

En el webhook, cuando `nextStatus === aprobado`, además de marcar el cobro:

1. Si el cobro tiene **vínculo a documento** → crear `recibos_cobro` (modo MULTI) con:
   - `recibo_cobro_facturas` = factura(s)/cuota(s) vinculadas, con `monto_pagado`.
   - `recibo_cobro_medios_pago` = `medio='NOVASIS_PAY'`, `referencia` = intent/reference Dpago,
     `cuenta_tesoreria_id` = cuenta "por liquidar" de la pasarela.
2. Llamar **`IntegracionService.integrarReciboMulti(recibo.id, empresaId)`** → asiento contable
   automático (Clientes al haber, Por Liquidar al debe).
3. Idempotencia: guardar `recibo_id` en el cobro. Si el webhook se repite, no duplica (ya hay
   dedupe por `event_id`, más un guard por cobro).

Cobros **sueltos** (sin vínculo): quedan aprobados y visibles en Tesorería, sin recibo contra
factura (o con un asiento simple a "por liquidar" — a definir).

**A confirmar en implementación:** cómo se resuelve la `cuenta_tesoreria_id` de "por liquidar"
(mapearla desde `cuenta_contable_por_liquidar` de la config, o crear una cuenta de tesorería
dedicada "Novasis Pay").

---

## Punto 2 — Link de pago asociado a factura desde el ERP

**Ya existe** (verificado): modal "Nuevo cobro" con Monto/Referencia/Descripción/Comprador →
"Generar link de pago", lista de cobros con estado en vivo, **selector de modalidad
(link / QR directo)** y **selector de medio de pago por proveedor**. Backend hosted + directo
(`cobros.service`) funcionando. **Pendiente para cerrar el punto 2:**

- Extender "Nuevo Cobro" para **vincular a factura/recibo/cuota** (por UUID) además del cobro
  suelto por referencia (usa la Base). Hoy solo guarda referencia + descripción libres.
- Botón **"Enviar link"** desde la propia factura (WhatsApp/email), no solo desde la pantalla de
  Novasis Pay.
- Al pagar → el punto 1 lo registra y asienta solo.
- (La pantalla de Cobros ya lista y refleja estado en vivo vía webhook + sync de respaldo.)
- **Requisito previo**: resolver el bloqueante de credenciales (ver *Estado actual verificado*)
  para que el link generado sea realmente pagable.

---

## Punto 3 — QR directo del cobrador (app móvil)

> **Base ya construida** (Tema 1): el path `directo` de cobro (`modo='directo'` → crea intent +
> confirma con `platform_id` → devuelve `qr`/`redirect_url`) y el endpoint de métodos ya existen y
> los reusa la app móvil. Falta lo específico de cobranza móvil (abajo).

- La app del cobrador pega al **ERP backend** (nunca al gateway; las credenciales quedan
  server-side). Reusa `POST /novasis-pay/cobros` con `modo='directo'` + `platform_id`.
- El cobro directo ya crea intent + confirma con `platformId` de QR → devuelve `qr` (embebido) y
  `reference`.
- El cobrador muestra el QR; la app hace **polling** al `sync` hasta `aprobado`.
- Al aprobarse, el recibo se crea con `origen='novasis_pay_qr'`, `cobrador_id`, `geo_lat/lng` y
  `rendicion_id` (campos de cobranza móvil ya existentes) y salda la(s) **cuota(s)**.
- Requiere credenciales **prod** de Dpago (el QR embebido no viene en sandbox).

---

## Transversal

- **Catálogo de medio de pago**: definir `NOVASIS_PAY` (o separar `QR` / `TARJETA`) en
  `recibo_cobro_medios_pago.medio` + su cuenta de tesorería.
- **Estados**: solo `aprobado` dispara recibo + asiento; `pending` / `processing` nunca.
- **Idempotencia**: dedupe por `event_id` (ya existe) + guard "cobro ya tiene recibo".
- **Sync de respaldo**: cron/manual para cobros pendientes cuando el webhook no llega.
- **Seguridad móvil**: el celular del cobrador nunca tiene el `secret_key`; siempre app → ERP →
  gateway.
- **Cuotas ≠ 1 pago = 1 cuota**: soportar que un pago salde varias (o parcial). Es la decisión de
  modelo más cara de cambiar después → se define en la Base.

---

## Secuencia sugerida

1. **Base** (vínculo UUID): 1 migración + DTO/service. Chico, desbloquea todo.
2. **Punto 1** (webhook → recibo → `integrarReciboMulti`): el núcleo; sobre él, 2 y 3 son fáciles.
3. **Punto 2** (UI Nuevo Cobro vinculado + enviar link).
4. **Punto 3** (endpoint directo QR + flujo móvil).

**Opinión fuerte:** no construir un asiento propio para Novasis Pay. Reusar `recibos_cobro` +
`integrarReciboMulti` da conciliación, tesorería, cobranza móvil y reportes **gratis y
consistentes** con el resto del ERP. Contabilidad paralela sería el error caro.

---

## Casos de uso adicionales recomendados (backlog)

Ordenados por ROI para un ERP paraguayo:

1. **Recupero de cartera / cuentas por cobrar**: botón "enviar link" por factura vencida + envío
   masivo por WhatsApp/email a morosos (usa `/links` + módulo Contactos).
2. **Suscripciones / cobros recurrentes**: link recurrente o método guardado para planes.
3. **Portal de autogestión del cliente**: el cliente ve sus facturas/cuotas y paga solo.
4. **QR dinámico en caja presencial**: mismo QR directo del cobrador, en el mostrador.
5. **Pago parcial / a cuenta**: un pago cubre saldo parcial o varias cuotas (tabla puente ya
   contemplada en la Base).

---

## Pendientes con soporte Dpago (bloquean prod)

1. ¿El estado se lee por `GET /transactions/:reference`, por webhook, o ambos? (la guía nueva solo
   documenta los dos POST).
2. Formato y firma reales del webhook / "URL de respuesta" (`parseWebhook` está TENTATIVO).
3. `platformId` habilitados para el comercio 829 (5/6/15 daban `DISABLED`).

Herramientas para probar: `docs/postman-dpago-novasis.json` (+ `.environment.json`) e
`docs/insomnia-dpago-novasis.json`.
