Skip to content

Endpoints Detallados - Portal de Clientes

Estado: Implementado

Documentacion detallada de cada endpoint con schemas de request y response.

Autenticacion

POST /portal/auth/register

Auto-registro de un cliente existente en ordcon. El DNI/CUIT debe coincidir con un registro en la tabla ordcon del tenant.

Request:

json
{
  "identifier": "12345678",
  "identifier_type": "dni",
  "email": "juan@example.com",
  "password": "SecurePass123!",
  "password_confirmation": "SecurePass123!",
  "tenant_id": 1,
  "sucursal_id": 1
}
CampoTipoRequeridoDescripcion
identifierstringSiDNI o CUIT del cliente
identifier_typestringSidni o cuit
emailstringSiEmail para comunicaciones y recuperacion
passwordstringSiPassword (minimo 8 caracteres, al menos 1 numero)
password_confirmationstringSiConfirmacion del password
tenant_idintegerSiID del tenant (desde .env del frontend)
sucursal_idintegerSiID de la sucursal (desde .env del frontend)

Response 201:

json
{
  "success": true,
  "data": {
    "portal_user_id": "550e8400-e29b-41d4-a716-446655440000",
    "nombre": "Juan Perez",
    "email": "juan@example.com"
  }
}

Errores:

CodigocodeCausa
404ORDCON_NOT_FOUNDDNI/CUIT no existe en ordcon del tenant
409USER_ALREADY_EXISTSYa existe un portal_user para este ordcon
422INVALID_SUCURSALLa sucursal no pertenece al tenant
422VALIDATION_ERRORDatos invalidos (password debil, email invalido)

Flujo:

mermaid
sequenceDiagram
    participant F as Frontend
    participant C as AuthController
    participant S as PortalAuthService
    participant DB as PostgreSQL

    F->>C: POST /portal/auth/register
    C->>C: Validar request (estructura)
    C->>S: register(data)
    S->>DB: Validar sucursal pertenece a tenant
    S->>DB: Buscar ordcon por DNI/CUIT en schema del tenant
    alt ordcon no encontrado
        S-->>C: Error ORDCON_NOT_FOUND
    end
    S->>DB: Verificar no exista portal_user para este ordcon
    alt ya existe
        S-->>C: Error USER_ALREADY_EXISTS
    end
    S->>S: Hash password (bcrypt)
    S->>DB: INSERT portal_users
    S-->>C: portal_user creado
    C-->>F: 201 Created

POST /portal/auth/login

Autenticacion con credenciales. El frontend envia tenant_id y sucursal_id desde su configuracion (.env).

Request:

json
{
  "identifier": "12345678",
  "identifier_type": "dni",
  "password": "SecurePass123!",
  "tenant_id": 1,
  "sucursal_id": 1
}
CampoTipoRequeridoDescripcion
identifierstringSiDNI o CUIT del cliente
identifier_typestringSidni o cuit
passwordstringSiPassword del usuario
tenant_idintegerSiID del tenant (desde .env del frontend)
sucursal_idintegerSiID de la sucursal (desde .env del frontend)

Response 200:

json
{
  "success": true,
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "550e8400-e29b-41d4-a716-446655440000",
    "expires_in": 3600,
    "user": {
      "portal_user_id": "550e8400-e29b-41d4-a716-446655440000",
      "nombre": "Juan Perez",
      "email": "juan@example.com"
    }
  }
}

El refresh_token es un UUID almacenado en portal_users. Solo hay UNA sesion activa por usuario: un nuevo login sobreescribe el refresh token anterior.

JWT Payload:

json
{
  "portal_user_id": "550e8400-e29b-41d4-a716-446655440000",
  "tenant_id": 1,
  "sucursal_id": 1,
  "iat": 1712678400,
  "exp": 1712682000
}

El JWT NO contiene nombres de base de datos ni schemas. Solo IDs que el backend resuelve internamente:

  • tenant_id -> base de datos via ini.sistema
  • sucursal_id -> schema (sucXXXX, o public si ordcon es a nivel empresa)

Errores:

