fix: mover la validación de pagos del chat web a una cola

El upload del comprobante hacía todo dentro de un solo request de Livewire:
Gemini (hasta 80s) más la lectura IMAP (20-60s) superaban el límite del
gateway y el usuario recibía un 504. Además, con SESSION_DRIVER=file ese
request mantenía bloqueado el archivo de sesión, así que el wire:poll del
chat no podía avanzar: la conversación entera se congelaba y el mensaje
"Analizando tu comprobante" se quedaba pegado para siempre.

Ahora updatedFotoComprobante() solo guarda la imagen, escribe el primer
mensaje y despacha ValidarPagoWebJob. Termina en ~1s.

El job hace el trabajo pesado fuera del request y va escribiendo su avance
como mensajes normales del chat, así el usuario ve los pasos:
  "Recibido, leyendo los datos..." -> "Datos leídos, buscando en el banco..."
  -> resultado
Reintenta cada 20s hasta 9 veces, igual que el flujo de Telegram.

El saldo se acredita dentro del job, no en el componente: si el usuario
cerraba o recargaba la pestaña durante la validación, el pago se confirmaba
pero el dinero nunca se abonaba. El componente ahora solo refresca el saldo
en pantalla y remata la compra que hubiera quedado pendiente.

Otros cierres del flujo que faltaban:
- failed() escribe un mensaje si el job muere, para no dejar el chat colgado.
- revisarPago() se rinde a los 6 min si el worker está caído.
- Los estados terminales dejan siempre un botón de menú.
- El botón Reintentar reencola en vez de validar dentro del request.

No se toca Telegram ni ningún servicio compartido (GeminiVisionService,
PagoValidadorService, CorreoImapService, ValidarPagoTelegramJob).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro
2026-08-01 14:12:57 +00:00
co-authored by Claude Sonnet 4.6
parent f4f689891c
commit 281c9d5b02
3 changed files with 401 additions and 198 deletions
@@ -263,13 +263,13 @@
</button>
</div>
{{-- Reintento automático de validación de pago.
Va en su propio elemento: Livewire v2 solo respeta el PRIMER
wire:poll de un mismo nodo, así que compartirlo con el poll de
mensajes hacía que este nunca se ejecutara. Solo se renderiza
mientras hay un pago pendiente, así no gasta requests. --}}
@if ($flujo === 'pago.pendiente')
<div wire:poll.20000ms="autoReintentarPago" class="w-0 h-0 overflow-hidden" aria-hidden="true"></div>
{{-- Mientras ValidarPagoWebJob trabaja, este poll solo pregunta si ya
hay desenlace (lectura de caché, sin llamadas externas).
Va en su propio elemento porque Livewire v2 solo respeta el
PRIMER wire:poll de un mismo nodo: compartirlo con el poll de
mensajes hacía que uno de los dos nunca se ejecutara. --}}
@if ($flujo === 'pago.procesando')
<div wire:poll.2000ms="revisarPago" class="w-0 h-0 overflow-hidden" aria-hidden="true"></div>
@endif
{{-- Mensajes --}}
@@ -673,16 +673,30 @@
</div>
</template>
{{-- "Escribiendo…" mientras el bot procesa cualquier acción --}}
<div wire:loading.flex wire:target="enviar, clickBoton, fotoComprobante" class="justify-start msg-in">
<div class="bg-[#202c33] rounded-xl rounded-tl-sm px-4 py-3 shadow bubble-in">
<div class="flex items-center gap-1">
<span class="typing-dot"></span>
<span class="typing-dot"></span>
<span class="typing-dot"></span>
{{-- "Escribiendo…". Mientras el job valida el pago se queda fijo,
porque el trabajo ocurre fuera del request y wire:loading no
lo cubriría. --}}
@if ($flujo === 'pago.procesando')
<div class="flex justify-start msg-in">
<div class="bg-[#202c33] rounded-xl rounded-tl-sm px-4 py-3 shadow bubble-in">
<div class="flex items-center gap-1">
<span class="typing-dot"></span>
<span class="typing-dot"></span>
<span class="typing-dot"></span>
</div>
</div>
</div>
</div>
@else
<div wire:loading.flex wire:target="enviar, clickBoton, fotoComprobante" class="justify-start msg-in">
<div class="bg-[#202c33] rounded-xl rounded-tl-sm px-4 py-3 shadow bubble-in">
<div class="flex items-center gap-1">
<span class="typing-dot"></span>
<span class="typing-dot"></span>
<span class="typing-dot"></span>
</div>
</div>
</div>
@endif
<div id="pub-messages-end"></div>
</div>
@@ -722,7 +736,7 @@
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8z"></path>
</svg>
Analizando comprobante con IA...
Enviando comprobante...
</div>
</div>
<div class="flex items-end gap-2 px-3 py-2.5">