feat: página de mensaje de prueba para simular webhook entrante

- GET /admin/test-message: formulario con empresa, número, nombre y texto
- POST /admin/test-message/send: construye payload WhatsApp, firma HMAC y
  hace POST al webhook propio — pipeline completo sin tocar Meta

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Lizandro Guarnizo
2026-06-19 21:50:25 -05:00
co-authored by Claude Sonnet 4.6
parent 7b8a4c2056
commit b083d49769
2 changed files with 166 additions and 0 deletions
+164
View File
@@ -3053,4 +3053,168 @@ HTML;
echo '</div>';
}
}
// ─── GET /admin/test-message ──────────────────────────────────────────────
public static function testMessage(): void
{
SessionAuth::require();
$user = SessionAuth::user();
$companies = CompanyRepository::all('is_active = 1');
$opts = '';
foreach ($companies as $c) {
$opts .= '<option value="' . self::h($c['phone_number_id']) . '" data-phone="' . self::h($c['display_phone'] ?? '') . '">'
. self::h($c['name']) . '</option>';
}
echo Layout::open('Mensaje de prueba', 'test', $user['username'] ?? 'Admin');
echo <<<HTML
<div class="wrap" style="max-width:640px">
<div style="margin-bottom:20px">
<h1 style="font-size:18px;font-weight:700;color:#111827">Mensaje de prueba</h1>
<p style="color:#6b7280;font-size:13px;margin-top:4px">Simula un mensaje entrante de WhatsApp al webhook. El bot responde igual que en producción.</p>
</div>
<div id="result" style="display:none;margin-bottom:16px"></div>
<div class="card">
<div class="card-b">
<form id="testForm">
<div class="form-group">
<label>Empresa</label>
<select name="phone_number_id" id="cmpSel" required>{$opts}</select>
</div>
<div class="form-row">
<div class="form-group">
<label>Número origen (sin +)</label>
<input type="text" name="from_number" placeholder="573001234567" required pattern="[0-9]+" value="573001234567">
</div>
<div class="form-group">
<label>Nombre</label>
<input type="text" name="from_name" placeholder="Cliente prueba" value="Cliente prueba">
</div>
</div>
<div class="form-group">
<label>Mensaje</label>
<textarea name="message" rows="3" placeholder="Hola, quiero información" required style="resize:vertical">Hola</textarea>
</div>
<button type="submit" class="btn-primary" id="sendBtn" style="width:100%">Enviar mensaje de prueba</button>
</form>
</div>
</div>
</div>
<script>
document.getElementById('testForm').addEventListener('submit', async function(e) {
e.preventDefault();
const btn = document.getElementById('sendBtn');
const res = document.getElementById('result');
btn.disabled = true;
btn.textContent = 'Enviando…';
res.style.display = 'none';
const fd = new FormData(this);
try {
const r = await fetch('/admin/test-message/send', { method:'POST', body: fd });
const json = await r.json();
res.style.display = '';
if (json.ok) {
res.innerHTML = '<div class="toast toast-success">✓ ' + json.message + '</div>';
} else {
res.innerHTML = '<div class="toast toast-error">✗ ' + json.error + '</div>';
}
} catch(err) {
res.style.display = '';
res.innerHTML = '<div class="toast toast-error">Error de red: ' + err.message + '</div>';
}
btn.disabled = false;
btn.textContent = 'Enviar mensaje de prueba';
});
</script>
HTML;
echo Layout::close();
}
// ─── POST /admin/test-message/send ───────────────────────────────────────
public static function testMessageSend(): void
{
SessionAuth::require();
header('Content-Type: application/json; charset=utf-8');
$phoneNumberId = trim($_POST['phone_number_id'] ?? '');
$fromNumber = preg_replace('/\D/', '', $_POST['from_number'] ?? '');
$fromName = trim($_POST['from_name'] ?? 'Test');
$message = trim($_POST['message'] ?? '');
if ($phoneNumberId === '' || $fromNumber === '' || $message === '') {
echo json_encode(['ok' => false, 'error' => 'Faltan campos requeridos']);
exit;
}
$payload = [
'object' => 'whatsapp_business_account',
'entry' => [[
'id' => 'TEST_WABA',
'changes' => [[
'field' => 'messages',
'value' => [
'messaging_product' => 'whatsapp',
'metadata' => [
'display_phone_number' => '0000000000',
'phone_number_id' => $phoneNumberId,
],
'contacts' => [[
'profile' => ['name' => $fromName],
'wa_id' => $fromNumber,
]],
'messages' => [[
'id' => 'wamid.TEST_' . time(),
'from' => $fromNumber,
'timestamp' => (string)time(),
'type' => 'text',
'text' => ['body' => $message],
]],
],
]],
]],
];
$json = json_encode($payload, JSON_UNESCAPED_UNICODE);
$appSecret = env('WHATSAPP_APP_SECRET', '');
$sig = $appSecret !== '' ? 'sha256=' . hash_hmac('sha256', $json, $appSecret) : 'sha256=test';
// POST al webhook propio vía cURL
$webhookUrl = 'https://admin.palmas360.com/admin/v1/wp-webhook';
$ch = curl_init($webhookUrl);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $json,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Hub-Signature-256: ' . $sig,
],
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 15,
CURLOPT_SSL_VERIFYPEER => false,
]);
$resp = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$curlErr = curl_error($ch);
curl_close($ch);
if ($curlErr) {
echo json_encode(['ok' => false, 'error' => 'cURL: ' . $curlErr]);
exit;
}
if ($httpCode === 200) {
echo json_encode(['ok' => true, 'message' => 'Mensaje enviado al webhook. Revisa Live Feed y logs.']);
} else {
$body = is_string($resp) ? substr($resp, 0, 200) : '';
echo json_encode(['ok' => false, 'error' => "Webhook respondió HTTP $httpCode. $body"]);
}
exit;
}
}
+2
View File
@@ -262,6 +262,8 @@ $routes = [
// ─── Procesar cola outbound (protegido o cron) ─────────────────────────
['GET', '/admin/process-queue', fn() => DashboardController::processQueue()],
['GET', '/admin/test-message', fn() => DashboardController::testMessage()],
['POST', '/admin/test-message/send', fn() => DashboardController::testMessageSend()],
// ─── Admin: listar pendientes de aprobación ─────────────────────────────
['GET', '/admin/pending-list', fn() => DashboardController::pendingList()],