Appearance
Bloqueo de Repago por Estado — Portal de Clientes
Módulo: Portal de Clientes Tipo: Process Estado: Implementado Fecha: 2026-05-27
Descripción
El portal bloquea el inicio de un nuevo pago si el cliente ya tiene un pago en estado no-terminal para la misma combinación (ordcon_id, sucursal_id). El bloqueo es permanente — no existe ventana de tiempo (TTL) — y se aplica tanto en el backend como en el frontend de forma proactiva.
Estados bloqueantes vs. no bloqueantes
| Estado | recibo_id | ¿Bloquea? |
|---|---|---|
pending | cualquiera | ✅ Sí |
issued | cualquiera | ✅ Sí |
deferred | cualquiera | ✅ Sí |
objected | cualquiera | ✅ Sí |
review | cualquiera | ✅ Sí |
validate | cualquiera | ✅ Sí |
approved | NULL | ✅ Sí (recibo pendiente) |
approved | NOT NULL | ❌ No (ciclo completo) |
cancelled | cualquiera | ❌ No |
rejected | cualquiera | ❌ No |
refunded | cualquiera | ❌ No |
Flujo backend
El método PortalPaymentRepository::findBlockingPaymentByOrdconAndSucursal() ejecuta una única query que cubre todos los estados bloqueantes sin restricción de tiempo:
sql
SELECT * FROM portal_payments
WHERE ordcon_id = :ordcon_id
AND sucursal_id = :sucursal_id
AND (
status IN ('pending','issued','deferred','objected','review','validate')
OR (status = 'approved' AND recibo_id IS NULL)
)
ORDER BY created_at DESC
LIMIT 1Si esta query retorna un resultado, PortalPaymentService::iniciar() lanza ActivePaymentExistsException → HTTP 409 ACTIVE_PAYMENT_EXISTS.
Endpoint de pre-check
GET /backend/portal/account/pago-activo?sucursal_id={id}
- Autenticado con JWT del portal (el
ordcon_idse extrae del token, no de la query) - Sin JWT válido → 401
- Sin
sucursal_iden query → 400
Respuesta cuando no hay pago activo
json
{ "activo": false }Respuesta cuando hay pago activo
json
{
"activo": true,
"payment_id": "uuid-del-pago",
"estado": "pending",
"created_at": "2026-05-27T10:00:00+00:00"
}Flujo frontend (PagarView)
Al montar PagarView, el hook usePagoActivo llama al endpoint de pre-check con staleTime: 10s (sin polling). Si activo: true:
- Se muestra un banner no-dismissible en la parte superior de la vista
- El botón Pagar queda deshabilitado
Texto del banner:
"Tenés un pago en proceso. Si necesitás asistencia, contactá a soporte."
El cliente no puede cancelar pagos desde el portal. La resolución de pagos colgados (por ejemplo, si el gateway no notifica el resultado) es responsabilidad del operador desde el backoffice.
Limitaciones conocidas
- Sin cancelación para el usuario: si un pago queda en estado no-terminal indefinidamente (gateway sin respuesta), el cliente queda bloqueado hasta que un operador resuelva el pago en el backoffice.
- Herramienta de backoffice: la funcionalidad para cancelar pagos del portal desde el backoffice está pendiente de implementación (scope separado).
Archivos relevantes
| Archivo | Rol |
|---|---|
bautista-backend/Modules/Portal/Infrastructure/Persistence/Repositories/PortalPaymentRepository.php | findBlockingPaymentByOrdconAndSucursal() |
bautista-backend/Modules/Portal/Application/Payment/Services/PortalPaymentService.php | Guard en iniciar() + getPagoActivo() |
bautista-backend/Modules/Portal/Presentation/Account/Controllers/PortalAccountController.php | pagoActivo() action |
portal-usuarios/src/features/pagos/hooks/usePagoActivo.ts | Hook de pre-check |
portal-usuarios/src/views/PagarView.tsx | Banner + botón bloqueado |