Appearance
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 enordctadel 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
- El cliente tiene un pago aprobado con recibo disponible (
recibo_idno nulo) - Hace click en "Descargar recibo" en la vista
PagoResultadoo en el historial - El frontend llama a
GET /backend/portal/pagos/{id}/recibocon el JWT del cliente - El backend valida el JWT y extrae
ordcon_id,sucursal_idytenant_id - El backend busca el pago en
portal_paymentsfiltrando poridyordcon_id(ownership check) - Si el pago tiene
status === 'approved'yrecibo_idno nulo, el backend cambia elsearch_pathasuc{sucursal_id}y consultaordcta+recfacpara obtener los datos del recibo - El backend resuelve la base de datos del tenant vía
ini.sistemay emite un JWT interno (s2s) - El backend hace un POST al servicio Informes (puerto 9999) con el case
"portal-recibo"y los datos del recibo - El servicio Informes genera el PDF usando
cargrecib_template.phpy lo retorna como binario - El backend streamea el PDF al cliente con
Content-Type: application/pdfyContent-Disposition: inline; filename="recibo-{numero}.pdf" - 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
endCasos de Error
| Código HTTP | Error Code | Causa | Comportamiento en UI |
|---|---|---|---|
| 404 | PAYMENT_NOT_FOUND | No existe portal_payments con el ID dado | Error genérico |
| 403 | FORBIDDEN | El pago existe pero pertenece a otro cliente (ordcon_id no coincide con JWT) | Error genérico |
| 409 | RECIBO_PENDIENTE | El pago está aprobado pero recibo_id es NULL (TX2 no completó) | Mensaje inline "Recibo en proceso, intentá nuevamente en unos minutos" — sin toast |
| 422 | RECIBO_NO_DISPONIBLE | El pago existe pero no está en estado approved | No aplica desde UI (botón solo aparece para approved) |
| 502 | INFORMES_ERROR | El servicio Informes no está disponible, timeout, o retorna error | Mensaje de error genérico |
| 401 | UNAUTHORIZED | JWT ausente o inválido | Redirige al login |
Fuentes de Datos
portal_payments (schema público — nivel empresa)
Campos relevantes para este proceso:
| Campo | Uso |
|---|---|
id | Identifica el pago solicitado (path param) |
ordcon_id | Ownership check contra el JWT |
status | Debe ser 'approved' para continuar |
recibo_id | UUID 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.
| Columna | Uso |
|---|---|
id | UUID — coincide con portal_payments.recibo_id |
nrocomp | Número del recibo → usado en filename="recibo-{nrocomp}.pdf" y en el PDF |
fecha | Fecha del recibo |
haber | Monto total del recibo |
cnro | ID del cliente (ordcon_id) — para validación cruzada |
comprobant | Nombre del comprobante |
recfac (schema sucursal — suc{sucursal_id})
Filas asociadas a ordcta.id que representan las facturas incluidas en el recibo.
| Columna | Uso |
|---|---|
id_recibo | FK → ordcta.id (NOT recibo_id) |
id_comprobante | UUID de la factura/comprobante pagada |
monent | Monto pagado por esta factura (NOT monto) |
saldo | Saldo anterior de la factura (NOT saldo_anterior) |
Nota: Los nombres de columna de
recfacdifieren del spec original. Los nombres correctos están verificados contra la migración20240823200749_new_table_recfac.php.
ordcon (schema sucursal — suc{sucursal_id})
Consultado por el servicio Informes para poblar los datos del cliente en el PDF.
| Columna | Uso |
|---|---|
id | FK desde ordcta.cnro |
cnom | Nombre del cliente |
cdom | Domicilio |
ccui | CUIT |
condicion_iva | Condición IVA |
Seguridad
- El
ordcon_idysucursal_idse 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 unid - El
sucursal_iddel JWT determina el schema a consultar enordcta— si el JWT es de una sucursal distinta, el lookup retorna vacío y el backend responde409 RECIBO_PENDIENTE(graceful degradation) - El PDF no se cachea:
Cache-Control: no-storeen 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
- Auto-Reconciliación — Proceso — TX2 que genera
recibo_id - Auto-Reconciliación — Técnico —
PortalReciboCreatorService - Descarga de Recibos — Vista — descripción desde la perspectiva del cliente
- Endpoints API — contrato completo del endpoint