CodigocodeCausa
401INVALID_CREDENTIALSDNI/CUIT o password incorrectos
403ACCOUNT_LOCKEDCuenta bloqueada por intentos fallidos
422INVALID_SUCURSALLa sucursal no pertenece al tenant

Flujo:

mermaid
sequenceDiagram
    participant F as Frontend
    participant C as AuthController
    participant S as PortalAuthService
    participant DB as PostgreSQL

    F->>C: POST /portal/auth/login
    C->>C: Validar request
    C->>S: login(data)
    S->>DB: Validar sucursal pertenece a tenant
    S->>DB: Resolver DB y schema desde tenant_id/sucursal_id
    S->>DB: Buscar portal_user por DNI/CUIT
    alt no encontrado
        S-->>C: Error INVALID_CREDENTIALS
    end
    S->>S: Verificar no este bloqueado (locked_until)
    alt bloqueado
        S-->>C: Error ACCOUNT_LOCKED
    end
    S->>S: Verificar password_hash (bcrypt)
    alt password incorrecto
        S->>DB: Incrementar failed_attempts
        alt >= 5 intentos
            S->>DB: Establecer locked_until = now + 15 min
        end
        S-->>C: Error INVALID_CREDENTIALS
    end
    S->>DB: Resetear failed_attempts, actualizar last_login
    S->>S: Generar JWT (portal_user_id, tenant_id, sucursal_id)
    S->>S: Generar refresh_token
    S-->>C: tokens + user data
    C-->>F: 200 OK

POST /portal/auth/forgot-password

Solicita un codigo de recuperacion de password. Se envia por email al email registrado del portal_user.

Request:

json
{
  "identifier": "12345678",
  "identifier_type": "dni",
  "tenant_id": 1,
  "sucursal_id": 1
}

Response 200:

json
{
  "success": true,
  "data": {
    "message": "Si el usuario existe, se envio un codigo al email registrado"
  }
}

La respuesta es siempre exitosa para no revelar si el usuario existe o no.

Comportamiento interno:

  1. Buscar portal_user por DNI/CUIT en el tenant
  2. Si existe, generar codigo numerico de 6 digitos
  3. Guardar codigo con expiracion (15 minutos) en portal_users
  4. Enviar email con el codigo
  5. Si no existe, no hacer nada (respuesta identica)

POST /portal/auth/reset-password

Restablece el password usando el codigo enviado por email.

Request:

json
{
  "identifier": "12345678",
  "identifier_type": "dni",
  "code": "482951",
  "password": "NewSecurePass456!",
  "password_confirmation": "NewSecurePass456!",
  "tenant_id": 1,
  "sucursal_id": 1
}

Response 200:

json
{
  "success": true,
  "data": {
    "message": "Password actualizado correctamente"
  }
}

Errores:

CodigocodeCausa
400INVALID_CODECodigo incorrecto o expirado
422VALIDATION_ERRORPassword no cumple requisitos

POST /portal/auth/refresh-token

Renueva el access JWT usando el refresh token (UUID). Permite mantener la sesion sin re-login. Genera un nuevo par access_token + refresh_token (rotacion).

Request:

json
{
  "refresh_token": "550e8400-e29b-41d4-a716-446655440000"
}

No requiere JWT en el header Authorization. El refresh token es suficiente para identificar al usuario.

Response 200:

json
{
  "success": true,
  "data": {
    "access_token": "eyJhbGciOiJIUzI1NiIs...",
    "refresh_token": "nuevo-uuid-generado",
    "expires_in": 3600
  }
}

Flujo interno:

  1. Buscar portal_users por refresh_token (UUID)
  2. Verificar que refresh_token_expires > NOW()
  3. Verificar que el usuario no este bloqueado
  4. Generar nuevo access JWT con { portal_user_id, tenant_id, sucursal_id }
  5. Generar nuevo UUID de refresh token
  6. UPDATE portal_users SET refresh_token = nuevo_uuid, refresh_token_expires = NOW() + duracion
  7. Retornar nuevo par de tokens

Errores:

CodigocodeCausa
401INVALID_REFRESH_TOKENRefresh token no encontrado, expirado, o usuario bloqueado

Perfil

GET /portal/perfil

Datos del perfil del usuario autenticado. Merge de datos de ordcon (identidad) y portal_users (contacto/sesion).

