confirmado|no_encontrado|monto_incorrecto|ya_usado, 'correo' => array|null] */ public function validar(array $datosPago, ?int $usuarioId = null, ?string $nombreUsuario = null, string $canal = 'telegram'): array { if (WhatsappSystemConfig::get('correo_imap_enabled', '0') !== '1') { return ['estado' => 'no_encontrado', 'correo' => null, 'motivo' => 'correo_deshabilitado']; } try { $correos = $this->fetchCorreos(); } catch (\Throwable $e) { Log::warning('[PagoValidador] Error leyendo correos: ' . $e->getMessage()); return ['estado' => 'no_encontrado', 'correo' => null, 'motivo' => 'error_imap']; } 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'] ?? '')); foreach ($correos as $correo) { $datosCorreo = BancolombiaParser::parse($correo['body'] ?? ''); if (! $datosCorreo) { continue; } // ── 1. Valor ──────────────────────────────────────────── $valorCorreo = (int) preg_replace('/[^0-9]/', '', (string) ($datosCorreo['valor'] ?? '0')); if ($valorIA <= 0 || $valorCorreo <= 0 || abs($valorIA - $valorCorreo) > $tolerancia) { if ($valorCorreo > 0 && $valorIA > 0 && abs($valorIA - $valorCorreo) > $tolerancia) { // Monto distinto — seguir buscando en el siguiente correo continue; } if ($valorIA <= 0 || $valorCorreo <= 0) { continue; } } // ── 2. Hash único del correo (anti-doble-uso) ─────────── $emailHash = sha1(($correo['body'] ?? '') . ($correo['date'] ?? '')); if (PagoConfirmado::yaUsado($emailHash)) { // Este correo ya confirmó a otro usuario return ['estado' => 'ya_usado', 'correo' => $datosCorreo, 'motivo' => 'correo_ya_aplicado']; } // ── 3. Fecha (mismo día) ──────────────────────────────── if ($fechaIA && ($datosCorreo['fecha'] ?? '')) { if (! $this->mismoDia($fechaIA, $datosCorreo['fecha'])) { continue; } } // ── 4. Hora (±10 min) ─────────────────────────────────── if ($horaIA && ($datosCorreo['hora'] ?? '')) { if (! $this->horaProxima($horaIA, $datosCorreo['hora'], 10)) { continue; } } // ── 5. Llave Bancolombia (si la foto la tiene) ────────── if ($llaveIA) { $llaveCorreo = strtolower(preg_replace('/\s+/', '', $datosCorreo['llave'] ?? '')); if ($llaveCorreo && $llaveIA !== $llaveCorreo) { continue; } } // ── 6. Remitente (coincidencia parcial con nombre de usuario) ── $coincideRemitente = $this->remitenteCoincide( $datosCorreo['remitente'] ?? '', $nombreUsuario ?? '' ); // Todos los filtros pasados → confirmar PagoConfirmado::marcar($emailHash, [ 'usuario_id' => $usuarioId, 'valor' => $valorIA, 'canal' => $canal, 'banco' => $datosCorreo['banco'] ?? null, 'referencia' => $datosPago['referencia'] ?? null, ]); return [ 'estado' => 'confirmado', 'correo' => $datosCorreo, 'coincide_remitente' => $coincideRemitente, ]; } // Si algún correo tenía el monto correcto pero otro campo falló, devolver monto_incorrecto // de lo contrario no_encontrado return ['estado' => 'no_encontrado', 'correo' => null, 'motivo' => 'sin_coincidencia']; } // ── Helpers ────────────────────────────────────────────────────────── private function mismoDia(string $fechaA, string $fechaB): bool { // Normaliza dd/mm/yyyy o dd/mm/yy $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; } // Comparación parcial insensible a mayúsculas y tildes $normalizar = fn (string $s): string => strtolower( iconv('UTF-8', 'ASCII//TRANSLIT//IGNORE', $s) ?: $s ); $re = $normalizar($remitenteEmail); $nu = $normalizar($nombreUsuario); // Basta con que una palabra del nombre de usuario aparezca en el remitente del email 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', '5'); $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: 10, minutesBack: $minutos); } }