fix(pago): diagnose and harden IMAP email validation

- PagoValidadorService: add detailed Log::info at every step (emails count,
  body preview, parser result, which filter fails) so failures are visible in laravel.log
- CorreoImapService::cleanBody: detect and decode base64-encoded BODY[1] before
  QP decoding — critical fix when Bancolombia sends HTML part as base64
- BancolombiaParser: make esBancolombia() broader (includes 'valor','$','abono' etc.),
  add extraerPrimero() helper, add multiple regex fallbacks for remitente/fecha/hora/valor
  to handle different Bancolombia email HTML formats beyond the SMS text pattern

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro
2026-07-18 17:01:34 +00:00
co-authored by Claude Sonnet 4.6
parent 1fd9a92afa
commit 595c75837f
3 changed files with 147 additions and 79 deletions
+76 -22
View File
@@ -39,16 +39,43 @@ class BancolombiaParser
}
$result = [
'banco' => 'Bancolombia',
'tipo' => self::detectarTipo($text),
'destinatario' => self::extraer($text, '/Bancolombia[:\s]+([A-ZÁÉÍÓÚÑÜ][A-Za-záéíóúñü\s]+?),\s+recibiste/u'),
'remitente' => self::extraer($text, '/transferencia\s+de\s+(.+?)\s+por\s+\$/iu'),
'valor' => self::extraerValor($text),
'cuenta' => self::extraer($text, '/cuenta\s+\*(\d+)/iu'),
'llave' => self::extraer($text, '/llave\s+([@\w.\-]+)/iu'),
'fecha' => self::extraer($text, '/el\s+(\d{1,2}\/\d{1,2}\/\d{2,4})/iu'),
'hora' => self::extraer($text, '/a\s+las\s+(\d{1,2}:\d{2})/iu'),
'texto_original' => $text,
'banco' => 'Bancolombia',
'tipo' => self::detectarTipo($text),
'destinatario' => self::extraerPrimero($text, [
'/Bancolombia[:\s]+([A-ZÁÉÍÓÚÑÜ][A-Za-záéíóúñü\s]+?),\s+recibiste/u',
'/Hola[,\s]+([A-ZÁÉÍÓÚÑÜ][A-Za-záéíóúñü\s]+?)[,\.]/u',
'/estimado[a]?\s+([A-ZÁÉÍÓÚÑÜ][A-Za-záéíóúñü\s]+?)[,\.]/iu',
]),
'remitente' => self::extraerPrimero($text, [
'/transferencia\s+de\s+(.+?)\s+por\s+\$/iu',
'/realiz[oó]\s+(?:un[a]?\s+)?(?:transferencia|pago)\s+de\s+\$[0-9,.]+\s+(?:a\s+tu\s+cuenta|para)\s+(.+?)[\.,]/iu',
'/(?:pagador|remitente|enviado\s+por)[:\s]+([A-ZÁÉÍÓÚÑÜ][A-Za-záéíóúñü\s]+?)[\.,]/iu',
'/de\s+([A-ZÁÉÍÓÚÑÜ][A-Za-záéíóúñü\s]{3,50}?)\s+(?:por\s+\$|a\s+tu)/iu',
]),
'valor' => self::extraerValor($text),
'cuenta' => self::extraerPrimero($text, [
'/cuenta\s+\*(\d+)/iu',
'/cuenta\s+(?:de\s+ahorros|corriente)?\s*(?:No\.?\s*)?\*?(\d{4,})/iu',
'/terminad[ao]\s+en\s+(\d{4})/iu',
]),
'llave' => self::extraerPrimero($text, [
'/llave\s+([@\w.\-]+)/iu',
'/llave\s+(?:de\s+pago\s+)?([@\w.\-]+)/iu',
'/([@][a-z0-9._\-]{3,})/iu',
]),
'fecha' => self::extraerPrimero($text, [
'/el\s+(\d{1,2}\/\d{1,2}\/\d{2,4})/iu',
'/(\d{1,2}\/\d{1,2}\/\d{2,4})/u',
'/(\d{1,2}\s+de\s+\w+\s+de\s+\d{4})/iu',
'/fecha[:\s]+(\d{1,2}[\/\-]\d{1,2}[\/\-]\d{2,4})/iu',
]),
'hora' => self::extraerPrimero($text, [
'/a\s+las\s+(\d{1,2}:\d{2})/iu',
'/hora[:\s]+(\d{1,2}:\d{2})/iu',
'/(\d{1,2}:\d{2})\s*(?:a\.?m\.?|p\.?m\.?)/iu',
'/(\d{2}:\d{2})(?::\d{2})?/u',
]),
'texto_original' => $text,
];
return $result;
@@ -60,13 +87,19 @@ class BancolombiaParser
private static function esBancolombia(string $text): bool
{
return stripos($text, 'bancolombia') !== false
&& (
stripos($text, 'transferencia') !== false
|| stripos($text, 'pago') !== false
|| stripos($text, 'recibiste') !== false
|| stripos($text, 'enviaste') !== false
);
if (stripos($text, 'bancolombia') === false) {
return false;
}
// Al menos uno de estos indica una notificación transaccional
$indicadores = ['transferencia', 'pago', 'recibiste', 'enviaste', 'credito', 'crédito',
'abono', 'consignacion', 'consignación', 'recibido', 'recibiste',
'valor', 'monto', '$'];
foreach ($indicadores as $ind) {
if (stripos($text, $ind) !== false) {
return true;
}
}
return false;
}
private static function detectarTipo(string $text): string
@@ -94,16 +127,37 @@ class BancolombiaParser
return '';
}
/**
* Prueba múltiples patrones y devuelve el primer match no-vacío.
*/
private static function extraerPrimero(string $text, array $patterns): string
{
foreach ($patterns as $pat) {
$v = self::extraer($text, $pat);
if ($v !== '') {
return $v;
}
}
return '';
}
/**
* Extrae el valor monetario como string limpio, ej: "1000.00"
* y también como float.
* Devuelve el string formateado con separadores originales.
*/
private static function extraerValor(string $text): string
{
// Captura números con puntos y comas: $1,000.00 | $1.000,00 | $500
if (preg_match('/por\s+\$([0-9][0-9.,]*)/iu', $text, $m)) {
return trim($m[1]);
// Patrones en orden de especificidad
$patterns = [
'/por\s+\$\s*([0-9][0-9.,]*)/iu',
'/valor[:\s]+\$?\s*([0-9][0-9.,]*)/iu',
'/monto[:\s]+\$?\s*([0-9][0-9.,]*)/iu',
'/recibiste[^$]*\$\s*([0-9][0-9.,]*)/iu',
'/\$\s*([0-9][0-9.,]{2,})/u',
];
foreach ($patterns as $pat) {
if (preg_match($pat, $text, $m)) {
return trim($m[1]);
}
}
return '';
}