preg_replace('/[^0-9]/', '', '1,000.00') = '100000' porque el .00 aporta 00.
Fix: primero quitar la parte decimal (/[.,]\d{1,2}$/) antes de limpiar separadores.
Aplicado en BancolombiaParser::normalizarValor() y como doble defensa en PagoValidadorService.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
199 lines
7.4 KiB
PHP
Executable File
199 lines
7.4 KiB
PHP
Executable File
<?php
|
|
|
|
namespace App\Services;
|
|
|
|
/**
|
|
* BancolombiaParser
|
|
*
|
|
* Extrae campos estructurados de los correos de notificación de Bancolombia.
|
|
* Soporta notificaciones de transferencias por llave de pago.
|
|
*
|
|
* Ejemplo de texto:
|
|
* "Bancolombia: LEDYS, recibiste una transferencia de HELMER DE JESUS RINCON ORTEGA
|
|
* por $1,000.00 en tu cuenta *0733 conectada a la llave @leon8909 el 21/04/26
|
|
* a las 19:51."
|
|
*/
|
|
class BancolombiaParser
|
|
{
|
|
/**
|
|
* Intenta parsear el cuerpo de texto del correo.
|
|
*
|
|
* @return array|null Null si el texto no parece una notificación de Bancolombia.
|
|
* Array con claves: destinatario, remitente, valor, cuenta, llave, fecha, hora, texto_original
|
|
*/
|
|
public static function parse(string $text): ?array
|
|
{
|
|
// Limpiar HTML si llega con etiquetas
|
|
$text = self::stripHtml($text);
|
|
|
|
// Limpiar referencias URL entre corchetes: "Logo Bancolombia [http://...]"
|
|
$text = preg_replace('/\[https?:\/\/[^\]]*\]/i', '', $text);
|
|
|
|
// Normalizar espacios y saltos de línea
|
|
$text = preg_replace('/\s+/', ' ', $text);
|
|
$text = trim($text);
|
|
|
|
// Detectar si es correo de Bancolombia
|
|
if (! self::esBancolombia($text)) {
|
|
return null;
|
|
}
|
|
|
|
$result = [
|
|
'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;
|
|
}
|
|
|
|
// ─────────────────────────────────────────────────────────
|
|
// Helpers privados
|
|
// ─────────────────────────────────────────────────────────
|
|
|
|
private static function esBancolombia(string $text): bool
|
|
{
|
|
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
|
|
{
|
|
if (stripos($text, 'recibiste') !== false) {
|
|
return 'transferencia_recibida';
|
|
}
|
|
if (stripos($text, 'enviaste') !== false) {
|
|
return 'transferencia_enviada';
|
|
}
|
|
if (stripos($text, 'pago') !== false) {
|
|
return 'pago';
|
|
}
|
|
return 'notificacion';
|
|
}
|
|
|
|
/**
|
|
* Ejecuta un regex y devuelve el primer grupo capturado, trimmed.
|
|
*/
|
|
private static function extraer(string $text, string $pattern): string
|
|
{
|
|
if (preg_match($pattern, $text, $m)) {
|
|
return trim($m[1]);
|
|
}
|
|
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 entero limpio sin decimales (ej: "1000").
|
|
* $1,000.00 → "1000" | $50.000,00 → "50000"
|
|
*/
|
|
private static function extraerValor(string $text): string
|
|
{
|
|
$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 self::normalizarValor(trim($m[1]));
|
|
}
|
|
}
|
|
return '';
|
|
}
|
|
|
|
/**
|
|
* Convierte "1,000.00" o "1.000,00" a "1000" (entero limpio).
|
|
*/
|
|
private static function normalizarValor(string $raw): string
|
|
{
|
|
// Eliminar parte decimal: ,XX o .XX al final (pesos colombianos no tienen centavos reales)
|
|
$raw = preg_replace('/[.,]\d{1,2}$/', '', $raw);
|
|
// Eliminar separadores de miles (comas, puntos)
|
|
return preg_replace('/[^0-9]/', '', $raw);
|
|
}
|
|
|
|
/**
|
|
* Elimina completamente bloques <style>, <script>, <head> y luego
|
|
* convierte etiquetas estructurales en saltos de línea antes de strip_tags.
|
|
*/
|
|
private static function stripHtml(string $text): string
|
|
{
|
|
// Eliminar bloques cuyo contenido no es texto visible
|
|
$text = preg_replace('/<style[^>]*>.*?<\/style>/si', '', $text);
|
|
$text = preg_replace('/<script[^>]*>.*?<\/script>/si', '', $text);
|
|
$text = preg_replace('/<head[^>]*>.*?<\/head>/si', '', $text);
|
|
|
|
// Convertir etiquetas estructurales a saltos de línea
|
|
$text = preg_replace('/<(br\s*\/?\s*|\/p|\/div|\/tr|\/li|\/td)>/i', "\n", $text);
|
|
|
|
// Quitar todas las etiquetas restantes
|
|
$text = strip_tags($text);
|
|
|
|
// Decodificar entidades HTML
|
|
$text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
|
|
|
|
return $text;
|
|
}
|
|
}
|