Skip to content

Proceso de Descarga de Recibo

Módulo: Portal de Clientes Tipo: Process Estado: Implementado Fecha: 2026-05-14


Descripción

Cuando un cliente realiza un pago online y este es aprobado por el gateway, el sistema genera automáticamente un recibo en la cuenta corriente (TX2 de auto-reconciliación). Una vez disponible, el cliente puede descargar ese recibo como PDF desde el portal sin intervención de un operador.

Este proceso describe el flujo completo desde que el cliente hace click en "Descargar recibo" hasta que el PDF se abre en el navegador.


Contexto

El campo portal_payments.recibo_id es la única fuente de verdad para determinar si el recibo está disponible:

  • recibo_id IS NULL: TX2 no completó (reconciliación pendiente o en proceso)
  • recibo_id IS NOT NULL: el recibo existe en ordcta del schema sucursal, y puede descargarse

El campo es escrito exclusivamente por PortalReciboCreatorService como parte del proceso de auto-reconciliación (TX2). Este proceso nunca escribe recibo_id.


Flujo End-to-End

Narrativa

  1. El cliente tiene un pago aprobado con recibo disponible (recibo_id no nulo)
  2. Hace click en "Descargar recibo" en la vista PagoResultado o en el historial
  3. El frontend llama a GET /backend/portal/pagos/{id}/recibo con el JWT del cliente
  4. El backend valida el JWT y extrae ordcon_id, sucursal_id y tenant_id
  5. El backend busca el pago en portal_payments filtrando por id y ordcon_id (ownership check)
  6. Si el pago tiene status === 'approved' y recibo_id no nulo, el backend cambia el search_path a suc{sucursal_id} y consulta ordcta + recfac para obtener los datos del recibo
  7. El backend resuelve la base de datos del tenant vía ini.sistema y emite un JWT interno (s2s)
  8. El backend hace un POST al servicio Informes (puerto 9999) con el case "portal-recibo" y los datos del recibo
  9. El servicio Informes genera el PDF usando cargrecib_template.php y lo retorna como binario
  10. El backend streamea el PDF al cliente con Content-Type: application/pdf y Content-Disposition: inline; filename="recibo-{numero}.pdf"
  11. El frontend recibe el Blob, crea un object URL y lo abre en una nueva pestaña con window.open

Diagrama de Secuencia

mermaid
sequenceDiagram
    autonumber
    actor U as Cliente (browser)
    participant P as Portal SPA
    participant B as bautista-backend
    participant DB as PostgreSQL
    participant I as Informes (port 9999)

    U->>P: Click "Descargar recibo"
    P->>B: GET /backend/portal/pagos/{id}/recibo<br/>Authorization: Bearer JWT
    B->>B: PortalJwtMiddleware → JwtClaims<br/>(ordcon_id, sucursal_id, tenant_id)
    B->>DB: SELECT portal_payments WHERE id=:id AND ordcon_id=:ordcon (public)
    alt no row
        B-->>P: 404 PAYMENT_NOT_FOUND
    else status != approved
        B-->>P: 422 RECIBO_NO_DISPONIBLE
    else recibo_id IS NULL
        B-->>P: 409 RECIBO_PENDIENTE
    end
    B->>DB: setSearchPath(suc{sucursal_id}, public)
    B->>DB: SELECT ordcta WHERE id=:recibo_id<br/>JOIN recfac → comprobantes (suc schema)
    B->>B: IniSistemaRepo.findDatabaseByTenantId → db<br/>InternalJwtIssuer.issue(db, schema) → s2s JWT
    B->>I: POST URL_INFORMES<br/>{codReporte:"portal-recibo", db, schema,<br/> total, cliente, comprobantes, fecha, numero, ...}<br/>X-Schema, Authorization: Bearer s2s
    alt Informes OK
        I-->>B: 200 application/pdf (binary)
        B-->>P: 200 application/pdf<br/>Content-Disposition: inline; filename="recibo-{numero}.pdf"<br/>Cache-Control: no-store
        P->>U: window.open(blobUrl)
    else Informes error / timeout
        I-->>B: 4xx/5xx / GuzzleException
        B-->>P: 502 INFORMES_ERROR
    end

Casos de Error

Código HTTPError CodeCausaComportamiento en UI
404PAYMENT_NOT_FOUNDNo existe portal_payments con el ID dadoError genérico
403FORBIDDENEl pago existe pero pertenece a otro cliente (ordcon_id no coincide con JWT)Error genérico
409RECIBO_PENDIENTEEl pago está aprobado pero recibo_id es NULL (TX2 no completó)Mensaje inline "Recibo en proceso, intentá nuevamente en unos minutos" — sin toast
422RECIBO_NO_DISPONIBLEEl pago existe pero no está en estado approvedNo aplica desde UI (botón solo aparece para approved)
502INFORMES_ERROREl servicio Informes no está disponible, timeout, o retorna errorMensaje de error genérico
401UNAUTHORIZEDJWT ausente o inválidoRedirige al login

Fuentes de Datos

portal_payments (schema público — nivel empresa)

Campos relevantes para este proceso:

CampoUso
idIdentifica el pago solicitado (path param)
ordcon_idOwnership check contra el JWT
statusDebe ser 'approved' para continuar
recibo_idUUID del ordcta generado por TX2; NULL si pendiente

ordcta (schema sucursal — suc{sucursal_id})

Accedido mediante search_path = suc{sucursal_id}, public sin cualificar los nombres de tabla.

ColumnaUso
idUUID — coincide con portal_payments.recibo_id
nrocompNúmero del recibo → usado en filename="recibo-{nrocomp}.pdf" y en el PDF
fechaFecha del recibo
haberMonto total del recibo
cnroID del cliente (ordcon_id) — para validación cruzada
comprobantNombre del comprobante

recfac (schema sucursal — suc{sucursal_id})

Filas asociadas a ordcta.id que representan las facturas incluidas en el recibo.

ColumnaUso
id_reciboFK → ordcta.id (NOT recibo_id)
id_comprobanteUUID de la factura/comprobante pagada
monentMonto pagado por esta factura (NOT monto)
saldoSaldo anterior de la factura (NOT saldo_anterior)

Nota: Los nombres de columna de recfac difieren del spec original. Los nombres correctos están verificados contra la migración 20240823200749_new_table_recfac.php.

ordcon (schema sucursal — suc{sucursal_id})

Consultado por el servicio Informes para poblar los datos del cliente en el PDF.

ColumnaUso
idFK desde ordcta.cnro
cnomNombre del cliente
cdomDomicilio
ccuiCUIT
condicion_ivaCondición IVA

Seguridad

  • El ordcon_id y sucursal_id se extraen siempre del JWT, nunca del body o query params del request
  • El ownership check usa findByIdForOrdcon($paymentId, $ordconId) que filtra por ambos campos en un único SELECT — no es posible acceder al recibo de otro cliente adivinando un id
  • El sucursal_id del JWT determina el schema a consultar en ordcta — si el JWT es de una sucursal distinta, el lookup retorna vacío y el backend responde 409 RECIBO_PENDIENTE (graceful degradation)
  • El PDF no se cachea: Cache-Control: no-store en cada respuesta; cada request genera un PDF fresco desde Informes
  • La comunicación backend → Informes usa un JWT interno (s2s) firmado con clave RSA privada

Dependencias