fix: deduplicate webhook — skip bot response on Meta retry

saveConversation() now returns bool; BotRouter only runs when the
message_id is new (INSERT IGNORE rowCount>0). Prevents WhatsApp
webhook retries from triggering duplicate bot responses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-30 09:00:50 -05:00
co-authored by Claude Sonnet 4.6
parent 16a03b546d
commit 904a1be6ea
+14 -9
View File
@@ -328,10 +328,10 @@ class WpWebhook
$body = $msg['text']['body'] ?? '';
self::log('MSG', "[TEXT] {$ctx['from']} ({$ctx['name']}): $body");
self::saveWebhookLog('messages', $ctx['from'], $ctx['name'], 'text', $body);
self::saveConversation($ctx, $body);
$isNew = self::saveConversation($ctx, $body);
self::forwardToCompany($ctx, $body);
if (self::$currentCompany !== null) {
if ($isNew && self::$currentCompany !== null) {
BotRouter::route(self::$currentCompany, $ctx, $body, 'text');
}
}
@@ -345,9 +345,9 @@ class WpWebhook
$preview = $caption ?: "[$type id:$mediaId]";
self::log('MSG', "[" . strtoupper($type) . "] {$ctx['from']} | id=$mediaId mime=$mime caption=$caption");
self::saveWebhookLog('messages', $ctx['from'], $ctx['name'], $type, $preview);
self::saveConversation($ctx, $preview, $mediaId);
$isNew = self::saveConversation($ctx, $preview, $mediaId);
self::forwardToCompany($ctx, $preview, $mediaId);
if (self::$currentCompany !== null) {
if ($isNew && self::$currentCompany !== null) {
BotRouter::route(self::$currentCompany, $ctx, $caption, $type);
}
}
@@ -378,10 +378,10 @@ class WpWebhook
$replyId = $reply['id'] ?? '';
self::log('MSG', "[INTERACTIVE/$iType] {$ctx['from']} | reply=" . json_encode($reply));
self::saveWebhookLog('messages', $ctx['from'], $ctx['name'], 'interactive', $preview);
self::saveConversation($ctx, $preview);
$isNew = self::saveConversation($ctx, $preview);
self::forwardToCompany($ctx, $preview);
if (self::$currentCompany !== null && $replyId !== '') {
if ($isNew && self::$currentCompany !== null && $replyId !== '') {
BotRouter::route(self::$currentCompany, $ctx, $replyId, 'interactive');
}
}
@@ -394,10 +394,10 @@ class WpWebhook
$replyId = $payload ?: $text;
self::log('MSG', "[BUTTON] {$ctx['from']} | text=$text payload=$payload");
self::saveWebhookLog('messages', $ctx['from'], $ctx['name'], 'button', $preview);
self::saveConversation($ctx, $preview);
$isNew = self::saveConversation($ctx, $preview);
self::forwardToCompany($ctx, $preview);
if (self::$currentCompany !== null && $replyId !== '') {
if ($isNew && self::$currentCompany !== null && $replyId !== '') {
BotRouter::route(self::$currentCompany, $ctx, $replyId, 'button');
}
}
@@ -481,7 +481,7 @@ class WpWebhook
// ─── Guardar en base de datos ─────────────────────────────────────────────
private static function saveConversation(array $ctx, string $content, ?string $mediaId = null): void
private static function saveConversation(array $ctx, string $content, ?string $mediaId = null): bool
{
try {
$companyId = self::$currentCompany['id'] ?? null;
@@ -503,9 +503,14 @@ class WpWebhook
if ($stmt->rowCount() > 0) {
$convId = (int) db()->lastInsertId();
self::saveNotification($convId, $ctx['from'], $content);
return true;
}
// Mensaje duplicado (webhook retry de Meta) — no procesar de nuevo
self::log('INFO', "Mensaje duplicado ignorado: {$ctx['message_id']} from={$ctx['from']}");
return false;
} catch (\PDOException $e) {
self::log('ERROR', 'DB saveConversation: ' . $e->getMessage());
return true; // En caso de error DB, dejar pasar para no silenciar mensajes reales
}
}