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:
co-authored by
Claude Sonnet 4.6
parent
7b8a4c2056
commit
b083d49769
@@ -3053,4 +3053,168 @@ HTML;
|
|||||||
echo '</div>';
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -262,6 +262,8 @@ $routes = [
|
|||||||
|
|
||||||
// ─── Procesar cola outbound (protegido o cron) ─────────────────────────
|
// ─── Procesar cola outbound (protegido o cron) ─────────────────────────
|
||||||
['GET', '/admin/process-queue', fn() => DashboardController::processQueue()],
|
['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 ─────────────────────────────
|
// ─── Admin: listar pendientes de aprobación ─────────────────────────────
|
||||||
['GET', '/admin/pending-list', fn() => DashboardController::pendingList()],
|
['GET', '/admin/pending-list', fn() => DashboardController::pendingList()],
|
||||||
|
|||||||
Reference in New Issue
Block a user