Fix: Asignación de PDV al resolver (no al ingresar) + verificación de concurrencia
Date: 2026-07-25 Status: 🔴 Crítico — bloquea el flujo core de M1 tal como está implementado Referencia funcional: doc/functional/2026-07-24-soterex-integracion-functional-analysis.md, sección 2.4 (nueva)
El problema
M1 ya está implementado con un supuesto incorrecto: que la transacción trae o recibe un PDV al momento de ingresar (SendRequest/seed). El PRD de M1 (doc/plans/2026-07-25-plan-desarrollo-modulos.md) incluso tiene como criterio de aceptación:
"Dado Backoffice acotado a GT-CAP-001, el listado no muestra transacciones de otros PDVs ni sin PDV..."
Esto es el error exacto al revés: si las transacciones entran correctamente sin PDV (porque Soterex no lo informa y es imposible saberlo a esa altura), esa regla hace que ningún Backoffice vea nunca una transacción pendiente — bloquea el flujo core del negocio por completo.
La regla correcta (confirmada por el cliente, 2026-07-25)
- Una transacción
ACCEPTEDrecién ingresada no tiene PDV — solo país. - Todas las
ACCEPTEDde un país son una bolsa compartida: cualquier Backoffice/Supervisor con acceso a ese país las ve y puede tomarlas, sin importar a qué PDV esté asignado. - El PDV se graba recién cuando alguien la resuelve (
PAIDoCANCELLED) — junto con el usuario y la fecha/hora. Antes de eso, es indefinible. - El acotamiento por PDV de la matriz de permisos aplica a transacciones ya resueltas (reporting/auditoría), no a las pendientes.
- Requisito reforzado: como varios cajeros de distintos PDVs comparten la misma bolsa, hay que re-verificar el estado de la transacción justo antes de confirmar el cambio (lock a nivel de fila, no un chequeo que se hizo cuando se abrió la pantalla) — si otro usuario ya la resolvió un instante antes, rechazar con un mensaje claro y refrescar el listado. Esto no es un caso raro a cubrir "por las dudas" — es esperable que pase seguido.
Qué hay que cambiar concretamente
Backend
- [ ] Ingesta (
SendRequestCron/seed/mock actual): sacar cualquier asignación destation_iden este paso. La transacción se crea constation_id = null, solocountry_id. - [ ]
GET /api/transacciones: corregir el scoping por permisos — - Parastatus = ACCEPTED: visible para cualquier usuario con rol en ese país (Backoffice o Supervisor), sin filtrar por PDV ni excluir las que tienenstation_id = null(van a ser todas, y está bien). - Parastatus IN (PAID, CANCELLED): acá sí aplica el acotamiento por PDV como ya estaba pensado (para reporting/auditoría por PDV). - [ ]
PATCH /api/transacciones/{id}/payy.../cancel: agregar el re-chequeo con lock pesimista (SELECT ... FOR UPDATEo equivalente en Eloquent) inmediatamente antes de escribir el nuevo estado — no alcanza con haber leído el estado al armar la respuesta delGETanterior. Si la fila ya no está enACCEPTEDal tomar el lock, devolver 409 con un mensaje claro ("Esta transacción ya fue procesada por otro usuario") y no tocar nada más (ni prefondeo, ni notificación a Soterex). (Nota: el PRD de M1 menciona que esto ya se contempló para el caso "dospayconcurrentes sobre la misma transacción" — verificar que la implementación real todavía lo cubre después de sacar el filtro por PDV, porque ahora la probabilidad de colisión sube.) - [x] En el momento de resolver exitosamente (
payocancel): grabarstation_id= PDV activo del usuario que resuelve +resolved_by_user_id+ timestamp (last_updated_at), además del audit logTRANSACTION_STATUS_CHANGEque ya existe. Implementado. - [x] Pregunta #14 — resuelta con "PDV activo" explícito (2026-07-25, a pedido del cliente): se agregó
role_assignments.default_station_id— no cambia el alcance de lectura (Admin sigue viendo todo el país sin restricción), pero es el PDV que se graba al resolver cuando no hay un único PDV obvio en el alcance del usuario. Admin quedó configurado conGT-CAP-001(Ciudad de Guatemala — Central) — encontrado en uso real: el cliente pagó el MTCN0000000001como Admin y no le asignó PDV (comportamiento documentado hasta ese momento), pidió que sí lo hiciera. Prioridad enresolveStationForResolution(): (1)default_station_idsi está seteado, (2) si no, único PDV del alcance, (3) si no, null. Supervisor/Backoffice con 2+ PDVs y sindefault_station_idsiguen cayendo en null — no pedido todavía, mismo mecanismo ya disponible para resolverlo cuando haga falta.
Frontend
- [ ] Listado de Transacciones: cuando el filtro de estado sea
ACCEPTED, no mostrar/aplicar filtro por PDV (no tiene sentido, ninguna lo tiene todavía). Cuando seaPAID/CANCELLED, sí mostrar el filtro por PDV como ya estaba. - [ ] Manejar el 409 de
pay/cancelcon un mensaje claro para el usuario (no un error genérico) y refrescar el listado automáticamente para que desaparezca la transacción ya tomada por otro usuario.
Tests
- [ ] Test de feature: dos requests concurrentes de
paysobre la misma transacción desde "usuarios" distintos (o PDVs distintos) — uno debe ganar (200) y el otro debe recibir 409, sin que el saldo de prefondeo se descuente doble. - [ ] Test de feature: un Backoffice sin PDV coincidente con ninguna transacción existente igual puede ver y pagar transacciones
ACCEPTEDde su país (esto es lo que estaba roto). - [ ] Test de feature: una vez pagada, la transacción sí queda filtrable por el PDV correcto (el del usuario que la resolvió).
Impacto en la documentación
Ya corregido en esta misma entrega — ver doc/functional/2026-07-24-soterex-integracion-functional-analysis.md:
- Sección 2.4 (nueva): la regla completa.
- Secciones 3.2, 3.3, 4.1, 4.2, 4.3: flujos y diagramas actualizados con el re-chequeo y la asignación de PDV en el momento correcto.
- Sección 5: secuencia de pantalla de Transacciones aclarada (filtro por PDV solo aplica a resueltas).
- Sección 6: nueva pregunta abierta #14 (qué PDV grabar si el usuario tiene más de uno asignado).
Por qué es crítico y va primero
Esto bloquea el flujo core que M1 ya "terminó" — sin este fix, en la práctica ningún Backoffice puede operar transacciones reales apenas haya más de un PDV en juego. Se resuelve antes de seguir con M2/M3/M4, aunque haya trabajo de esos módulos en curso.