Headers: Authorization: Bearer {jwt_token}

Response 200:

json
{
  "success": true,
  "data": {
    "nombre": "Juan Perez",
    "dni_cuit": "12345678",
    "email": "juan@example.com",
    "telefono": "1155443322",
    "last_login": "2026-04-08T14:30:00Z"
  }
}

Origen de los campos:

CampoFuenteEditable
nombreordcon (cnom)No (solo admin)
dni_cuitordcon (ccui) / portal_usersNo (solo admin)
emailportal_usersSi
telefonoportal_usersSi
last_loginportal_usersNo (automatico)

PUT /portal/perfil

Actualiza datos de contacto del perfil. Solo email y telefono son editables.

Headers: Authorization: Bearer {jwt_token}

Request:

json
{
  "email": "nuevo@example.com",
  "telefono": "1166554433"
}
CampoTipoRequeridoDescripcion
emailstringNoNuevo email (debe tener formato valido)
telefonostringNoNuevo telefono

Al menos uno de los campos debe estar presente.

Response 200:

json
{
  "success": true,
  "data": {
    "nombre": "Juan Perez",
    "dni_cuit": "12345678",
    "email": "nuevo@example.com",
    "telefono": "1166554433",
    "last_login": "2026-04-08T14:30:00Z"
  }
}

Errores:

CodigocodeCausa
422VALIDATION_ERROREmail con formato invalido o ningun campo enviado

PUT /portal/auth/cambiar-password

Cambio de password del usuario autenticado. Requiere verificacion del password actual.

Headers: Authorization: Bearer {jwt_token}

Request:

json
{
  "current_password": "MiPasswordActual123",
  "new_password": "NuevoPassword456"
}
CampoTipoRequeridoDescripcion
current_passwordstringSiPassword actual para verificacion
new_passwordstringSiNuevo password (minimo 8 caracteres, al menos 1 numero)

Response 200:

json
{
  "success": true,
  "data": {
    "message": "Password actualizado correctamente"
  }
}

Errores:

CodigocodeCausa
401INVALID_PASSWORDEl password actual no es correcto
422VALIDATION_ERROREl nuevo password no cumple politica (8 chars + 1 numero)

Flujo:

  1. Extraer portal_user_id del JWT
  2. Buscar portal_user en la base
  3. Verificar current_password contra password_hash (bcrypt_verify)
  4. Si no coincide -> Error INVALID_PASSWORD
  5. Validar que new_password cumple politica: minimo 8 caracteres + al menos 1 numero
  6. Hash nuevo password con bcrypt
  7. UPDATE portal_users SET password_hash = nuevo_hash

Cuenta Corriente

GET /portal/mi-cuenta

Resumen del estado de cuenta del usuario autenticado. El portal_user_id se extrae del JWT.

Headers: Authorization: Bearer {jwt_token}

Response 200:

json
{
  "success": true,
  "data": {
    "nombre": "Juan Perez",
    "saldo_total": 15000.00,
    "facturas_vencidas": 3,
    "facturas_pendientes": 5,
    "ultimo_pago": {
      "fecha": "2026-01-15",
      "monto": 5000.00
    }
  }
}

GET /portal/deudas

Listado completo de deudas pendientes del usuario autenticado.

Headers: Authorization: Bearer {jwt_token}

Response 200:

json
{
  "success": true,
  "data": {
    "items": [
      {
        "id": "1f2e3d4c-...",
        "comprobante": "Factura A",
        "nrocomp": "0001-00000001",
        "fecha": "2026-01-01",
        "vencimiento": "2026-01-31",
        "debe": 10000.00,
        "vencido": true
      }
    ]
  }
}

Pagos

POST /portal/pagos/iniciar

Inicia un pago online. Crea un registro en portal_payments y obtiene la URL de checkout del gateway. Las facturas seleccionadas deben formar un prefijo contiguo del orden canónico de deudas pendientes (FIFO cronológico). Cada factura seleccionada se paga por el total pendiente.

Headers: Authorization: Bearer {jwt_token}

Request:

