Files
sirpremiumv2/app/Services/PagoValidadorService.php
T
LizandroandClaude Sonnet 4.6 65d4155f02 fix: ya_usado no bloquea a otros usuarios con el mismo monto
Con return inmediato, si Usuario A usaba email_A ($20k), Usuario B
(también $20k) encontraba ese email ya_usado y se bloqueaba sin revisar
el email_B que le pertenecía.

Ahora: ya_usado → continue (sigue iterando); solo retorna ya_usado
al final del loop si todos los emails coincidentes estaban agotados
(caso real de comprobante reutilizado).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-07-19 15:18:08 +00:00

218 lines
9.6 KiB
PHP
Executable File

<?php
namespace App\Services;
use App\Models\ChatConfig;
use App\Models\PagoConfirmado;
use App\Models\WhatsappSystemConfig;
use Illuminate\Support\Facades\Log;
class PagoValidadorService
{
public function validar(array $datosPago, ?int $usuarioId = null, ?string $nombreUsuario = null, string $canal = 'telegram'): array
{
$host = WhatsappSystemConfig::get('correo_imap_host', '');
if (! $host) {
Log::info('[PagoValidador] IMAP sin host configurado');
return ['estado' => 'no_encontrado', 'correo' => null, 'motivo' => 'correo_deshabilitado'];
}
try {
$correos = $this->fetchCorreos();
} catch (\Throwable $e) {
Log::warning('[PagoValidador] Error IMAP: ' . $e->getMessage());
return ['estado' => 'no_encontrado', 'correo' => null, 'motivo' => 'error_imap', 'debug' => $e->getMessage()];
}
Log::info('[PagoValidador] Correos obtenidos: ' . count($correos));
if (empty($correos)) {
return ['estado' => 'no_encontrado', 'correo' => null, 'motivo' => 'sin_correos'];
}
$valorIA = (int) ($datosPago['valor'] ?? 0);
$tolerancia = (int) ChatConfig::get('validacion_monto_tolerancia', '0');
$horaIA = $datosPago['hora'] ?? '';
$fechaIA = $datosPago['fecha'] ?? '';
$llaveIA = strtolower(preg_replace('/\s+/', '', $datosPago['llave'] ?? ''));
Log::info('[PagoValidador] Buscando: valor=' . $valorIA . ' fecha=' . $fechaIA . ' hora=' . $horaIA . ' llave=' . $llaveIA);
$hayYaUsado = false; // email que coincide en todo pero ya fue usado
foreach ($correos as $i => $correo) {
$fuera = $correo['fuera_de_ventana'] ?? false;
$from = $correo['from'] ?? '';
$subj = $correo['subject'] ?? '';
$date = $correo['date'] ?? '';
$body = $correo['body'] ?? '';
Log::info("[PagoValidador] Correo #{$i}: from={$from} | subj={$subj} | date={$date} | fuera_ventana=" . ($fuera ? 'SI' : 'NO') . " | body(200)=" . substr($body, 0, 200));
$datosCorreo = BancolombiaParser::parse($body);
if (! $datosCorreo) {
Log::info("[PagoValidador] Correo #{$i}: NO es Bancolombia (parser retornó null)");
continue;
}
Log::info("[PagoValidador] Correo #{$i} parseado: " . json_encode($datosCorreo));
// ── 1. Valor ────────────────────────────────────────────
// Normalizar: "1,000.00" → 1000 (strip decimals first, then non-digits)
$valorRaw = (string) ($datosCorreo['valor'] ?? '0');
$valorRaw = preg_replace('/[.,]\d{1,2}$/', '', $valorRaw); // quitar decimales
$valorCorreo = (int) preg_replace('/[^0-9]/', '', $valorRaw);
if ($valorIA <= 0 || $valorCorreo <= 0) {
Log::info("[PagoValidador] Correo #{$i}: valor inválido (ia={$valorIA} correo={$valorCorreo})");
continue;
}
if (abs($valorIA - $valorCorreo) > $tolerancia) {
Log::info("[PagoValidador] Correo #{$i}: monto no coincide (ia={$valorIA} correo={$valorCorreo} tol={$tolerancia})");
continue;
}
// ── 2. Fecha (mismo día) — solo si ambos tienen fecha ──
if ($fechaIA && ($datosCorreo['fecha'] ?? '')) {
if (! $this->mismoDia($fechaIA, $datosCorreo['fecha'])) {
Log::info("[PagoValidador] Correo #{$i}: fecha no coincide (ia={$fechaIA} correo={$datosCorreo['fecha']})");
continue;
}
}
// ── 3. Hora (±15 min) — solo si ambos tienen hora ──────
if ($horaIA && ($datosCorreo['hora'] ?? '')) {
if (! $this->horaProxima($horaIA, $datosCorreo['hora'], 15)) {
Log::info("[PagoValidador] Correo #{$i}: hora fuera de rango (ia={$horaIA} correo={$datosCorreo['hora']})");
continue;
}
}
// ── 4. Llave Bancolombia (si la foto la tiene) ──────────
if ($llaveIA) {
$llaveCorreo = strtolower(preg_replace('/\s+/', '', $datosCorreo['llave'] ?? ''));
if ($llaveCorreo && $llaveIA !== $llaveCorreo) {
Log::info("[PagoValidador] Correo #{$i}: llave no coincide (ia={$llaveIA} correo={$llaveCorreo})");
continue;
}
}
// ── 5. Hash único (anti-doble-uso) — solo tras verificar fecha/hora/llave ──
// Si el email coincide en todos los campos pero ya fue usado → es el mismo comprobante
$emailHash = sha1($body . $date);
try {
if (PagoConfirmado::yaUsado($emailHash)) {
Log::info("[PagoValidador] Correo #{$i}: ya usado (hash={$emailHash}), buscando siguiente");
$hayYaUsado = true;
continue; // seguir buscando: otro usuario puede tener un email fresco del mismo monto
}
} catch (\Throwable $e) {
// Falla cerrado: si no se puede verificar el anti-duplicado, no confirmar
Log::error('[PagoValidador] ERROR tabla pagos_confirmados (ejecutar migrate): ' . $e->getMessage());
return ['estado' => 'no_encontrado', 'correo' => null, 'motivo' => 'error_sistema'];
}
// ── 6. Remitente (informativo, no bloquea) ──────────────
$coincideRemitente = $this->remitenteCoincide($datosCorreo['remitente'] ?? '', $nombreUsuario ?? '');
Log::info("[PagoValidador] Correo #{$i}: CONFIRMADO (remitente_coincide=" . ($coincideRemitente ? 'SI' : 'NO') . ")");
try {
PagoConfirmado::marcar($emailHash, [
'usuario_id' => $usuarioId,
'valor' => $valorIA,
'canal' => $canal,
'banco' => $datosCorreo['banco'] ?? null,
'referencia' => $datosPago['referencia'] ?? null,
]);
} catch (\Throwable $e) {
// Falla cerrado: si no se puede registrar el hash, no confirmar
Log::error('[PagoValidador] ERROR al marcar hash (ejecutar migrate): ' . $e->getMessage());
return ['estado' => 'no_encontrado', 'correo' => null, 'motivo' => 'error_sistema'];
}
return [
'estado' => 'confirmado',
'correo' => $datosCorreo,
'coincide_remitente' => $coincideRemitente,
];
}
if ($hayYaUsado) {
Log::info('[PagoValidador] Todos los emails coincidentes ya fueron usados');
return ['estado' => 'ya_usado', 'correo' => null, 'motivo' => 'correo_ya_aplicado'];
}
Log::info('[PagoValidador] Ningún correo coincidió con los criterios');
return ['estado' => 'no_encontrado', 'correo' => null, 'motivo' => 'sin_coincidencia', 'emails_revisados' => count($correos)];
}
// ── Helpers ──────────────────────────────────────────────────────────
private function mismoDia(string $fechaA, string $fechaB): bool
{
$normA = $this->normalizarFecha($fechaA);
$normB = $this->normalizarFecha($fechaB);
return $normA && $normB && $normA === $normB;
}
private function normalizarFecha(string $fecha): ?string
{
if (preg_match('#(\d{1,2})/(\d{1,2})/(\d{2,4})#', $fecha, $m)) {
$y = strlen($m[3]) === 2 ? '20' . $m[3] : $m[3];
return sprintf('%02d/%02d/%s', $m[1], $m[2], $y);
}
return null;
}
private function horaProxima(string $horaA, string $horaB, int $minutos): bool
{
$toMin = fn (string $h): ?int => preg_match('/(\d{1,2}):(\d{2})/', $h, $m)
? (int) $m[1] * 60 + (int) $m[2]
: null;
$a = $toMin($horaA);
$b = $toMin($horaB);
return $a !== null && $b !== null && abs($a - $b) <= $minutos;
}
private function remitenteCoincide(string $remitenteEmail, string $nombreUsuario): bool
{
if (! $remitenteEmail || ! $nombreUsuario) {
return false;
}
$normalizar = fn (string $s): string => strtolower(
iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s) ?: $s
);
$re = $normalizar($remitenteEmail);
$nu = $normalizar($nombreUsuario);
foreach (explode(' ', $nu) as $palabra) {
if (strlen($palabra) >= 3 && str_contains($re, $palabra)) {
return true;
}
}
return false;
}
private function fetchCorreos(): array
{
$minutos = (int) WhatsappSystemConfig::get('correo_imap_minutos', '120');
$service = new CorreoImapService(
host: WhatsappSystemConfig::get('correo_imap_host', ''),
port: (int) WhatsappSystemConfig::get('correo_imap_port', '993'),
useSsl: WhatsappSystemConfig::get('correo_imap_ssl', '1') === '1',
user: WhatsappSystemConfig::get('correo_imap_user', ''),
password: WhatsappSystemConfig::get('correo_imap_password', ''),
folder: WhatsappSystemConfig::get('correo_imap_folder', 'INBOX'),
);
return $service->fetchLatest(limit: 30, minutesBack: $minutos);
}
}