usuarioId); if (! $solicitud || $solicitud->id !== $this->solicitudId) { Log::info("[ValidarPagoWebJob] conv={$this->convId}: la solicitud ya no está pendiente, abortando."); $this->resolver('cancelado'); return; } // ── Paso 1: leer el comprobante con Gemini (solo en el primer intento) if ($this->datosPago === null) { $datos = $this->leerComprobante(); if ($datos === null) { return; // leerComprobante() ya avisó al usuario y cerró el flujo } $valorIA = (int) ($datos['valor'] ?? 0); if ($valorIA !== $solicitud->monto) { $this->bot( "⚠️ El monto del comprobante ($" . number_format($valorIA) . ") no coincide " . "con tu solicitud de recarga ($" . number_format($solicitud->monto) . ")." ); $this->resolver('monto_incorrecto'); return; } $this->datosPago = $datos; $this->bot("📧 Datos leídos correctamente. Ahora estoy buscando tu pago en el banco..."); } // ── Paso 2: buscar el pago en el correo $resultado = app(PagoValidadorService::class)->validar( $this->datosPago, $this->usuarioId, $this->nombreRemitente, 'web' ); $motivo = $resultado['motivo'] ?? ''; if ($resultado['estado'] === 'confirmado') { // El saldo se acredita aquí y no en el componente: si el usuario // cierra o recarga la pestaña mientras validamos, el dinero tiene // que quedar abonado igual. El componente solo se encarga después // de completar la compra que hubiera quedado pendiente. $this->aplicarRecarga($solicitud); $this->resolver('confirmado', ['monto' => $solicitud->monto]); return; } if ($resultado['estado'] === 'ya_usado') { $this->bot("⛔ Comprobante ya utilizado. Este pago ya fue registrado anteriormente."); $this->resolver('ya_usado'); return; } if ($motivo === 'sin_coincidencia' || $motivo === 'sin_correos') { $this->reintentarOAgotar(); return; } $this->bot("⚠️ No confirmado: " . match ($motivo) { 'correo_deshabilitado' => 'la verificación por correo no está configurada', 'error_imap' => 'no pudimos leer el correo del banco', 'error_sistema' => 'error interno, intenta de nuevo en unos minutos', default => 'no se pudo verificar automáticamente', }); $this->resolver('error'); } /** * El pago aún no aparece en el correo: reintentar con espera o rendirse. * El banco puede tardar un par de minutos en enviar la notificación. */ private function reintentarOAgotar(): void { if ($this->intento === 1) { $this->bot( "⏳ Estamos validando tu pago, esto puede tardar unos minutos.\n" . "Te confirmo por aquí en cuanto lo encuentre." ); } if ($this->intento < self::MAX_INTENTOS) { self::dispatch( $this->convId, $this->usuarioId, $this->solicitudId, $this->comprobantePath, $this->nombreRemitente, $this->datosPago, $this->intento + 1, )->delay(now()->addSeconds(20)); return; } ChatMessage::create([ 'conversation_id' => $this->convId, 'tipo' => 'bot', 'tipo_ui' => 'validacion', 'contenido' => 'No pudimos confirmar tu pago automáticamente.', 'payload' => [ 'estado' => 'pendiente', 'motivo' => 'No encontramos el pago en el correo del banco.', 'ia_datos' => $this->datosPago, 'botones' => [ ['label' => '🔄 Reintentar', 'action' => 'pago.reintentar', 'data' => []], ['label' => '← Menú principal', 'action' => 'menu.principal', 'data' => []], ], ], 'leido' => true, ]); $this->resolver('agotado', ['datosPago' => $this->datosPago]); } /** Acredita el saldo, deja registro de la recarga y cierra la solicitud. */ private function aplicarRecarga(SolicitudRecarga $solicitud): void { $monto = (int) $solicitud->monto; DB::transaction(function () use ($solicitud, $monto) { $user = User::with('saldo')->find($this->usuarioId); $saldo = $user->saldo ?? Saldo::create(['usuario_id' => $this->usuarioId, 'valor' => 0]); $saldo->update(['valor' => $saldo->valor + $monto]); Recarga::create([ 'usuario_id' => $this->usuarioId, 'saldo_id' => $saldo->id, 'monto' => $monto, 'valor_recarga' => $monto, 'status' => 'Confirmado', 'reference' => 'breb-chat-' . $solicitud->id, ]); $solicitud->confirmar(); $this->bot( "✅ ¡Tu recarga de $" . number_format($monto) . " fue aplicada!\n" . "Tu nuevo saldo es $" . number_format($saldo->valor) . "." ); }); } /** * Lee el comprobante con Gemini. Devuelve null (y avisa al usuario) si no * se pudo interpretar. */ private function leerComprobante(): ?array { $ruta = storage_path('app/' . $this->comprobantePath); if (! is_file($ruta)) { Log::warning("[ValidarPagoWebJob] conv={$this->convId}: no existe {$ruta}"); $this->bot("No pude recuperar la imagen del comprobante. Envíala de nuevo, por favor."); $this->resolver('error'); return null; } $datos = app(GeminiVisionService::class)->extraerPago( base64_encode(file_get_contents($ruta)), mime_content_type($ruta) ?: 'image/jpeg', $this->usuarioId, 'web' ); if (! $datos || isset($datos['__error']) || isset($datos['__parse_error'])) { Log::warning('[ValidarPagoWebJob] Gemini: ' . ($datos['__error'] ?? $datos['__parse_error'] ?? 'sin respuesta')); $this->bot("No pude leer el comprobante. Intenta con una foto más clara o con mejor iluminación."); $this->resolver('error'); return null; } return $datos; } /** Si el job muere (excepción o timeout) el chat no puede quedarse colgado. */ public function failed(?\Throwable $e): void { Log::error('[ValidarPagoWebJob] falló: ' . ($e?->getMessage() ?? 'sin detalle')); $this->bot("Tuvimos un problema validando tu comprobante. Intenta enviarlo de nuevo o pide un asesor."); $this->resolver('error'); } private function bot(string $contenido): void { ChatMessage::create([ 'conversation_id' => $this->convId, 'tipo' => 'bot', 'tipo_ui' => 'text', 'contenido' => $contenido, 'leido' => true, ]); } private function resolver(string $estado, array $extra = []): void { Cache::put( self::claveResultado($this->convId, $this->solicitudId), array_merge(['estado' => $estado], $extra), self::RESULTADO_TTL ); } }