json
{
  "facturas": [
    {"id": 1234, "monto": 10000.00},
    {"id": 5678, "monto": 3000.00}
  ],
  "total": 13000.00
}
CampoTipoRequeridoDescripcion
facturasarraySiFacturas seleccionadas para pagar con sus montos completos
facturas[].idstring (UUID)SiID de la deuda/factura (ordcta.id) — es UUID desde la migración de Sept 2024
facturas[].montodecimalSiMonto a pagar. Debe coincidir con el total pendiente de esa factura (deuda.debe)
totaldecimalSiSuma de todos los montos de facturas

Pago parcial prohibido: El portal no permite pagar parcialmente una factura. Si una factura tiene deuda pendiente de $10.000, el request debe enviar monto: 10000.00. Un request con monto: 3000.00 se rechaza con PARTIAL_PAYMENT_NOT_ALLOWED.

No se envia cliente_id en el body. Se obtiene del JWT via portal_user_id -> ordcon.

No se envia gateway en el request. El gateway se resuelve automaticamente desde la configuracion del tenant (ini.sistema.payment_gateway). El campo payment_method en la respuesta refleja que gateway se uso.

Response 200:

json
{
  "success": true,
  "data": {
    "payment_id": "uuid-payment-123",
    "redirect_url": "https://checkout.gateway.com/...",
    "payment_method": "paypertic"
  }
}

Validaciones:

  1. Cada factura debe existir en el schema del tenant
  2. Cada factura debe pertenecer al cliente autenticado (via ordcon)
  3. Cada monto debe ser > 0
  4. Cada monto debe ser <= factura.saldo (no se puede pagar mas del saldo pendiente)
  5. Cada monto debe coincidir con deuda.debe dentro de una tolerancia de 0.001; montos parciales se rechazan
  6. total debe coincidir con la suma de los monto de todas las facturas
  7. Orden canónico (FIFO): los IDs de facturas enviados deben ser exactamente los primeros K del array canónico de deudas pendientes del cliente, ordenado por vencimiento ASC con nulls al final. El orden dentro del payload no importa; solo importa que el conjunto de IDs sea un prefijo contiguo del canónico. No se puede pagar una deuda más nueva sin primero pagar todas las más antiguas.
  8. El request debe incluir header Origin, usado para construir el retorno post-pago
  9. El backend debe tener BACKEND_URL configurada para construir la URL publica del webhook
  10. El tenant debe tener un gateway de pago configurado

Errores:

CodigocodeCausa
400INVALID_FACTURASFacturas invalidas, no pertenecen al cliente, o monto excede saldo
400MONTO_MISMATCHTotal no coincide con suma de montos de facturas
400PARTIAL_PAYMENT_NOT_ALLOWEDAl menos una factura fue enviada con monto menor al total pendiente
400MISSING_ORIGINFalta header Origin; no se puede construir return_url
500MISCONFIGUREDFalta BACKEND_URL; no se puede construir notification_url
422GATEWAY_NOT_CONFIGUREDTenant no tiene gateway de pago configurado
422RECIBO_NOT_CONFIGUREDFalta portal.recibo.cuenta_bancaria o portal.recibo.caja_schema — auto-reconciliación no puede operar
422PAYMENT_ORDER_VIOLATIONLas facturas no forman un prefijo contiguo del orden canónico FIFO (cronológico) de deudas pendientes

POST /portal/pagos/webhook

Recibe notificaciones del gateway de pago. Endpoint publico (no requiere JWT), validado por firma del gateway.

URL registrada en gateway:

text
{BACKEND_URL}/backend/portal/pagos/webhook?tenant_id={tenantId}&sucursal_id={sucursalId}&token={webhookToken}

Gateway-agnostic: Este endpoint usa la misma URL para todos los gateways. No se necesita un endpoint por gateway. La resolucion del adapter se hace internamente: se busca portal_payments por external_id del payload, y el campo gateway de la fila indica que adapter usar para validar y procesar el webhook.

Headers:

  • x-signature: Firma del webhook (validacion de seguridad, formato variable por gateway)
  • x-request-id: ID unico del request (idempotencia)

Request: Variable segun gateway. Cada adapter parsea el formato nativo de su gateway.

Response 200:

json
{
  "success": true
}

