260 lines
10 KiB
PHP
260 lines
10 KiB
PHP
<?php
|
|
/**
|
|
* WhatsApp Bot Webhook
|
|
* POST /v1/whatsapp/webhook
|
|
*
|
|
* Receives incoming WhatsApp messages (text + images) via the proxy bot.
|
|
* Flow: User sends invoice image → Bot processes via AI → Returns extracted data.
|
|
*
|
|
* Supported commands:
|
|
* - Image/Document: Extracts invoice data via AI
|
|
* - "ربط [CODE]": Links WhatsApp number to Musadaq account
|
|
* - "حالتي" or "status": Returns account summary
|
|
* - "مساعدة" or "help": Returns command list
|
|
*/
|
|
|
|
declare(strict_types=1);
|
|
|
|
use App\Core\Database;
|
|
use App\Core\AI;
|
|
use App\Core\Encryption;
|
|
use App\Core\AuditLogger;
|
|
|
|
// No auth middleware — this is a webhook from the bot proxy
|
|
// Verify webhook secret instead
|
|
$webhookSecret = env('WHATSAPP_WEBHOOK_SECRET', '');
|
|
$incomingSecret = $_SERVER['HTTP_X_WEBHOOK_SECRET'] ?? '';
|
|
|
|
if (!empty($webhookSecret) && !hash_equals($webhookSecret, $incomingSecret)) {
|
|
json_error('Unauthorized webhook', 401);
|
|
}
|
|
|
|
$body = json_decode(file_get_contents('php://input'), true);
|
|
|
|
if (!$body) {
|
|
json_error('Invalid payload', 400);
|
|
}
|
|
|
|
$from = $body['from'] ?? ''; // Phone number (962XXXXXXXXX)
|
|
$text = $body['message']['text'] ?? '';
|
|
$imageUrl = $body['message']['image_url'] ?? null;
|
|
$imageData = $body['message']['image_base64'] ?? null;
|
|
$mimeType = $body['message']['mime_type'] ?? 'image/jpeg';
|
|
|
|
if (empty($from)) {
|
|
json_error('Missing sender number', 400);
|
|
}
|
|
|
|
$db = Database::getInstance();
|
|
$wa = new \App\Services\WhatsAppProxyService();
|
|
|
|
try {
|
|
// 1. Look up linked account by phone hash
|
|
$phoneClean = preg_replace('/[^0-9+]/', '', $from);
|
|
$phoneHash = hash('sha256', $phoneClean);
|
|
|
|
$stmt = $db->prepare("SELECT u.id, u.tenant_id, u.name, u.role FROM users u WHERE u.phone_hash = ? AND u.is_active = 1 LIMIT 1");
|
|
$stmt->execute([$phoneHash]);
|
|
$user = $stmt->fetch();
|
|
|
|
// 2. Handle commands
|
|
$textLower = mb_strtolower(trim($text));
|
|
|
|
// === LINK COMMAND ===
|
|
if (str_starts_with($textLower, 'ربط ') || str_starts_with($textLower, 'link ')) {
|
|
$code = trim(str_replace(['ربط', 'link'], '', $text));
|
|
handleLinkCommand($db, $wa, $from, $phoneHash, $code);
|
|
exit;
|
|
}
|
|
|
|
// === HELP COMMAND ===
|
|
if (in_array($textLower, ['مساعدة', 'help', '؟', '?'])) {
|
|
$wa->sendMessage($from, "🤖 *أوامر مُصادَق:*\n\n"
|
|
. "📸 أرسل صورة فاتورة → نستخرج البيانات بالـ AI\n"
|
|
. "🔗 ربط [الكود] → لربط رقمك بحسابك\n"
|
|
. "📊 حالتي → ملخص حسابك\n"
|
|
. "❓ مساعدة → هذه الرسالة\n\n"
|
|
. "للتسجيل: musadaq.intaleqapp.com");
|
|
json_success(null, 'Help sent');
|
|
exit;
|
|
}
|
|
|
|
// === ACCOUNT NOT LINKED ===
|
|
if (!$user) {
|
|
$wa->sendMessage($from, "👋 مرحباً!\n\n"
|
|
. "رقمك غير مربوط بحساب مُصادَق.\n"
|
|
. "لربط حسابك، أرسل: *ربط [الكود]*\n\n"
|
|
. "للحصول على الكود، افتح تطبيق مُصادَق → الإعدادات → ربط واتساب.\n\n"
|
|
. "أو سجّل حساب جديد: musadaq.intaleqapp.com");
|
|
json_success(null, 'Unlinked user guided');
|
|
exit;
|
|
}
|
|
|
|
$userName = Encryption::decrypt($user['name']) ?: 'المستخدم';
|
|
|
|
// === STATUS COMMAND ===
|
|
if (in_array($textLower, ['حالتي', 'status', 'حالة'])) {
|
|
handleStatusCommand($db, $wa, $from, $user, $userName);
|
|
exit;
|
|
}
|
|
|
|
// === IMAGE/INVOICE PROCESSING ===
|
|
if ($imageData || $imageUrl) {
|
|
handleInvoiceImage($db, $wa, $from, $user, $userName, $imageData, $imageUrl, $mimeType);
|
|
exit;
|
|
}
|
|
|
|
// === DEFAULT: Unknown text ===
|
|
$wa->sendMessage($from, "مرحباً {$userName} 👋\n\n"
|
|
. "لم أفهم طلبك. يمكنك:\n"
|
|
. "📸 إرسال صورة فاتورة لاستخراج البيانات\n"
|
|
. "📊 كتابة *حالتي* لملخص حسابك\n"
|
|
. "❓ كتابة *مساعدة* لقائمة الأوامر");
|
|
|
|
json_success(null, 'Default response sent');
|
|
|
|
} catch (\Throwable $e) {
|
|
error_log("[whatsapp/webhook] Error: " . $e->getMessage());
|
|
try {
|
|
$wa->sendMessage($from, "⚠️ حدث خطأ أثناء المعالجة. يرجى المحاولة مرة أخرى.");
|
|
} catch (\Throwable $ignore) {}
|
|
json_success(null, 'Error handled'); // Return 200 so the bot doesn't retry
|
|
}
|
|
|
|
// ═══════════════════════════════════════════
|
|
// HANDLER FUNCTIONS
|
|
// ═══════════════════════════════════════════
|
|
|
|
function handleLinkCommand($db, $wa, string $from, string $phoneHash, string $code): void
|
|
{
|
|
if (empty($code)) {
|
|
$wa->sendMessage($from, "❌ يرجى إرسال الكود. مثال: *ربط ABC123*");
|
|
json_success(null, 'Empty code');
|
|
return;
|
|
}
|
|
|
|
// Find user by link code
|
|
$stmt = $db->prepare("SELECT id, tenant_id FROM users WHERE whatsapp_link_code = ? AND is_active = 1 LIMIT 1");
|
|
$stmt->execute([strtoupper(trim($code))]);
|
|
$targetUser = $stmt->fetch();
|
|
|
|
if (!$targetUser) {
|
|
$wa->sendMessage($from, "❌ الكود غير صحيح. تأكد من الكود في تطبيق مُصادَق → الإعدادات → ربط واتساب.");
|
|
json_success(null, 'Invalid code');
|
|
return;
|
|
}
|
|
|
|
// Update user's phone hash
|
|
$updateStmt = $db->prepare("UPDATE users SET phone_hash = ?, whatsapp_linked = 1, whatsapp_link_code = NULL WHERE id = ?");
|
|
$updateStmt->execute([$phoneHash, $targetUser['id']]);
|
|
|
|
$wa->sendMessage($from, "✅ تم ربط رقمك بحسابك بنجاح! 🎉\n\n"
|
|
. "الآن يمكنك إرسال صور الفواتير مباشرة هنا وسنستخرج البيانات تلقائياً.");
|
|
|
|
json_success(null, 'Account linked');
|
|
}
|
|
|
|
function handleStatusCommand($db, $wa, string $from, array $user, string $userName): void
|
|
{
|
|
$tenantId = $user['tenant_id'];
|
|
|
|
// Get stats
|
|
$invoiceStmt = $db->prepare("SELECT COUNT(*) as total, SUM(CASE WHEN status='extracted' THEN 1 ELSE 0 END) as pending FROM invoices WHERE tenant_id = ?");
|
|
$invoiceStmt->execute([$tenantId]);
|
|
$stats = $invoiceStmt->fetch();
|
|
|
|
$subStmt = $db->prepare("SELECT plan_slug, invoices_used_this_month, max_invoices_per_month FROM subscriptions WHERE tenant_id = ?");
|
|
$subStmt->execute([$tenantId]);
|
|
$sub = $subStmt->fetch();
|
|
|
|
$plan = $sub['plan_slug'] ?? 'free';
|
|
$used = $sub['invoices_used_this_month'] ?? 0;
|
|
$max = $sub['max_invoices_per_month'] ?? 15;
|
|
|
|
$msg = "📊 *ملخص حسابك، {$userName}:*\n\n"
|
|
. "📋 إجمالي الفواتير: {$stats['total']}\n"
|
|
. "⏳ بانتظار المراجعة: {$stats['pending']}\n"
|
|
. "📦 الباقة: {$plan}\n"
|
|
. "🔢 الاستخدام: {$used}/{$max} فاتورة هذا الشهر\n\n"
|
|
. "🌐 لوحة التحكم: musadaq.intaleqapp.com";
|
|
|
|
$wa->sendMessage($from, $msg);
|
|
json_success(null, 'Status sent');
|
|
}
|
|
|
|
function handleInvoiceImage($db, $wa, string $from, array $user, string $userName, ?string $imageData, ?string $imageUrl, string $mimeType): void
|
|
{
|
|
$wa->sendMessage($from, "📸 استلمت الصورة! جارٍ استخراج البيانات بالذكاء الاصطناعي... ⏳");
|
|
|
|
// Get image data
|
|
if (!$imageData && $imageUrl) {
|
|
$imageContent = @file_get_contents($imageUrl);
|
|
if (!$imageContent) {
|
|
$wa->sendMessage($from, "❌ فشل تحميل الصورة. يرجى إرسالها مرة أخرى.");
|
|
json_success(null, 'Image download failed');
|
|
return;
|
|
}
|
|
$imageData = base64_encode($imageContent);
|
|
}
|
|
|
|
if (!$imageData) {
|
|
$wa->sendMessage($from, "❌ لم أتمكن من قراءة الصورة.");
|
|
json_success(null, 'No image data');
|
|
return;
|
|
}
|
|
|
|
// Run AI extraction
|
|
$extracted = AI::extractInvoiceData($imageData, $mimeType);
|
|
|
|
if (!$extracted) {
|
|
$wa->sendMessage($from, "⚠️ لم أتمكن من استخراج البيانات. تأكد أن الصورة واضحة وتحتوي على فاتورة.");
|
|
json_success(null, 'AI extraction failed');
|
|
return;
|
|
}
|
|
|
|
// Format response
|
|
$supplierName = $extracted['supplier']['name'] ?? 'غير محدد';
|
|
$invoiceNum = $extracted['invoice_number'] ?? '-';
|
|
$invoiceDate = $extracted['invoice_date'] ?? '-';
|
|
$subtotal = number_format((float)($extracted['subtotal'] ?? 0), 2);
|
|
$tax = number_format((float)($extracted['tax_amount'] ?? 0), 2);
|
|
$total = number_format((float)($extracted['grand_total'] ?? 0), 2);
|
|
$linesCount = count($extracted['lines'] ?? []);
|
|
|
|
$msg = "✅ *تم استخراج بيانات الفاتورة:*\n\n"
|
|
. "🏢 المورد: {$supplierName}\n"
|
|
. "🔢 رقم الفاتورة: {$invoiceNum}\n"
|
|
. "📅 التاريخ: {$invoiceDate}\n"
|
|
. "📦 البنود: {$linesCount}\n"
|
|
. "───────────────\n"
|
|
. "💰 المبلغ قبل الضريبة: {$subtotal} دينار\n"
|
|
. "🏛️ الضريبة: {$tax} دينار\n"
|
|
. "📊 *الإجمالي: {$total} دينار*\n\n";
|
|
|
|
// Add warnings if any
|
|
if (!empty($extracted['validation_warnings'])) {
|
|
$msg .= "⚠️ *تحذيرات:*\n";
|
|
foreach ($extracted['validation_warnings'] as $w) {
|
|
$msg .= "• {$w}\n";
|
|
}
|
|
$msg .= "\n";
|
|
}
|
|
|
|
$msg .= "💡 لحفظ هذه الفاتورة رسمياً، ارفعها من تطبيق مُصادَق.";
|
|
|
|
$wa->sendMessage($from, $msg);
|
|
|
|
// Log the interaction
|
|
try {
|
|
AuditLogger::log('whatsapp.invoice_extracted', 'whatsapp', null, null, [
|
|
'from' => substr($from, 0, 6) . '****',
|
|
'invoice_number' => $invoiceNum,
|
|
'total' => $total,
|
|
], ['user_id' => $user['id'], 'tenant_id' => $user['tenant_id'], 'role' => $user['role']]);
|
|
} catch (\Throwable $e) {
|
|
// Non-critical
|
|
}
|
|
|
|
json_success(null, 'Invoice extracted via WhatsApp');
|
|
}
|