Resolucion de tenant en webhooks: El webhook no lleva JWT. La resolucion se hace con los query params registrados en notification_url y se valida con el token configurado:

  1. El gateway llama la URL con tenant_id, sucursal_id y token
  2. El backend valida el token contra la configuracion del tenant
  3. Con tenant_id resuelve la DB via ini.sistema; con sucursal_id resuelve el schema
  4. Busca en portal_payments por external_id para verificar idempotencia y obtener datos completos

Si falta o no coincide el token, responde 401 y no modifica portal_payments. Si tenant_id o sucursal_id faltan o no son numericos, responde 400 y no modifica portal_payments.

Flujo de acreditacion:

mermaid
sequenceDiagram
    participant GW as Gateway
    participant C as PagosController
    participant S as PortalPaymentService
    participant DB as PostgreSQL

    GW->>C: POST /backend/portal/pagos/webhook?tenant_id=...&sucursal_id=...&token=...
    C->>S: procesarWebhook(headers, body)
    S->>S: Validar token y query params
    S->>DB: Resolver DB y schema
    S->>DB: Buscar portal_payment por external_id
    S->>S: Verificar idempotencia (no procesar dos veces)
    alt pago aprobado
        S->>DB: TX1: UPDATE portal_payments SET status = 'approved'
        S->>DB: TX2: INSERT ordcta (recibo) + movimi (caja)
        S->>DB: TX2: UPDATE portal_payments SET recibo_id, recibo_at
    else pago rechazado
        S->>DB: UPDATE portal_payments SET status = 'rejected'
    end
    S-->>C: processed
    C-->>GW: 200 OK

Auto-reconciliación: El webhook no solo actualiza el estado. Tras commitear TX1 (status = approved), dispara TX2 que genera el recibo en ordcta y el movimiento en caja automáticamente. TX2 es independiente de TX1 — un fallo de TX2 no revierte la aprobación. Ver auto-reconciliación técnico.


GET /portal/pagos/historial

Historial de pagos del usuario autenticado.

Headers: Authorization: Bearer {jwt_token}

Response 200:

json
{
  "success": true,
  "data": [
    {
      "id": "uuid-payment-123",
      "fecha": "2026-01-20",
      "metodo": "online",
      "monto": 15000.00,
      "estado": "approved",
      "facturas_pagadas": [
        {"tipo": "Factura A", "numero": 123, "monto": 10000.00}
      ],
      "recibo_numero": "REC-00123"
    }
  ]
}

GET /backend/portal/pagos/{id}/recibo

Descarga el PDF del recibo de un pago aprobado. El backend hace ownership-check, consulta ordcta + recfac en el schema sucursal, delega la generación del PDF al servicio Informes (case "portal-recibo"), y retorna el binario como stream.

El cliente nunca accede directamente al servicio Informes.

Headers: Authorization: Bearer {jwt_token}

Path Parameters:

ParametroTipoDescripcion
idintegerportal_payments.id del pago

Response 200:

Content-Type: application/pdf
Content-Disposition: inline; filename="recibo-{numero}.pdf"
Cache-Control: no-store

<binary PDF data>

El PDF se abre inline en el navegador. El frontend recibe el response como blob (responseType: 'blob') y usa window.open(URL.createObjectURL(blob)) para abrirlo en nueva pestaña.

Errores:

CodigocodeCausa
401UNAUTHORIZEDJWT ausente o invalido
403FORBIDDENEl pago existe pero pertenece a otro cliente (ordcon_id del JWT no coincide)
404PAYMENT_NOT_FOUNDNo existe portal_payments con el ID dado
409RECIBO_PENDIENTEEl pago esta aprobado pero recibo_id IS NULL (TX2 no completo aun)
422RECIBO_NO_DISPONIBLEEl pago existe pero no esta en estado approved
502INFORMES_ERRORError al comunicarse con el servicio Informes (timeout, 4xx, 5xx)

Notas:

  • recibo_id IS NOT NULL en portal_payments es la unica fuente de verdad para "recibo disponible"
  • Cache-Control: no-store evita que proxies cacheen PDFs de un cliente y los sirvan a otro
  • El sucursal_id para el cross-schema lookup viene siempre del JWT, nunca del body

Flujo:

mermaid
sequenceDiagram
    participant F as Frontend
    participant C as PortalPaymentController
    participant S as PortalPaymentService
    participant DB as PostgreSQL
    participant I as Informes Service<br/>(puerto 9999)

    F->>C: GET /backend/portal/pagos/{id}/recibo<br/>Authorization: Bearer {jwt}
    C->>C: Extraer ordcon_id, sucursal_id, tenant_id del JWT
    C->>S: getReciboPdf(paymentId, ordconId, sucursalId, tenantId)
    S->>DB: SELECT portal_payments WHERE id=:id AND ordcon_id=:ordcon
    alt no row
        S-->>C: PaymentNotFoundException → 404
    else status != approved
        S-->>C: ReciboNoDisponibleException → 422
    else recibo_id IS NULL
        S-->>C: ReciboPendienteException → 409
    end
    S->>DB: setSearchPath(suc{sucursal_id})<br/>SELECT ordcta + recfac
    S->>S: Resolver DB via IniSistemaRepo<br/>Emitir s2s JWT (InternalJwtIssuer)
    S->>I: POST {URL_INFORMES} {codReporte:"portal-recibo", ...}
    alt Informes OK
        I-->>S: 200 application/pdf
        S-->>C: [pdfBinary, numeroRecibo]
        C-->>F: 200 application/pdf<br/>Content-Disposition: inline; filename="recibo-{numero}.pdf"
    else Informes error
        I-->>S: error / timeout
        S-->>C: InformesUnavailableException → 502
    end

POST /portal/pagos/cancelar

Cancela un pago pendiente. Solo se pueden cancelar pagos con status pending.

Headers: Authorization: Bearer {jwt_token}

Request:

json
{
  "payment_id": "uuid-payment-123"
}
CampoTipoRequeridoDescripcion
payment_idstring (UUID)SiID del portal_payment a cancelar

Response 200:

json
{
  "success": true,
  "data": {
    "payment_id": "uuid-payment-123",
    "status": "cancelled"
  }
}

Errores:

CodigocodeCausa
404PAYMENT_NOT_FOUNDEl payment_id no existe o no pertenece al usuario
409INVALID_STATUSEl pago no esta en estado pending
422GATEWAY_ERRORError al comunicar la cancelacion al gateway

POST /portal/pagos/devolver

Solicita la devolucion (reembolso) de un pago aprobado. Solo se pueden devolver pagos con status approved.

Headers: Authorization: Bearer {jwt_token}

Request:

json
{
  "payment_id": "uuid-payment-123"
}
CampoTipoRequeridoDescripcion
payment_idstring (UUID)SiID del portal_payment a devolver

Response 200:

json
{
  "success": true,
  "data": {
    "payment_id": "uuid-payment-123",
    "status": "refunded"
  }
}

Errores:

CodigocodeCausa
404PAYMENT_NOT_FOUNDEl payment_id no existe o no pertenece al usuario
409INVALID_STATUSEl pago no esta en estado approved
422GATEWAY_ERRORError al comunicar la devolucion al gateway

PDF de Cupones

GET /portal/cupones/{id}/pdf

Descarga el PDF de un cupon de pago. El backend actua como proxy: recibe el request del frontend, valida autenticacion y autorizacion, llama internamente al servicio de informes (puerto 9999), y retorna el PDF como stream.

El cliente nunca accede directamente al servicio de informes.

Headers: Authorization: Bearer {jwt_token}

Path Parameters:

ParametroTipoDescripcion
idstringID del cupon

Response 200:

Content-Type: application/pdf
Content-Disposition: attachment; filename="cupon-{id}.pdf"

<binary PDF data>

El frontend recibe el response como blob (responseType: 'blob' en Axios) y triggerea la descarga del archivo.

Errores:

CodigocodeCausa
404CUPON_NOT_FOUNDEl cupon no existe
403FORBIDDENEl cupon no pertenece al usuario autenticado
502INFORMES_SERVICE_ERRORError al comunicarse con el servicio de informes (puerto 9999)

Flujo:

mermaid
sequenceDiagram
    participant F as Frontend
    participant C as CuponController
    participant S as PortalCuponService
    participant I as Informes Service<br/>(puerto 9999)
    participant DB as PostgreSQL

    F->>C: GET /portal/cupones/{id}/pdf<br/>Authorization: Bearer {jwt}
    C->>C: Extraer portal_user_id del JWT
    C->>S: descargarPdf(cuponId, portalUserId)
    S->>DB: Buscar cupon por ID
    alt cupon no encontrado
        S-->>C: Error CUPON_NOT_FOUND
    end
    S->>S: Verificar cupon pertenece al usuario
    alt no pertenece
        S-->>C: Error FORBIDDEN
    end
    S->>I: GET http://localhost:9999/cupon/{id}
    alt servicio no disponible
        S-->>C: Error INFORMES_SERVICE_ERROR
    end
    I-->>S: PDF binary
    S-->>C: PDF stream
    C-->>F: 200 OK<br/>Content-Type: application/pdf<br/>Content-Disposition: attachment

Implementacion backend:

  • El controller recibe el request y valida JWT via middleware
  • El service verifica que el cupon existe y pertenece al usuario autenticado
  • El service realiza un HTTP GET interno a http://localhost:9999/cupon/{id} (servicio de informes)
  • El response del servicio de informes se retransmite como stream al cliente
  • No se almacena el PDF en disco ni en cache: cada request genera un PDF fresco

Implementacion frontend:

typescript
// lib/api/cupones.ts
export async function descargarPdf(cuponId: string): Promise<Blob> {
  const response = await apiClient.get(`/portal/cupones/${cuponId}/pdf`, {
    responseType: 'blob',
  })
  return response.data
}

Cupones

El sub-modulo Cupon delega a los servicios existentes CuponPagoService y CuponValidacionService del modulo CtaCte. NO existe tabla portal_cupones.

POST /portal/cupones/generar

Genera un cupon de pago con codigo de barras ITF. Delega a CuponPagoService existente.

Headers: Authorization: Bearer {jwt_token}

Request:

json
{
  "facturas": [
    {"id": "uuid-1234", "tipo": "Factura A", "numero": 123, "monto": 10000.00}
  ],
  "total": 10000.00,
  "dias_vencimiento": 30
}

No se envia cliente_id en el body. Se obtiene del JWT.

Response 200:

json
{
  "success": true,
  "data": {
    "cupon_id": "uuid-cupon-123",
    "codigo_barras": "0001056789202601274",
    "monto": 10000.00,
    "fecha_vencimiento": "2026-02-27",
    "facturas": [
      {"id": "uuid-1234", "tipo": "Factura A", "numero": 123, "monto": 10000.00}
    ]
  }
}

GET /portal/cupones/mis-cupones

Lista los cupones del usuario autenticado con filtros opcionales.

Headers: Authorization: Bearer {jwt_token}

Query Parameters:

ParametroTipoDefaultDescripcion
estadostring(todos)Filtrar: pending, used, expired, cancelled
limitinteger50Cantidad de registros (max 100)
offsetinteger0Offset para paginacion

Response 200:

json
{
  "success": true,
  "data": {
    "cupones": [
      {
        "cupon_id": "uuid-cupon-123",
        "codigo_barras": "0001056789202601274",
        "monto": 10000.00,
        "estado": "pending",
        "fecha_generacion": "2026-01-27",
        "fecha_vencimiento": "2026-02-27"
      }
    ],
    "total": 1,
    "has_more": false
  }
}

GET /portal/cupones/

Obtiene el detalle de un cupon especifico por su codigo de barras.

Headers: Authorization: Bearer {jwt_token}

Response 200:

json
{
  "success": true,
  "data": {
    "cupon_id": "uuid-cupon-123",
    "codigo_barras": "0001056789202601274",
    "monto": 10000.00,
    "estado": "pending",
    "fecha_generacion": "2026-01-27",
    "fecha_vencimiento": "2026-02-27",
    "facturas": [
      {"id": "uuid-1234", "tipo": "Factura A", "numero": 123, "monto": 10000.00}
    ]
  }
}

Errores:

CodigocodeCausa
404CUPON_NOT_FOUNDCodigo de barras no encontrado
403FORBIDDENEl cupon no pertenece al usuario autenticado