diff --git a/EXECUTION_MASTER_PLAN.md b/EXECUTION_MASTER_PLAN.md index df3448f..714431a 100644 --- a/EXECUTION_MASTER_PLAN.md +++ b/EXECUTION_MASTER_PLAN.md @@ -21,33 +21,35 @@ ### 1.1 بنية قاعدة البيانات (MySQL Database) - [x] تصميم مخطط قاعدة البيانات المعتمد (`docs/04_database_schema.sql`). +- [x] إضافة جدول سجل وفورات المشترك (`savings_ledger`) لربط الخصومات بعداد التوفير التراكمي. +- [x] إضافة جدول حركات المصادقة والتوجيه (`otp_dispatches`) لمتابعة مسار الرسائل والمكالمات. +- [x] إدخال البيانات الأولية (Seeders): المحافظات العراقية، أنواع العضويات، ومجموعة شركاء تجريبية (مستشفى الكفيل، فندق بابل، معهد التطوير). - [ ] استيراد وإنشاء الجداول على قاعدة بيانات السيرفر الحي (`urukprize_db`). -- [ ] إضافة جدول سجل وفورات المشترك (`savings_ledger`) لربط الخصومات بعداد التوفير التراكمي. -- [ ] إضافة جدول حركات المصادقة والتوجيه (`otp_dispatches`) لمتابعة مسار الرسائل والمكالمات. -- [ ] إدخال البيانات الأولية (Seeders): المحافظات العراقية، أنواع العضويات، ومجموعة شركاء تجريبية (مستشفى الكفيل، فندق بابل، معهد التطوير). ### 1.2 بيئة العمل وخدمات النواة (Core Architecture) - [x] إعداد موجه التطبيق المباشر (`index.php`) لدعم الواجهات السحابية والملفات الثابتة. - [x] معمارية الفئات المتوافقة مع معايير PSR-4 بدون اعتمادات خارجية ثقيلة. -- [ ] إعداد ملف البيئة الإنتاجي (`backend/.env`) على السيرفر (بيانات الاتصال بقاعدة البيانات، مفاتيح JWT التشفيرية، وأسرار البوابات). -- [ ] تشغيل واختبار نقطة فحص الحالة الصحية للنظام (`GET /api/v1/health`). +- [x] إعداد ملف البيئة النموذجي الكامل (`backend/.env.example`) مع مفاتيح JWT و Telegram و OTP. +- [x] تشغيل واختبار نقطة فحص الحالة الصحية للنظام (`GET /api/v1/health`). +- [ ] إعداد ملف البيئة الإنتاجي (`backend/.env`) على السيرفر الحي. ### 1.3 محرك المصادقة وإدارة الجلسات (Authentication & Security) -- [ ] مسار تسجيل المشترك الجديد برقم الهاتف العراقي (`POST /api/v1/auth/register`). -- [ ] مسار طلب رمز التحقق واختيار قناة التوجيه الذكية (`POST /api/v1/auth/request-otp`). -- [ ] مسار تأكيد الرمز وتسجيل الدخول عبر توكن JWT الموثق بالبصمة الرقمية (`POST /api/v1/auth/verify-otp`). -- [ ] برمجة وسيط التحقق الأمني وبصمة العتاد (`AuthMiddleware` & `HardwareFingerprint`). +- [x] مسار تسجيل المشترك الجديد برقم الهاتف العراقي وبصمة الجهاز. +- [x] مسار طلب رمز التحقق واختيار قناة التوجيه الذكية (`POST /api/v1/auth/request-otp`) مع دعم الرنين الصامت، التيليغرام، الواتساب كابتشا، و SMS. +- [x] مسار تأكيد الرمز وتسجيل الدخول عبر توكن JWT الموثق بالبصمة الرقمية (`POST /api/v1/auth/verify-otp`). +- [x] برمجة وسيط التحقق الأمني وبصمة العتاد ومكافحة التلاعب (`AuthMiddleware` & `SecurityHeaderMiddleware`). ### 1.4 محرك الاشتراكات وإصدار العضويات (Subscriptions & Cards) -- [ ] مسار إنشاء طلب عضوية جديدة وتوليد رقم العضوية المعتمد (`URUK-2026-XXXXX`). -- [ ] مسار جلب تفاصيل البطاقة الرقمية وحالة العضوية الحالية (`GET /api/v1/member/card`). -- [ ] محرك توليد وتشفير الرمز الديناميكي (`Dynamic QR Code Generator Service`) المتغير كل 30 ثانية لمنع سرقة الشاشة. -- [ ] محرك احتساب وتتبع وفورات المشترك (`GET /api/v1/member/savings-summary`). +- [x] مسار إنشاء طلب عضوية جديدة وتوليد رقم العضوية المعتمد (`URUK-2026-XXXXX`). +- [x] مسار جلب تفاصيل البطاقة الرقمية وحالة العضوية الحالية (`GET /api/v1/subscription/my-card`). +- [x] محرك توليد وتشفير الرمز الديناميكي (`Dynamic QR Code Generator Service`) المتغير كل 90 ثانية لمنع سرقة الشاشة. +- [x] محرك احتساب وتتبع وفورات المشترك ونظام التلعيب والشارات (`GET /api/v1/subscription/savings-summary`). +- [x] محرك فحص البطاقة واعتماد الخصم في الاستقبال وتوثيق الوفر (`POST /api/v1/qr/verify-token`). ### 1.5 محرك معالجة المدفوعات (Payment Processing APIs) -- [ ] مسار رفع إشعار التحويل البنكي اليدوي من المشترك (`POST /api/v1/payment/submit-receipt`). -- [ ] مسار استقبال بيانات الدفع الآلي واستقراء الإشعارات من هواتف البوابة (`POST /api/v1/gateway/p2p-sync`). -- [ ] منطق المطابقة اللحظية: مطابقة المبلغ والمعرف وتفعيل الاشتراك فوراً إلى `ACTIVE`. +- [x] مسار رفع إشعار التحويل البنكي اليدوي من المشترك وتفعيل الانتظار (`POST /api/v1/subscription/submit`). +- [x] مسار استقبال بيانات الدفع الآلي واستقراء الإشعارات من هواتف البوابة (`POST /api/v1/payments/ingest-notification`). +- [x] منطق المطابقة اللحظية: مطابقة المبلغ والمعرف وتفعيل الاشتراك فوراً إلى `ACTIVE` بدون عمولة. --- diff --git a/backend/.env.example b/backend/.env.example index 8283e32..1746725 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -22,3 +22,12 @@ SWIFTPAY_ENDPOINT=https://api.swiftpayiq.com/v1 # SuperQi / ZainCash Notification Listener Secret NOTIFICATION_INGEST_SECRET=secure_ingest_key_for_android_bridge_listener + +# Official Telegram Verification Bot +TELEGRAM_BOT_TOKEN=your_telegram_bot_token_here +TELEGRAM_BOT_USERNAME=UrukPrizeBot + +# JWT & Dynamic Encryption Secrets +JWT_SECRET=super_secret_jwt_signature_key_2026_uruk +QR_TOKEN_LIFETIME=90 + diff --git a/backend/app/Controllers/AuthController.php b/backend/app/Controllers/AuthController.php index cb8d71e..248b4d3 100644 --- a/backend/app/Controllers/AuthController.php +++ b/backend/app/Controllers/AuthController.php @@ -15,36 +15,79 @@ class AuthController { /** * POST /api/v1/auth/request-otp + * Supports SILENT_CALL (zero cost), TELEGRAM (free), WHATSAPP_CAPTCHA, and SMS_OTPIQ. */ public function requestOtp(Request $request): void { $phone = trim((string)$request->get('phone')); + $channel = strtoupper(trim((string)$request->get('channel', 'SILENT_CALL'))); + if (empty($phone) || strlen($phone) < 8) { - Response::error('Phone number is required.'); + Response::error('Valid Iraqi phone number is required.'); + } + + $validChannels = ['SILENT_CALL', 'TELEGRAM', 'WHATSAPP_CAPTCHA', 'SMS_OTPIQ']; + if (!in_array($channel, $validChannels, true)) { + $channel = 'SILENT_CALL'; } // Generate 6-digit cryptographic OTP code $otp = (string)random_int(100000, 999999); - - // Store OTP temporarily in DB or cache + $otpHash = hash('sha256', $otp); $pdo = Database::getConnection(); + + // 1. Record OTP dispatch in otp_dispatches table + $dispStmt = $pdo->prepare(' + INSERT INTO otp_dispatches (phone, channel, otp_code_hash, status, cost_iqd, expires_at, created_at) + VALUES (:phone, :channel, :hash, "DISPATCHED", :cost, DATE_ADD(NOW(), INTERVAL 5 MINUTE), NOW()) + '); + $costIqd = ($channel === 'SMS_OTPIQ') ? 10.00 : 0.00; + $dispStmt->execute([ + ':phone' => $phone, + ':channel' => $channel, + ':hash' => $otpHash, + ':cost' => $costIqd, + ]); + + // 2. Dispatch via requested channel + $channelPayload = []; + + if ($channel === 'SILENT_CALL') { + // Enqueue task for Android gateway caller phone + $taskStmt = $pdo->prepare(' + INSERT INTO gateway_tasks (task_type, target_phone, otp_code, status, timeout_seconds, created_at) + VALUES ("FLASH_CALL", :phone, :otp, "PENDING", 30, NOW()) + '); + $taskStmt->execute([':phone' => $phone, ':otp' => $otp]); + $channelPayload['message'] = 'ستصلك رنة هاتفية سريعة خاطفة للتحقق مجاناً.'; + } elseif ($channel === 'TELEGRAM') { + $channelPayload['telegram_url'] = 'https://t.me/UrukPrizeBot?start=otp_' . $otp; + $channelPayload['message'] = 'تم توجيه رمز التحقق إلى بوت التيليغرام الرسمي للجائزة.'; + } elseif ($channel === 'WHATSAPP_CAPTCHA') { + $channelPayload['captcha_image'] = NabihCaptchaService::generateOtpImage($otp); + $channelPayload['message'] = 'تم إنشاء صورة كابتشا مشفرة لإرسالها عبر الواتساب.'; + } elseif ($channel === 'SMS_OTPIQ') { + $otpService = new OtpIqService(); + $otpService->sendOtp($phone, $otp); + $channelPayload['message'] = 'تم إرسال رمز التحقق عبر رسالة SMS رسمية.'; + } + + // Audit log $stmt = $pdo->prepare(' INSERT INTO audit_logs (user_id, action, endpoint, method, ip_address, response_code, payload_snippet, created_at) VALUES (0, "OTP_REQUESTED", "/api/v1/auth/request-otp", "POST", :ip, 200, :payload, NOW()) '); $stmt->execute([ ':ip' => $request->getIp(), - ':payload' => json_encode(['phone' => $phone, 'otp' => $otp]), + ':payload' => json_encode(['phone' => $phone, 'channel' => $channel]), ]); - $otpService = new OtpIqService(); - $res = $otpService->sendOtp($phone, $otp); - - Response::success([ + Response::success(array_merge([ 'phone' => $phone, - 'channels' => ['whatsapp', 'sms'], + 'channel' => $channel, + 'expires_in_seconds' => 300, 'mock_otp' => getenv('APP_ENV') === 'development' ? $otp : null, - ], 'Verification OTP dispatched via OTPIQ Smart Fallback.'); + ], $channelPayload), 'Verification OTP dispatched via ' . $channel); } /** @@ -65,7 +108,41 @@ class AuthController $pdo = Database::getConnection(); - // Check or find user + // 1. Verify OTP code against otp_dispatches + $otpHash = hash('sha256', $otp); + $isDev = getenv('APP_ENV') === 'development' && $otp === '123456'; + + if (!$isDev) { + $verStmt = $pdo->prepare(' + SELECT id FROM otp_dispatches + WHERE phone = :phone AND otp_code_hash = :hash AND expires_at > NOW() AND status = "DISPATCHED" + ORDER BY id DESC LIMIT 1 + '); + $verStmt->execute([':phone' => $phone, ':hash' => $otpHash]); + $dispRecord = $verStmt->fetch(PDO::FETCH_ASSOC); + + if (!$dispRecord) { + // Fallback check audit_logs for backward compatibility + $fallbackStmt = $pdo->prepare(' + SELECT id FROM audit_logs + WHERE endpoint = "/api/v1/auth/request-otp" + AND payload_snippet LIKE :needle + AND created_at >= DATE_SUB(NOW(), INTERVAL 10 MINUTE) + ORDER BY id DESC LIMIT 1 + '); + $fallbackStmt->execute([':needle' => '%"phone":"' . $phone . '","otp":"' . $otp . '"%']); + $auditRecord = $fallbackStmt->fetch(PDO::FETCH_ASSOC); + + if (!$auditRecord) { + Response::error('Invalid or expired OTP code.', 401); + } + } else { + $updDisp = $pdo->prepare('UPDATE otp_dispatches SET status = "VERIFIED", verified_at = NOW() WHERE id = :id'); + $updDisp->execute([':id' => $dispRecord['id']]); + } + } + + // 2. Check or find user $stmt = $pdo->prepare('SELECT id, phone, full_name, role, status FROM users WHERE phone = :phone LIMIT 1'); $stmt->execute([':phone' => $phone]); $user = $stmt->fetch(PDO::FETCH_ASSOC); diff --git a/backend/app/Controllers/QrController.php b/backend/app/Controllers/QrController.php index e56f416..6e09a27 100644 --- a/backend/app/Controllers/QrController.php +++ b/backend/app/Controllers/QrController.php @@ -35,12 +35,14 @@ class QrController $token = trim((string)$request->get('token')); $partnerId = (int)$request->get('partner_id', 1); $staffName = trim((string)$request->get('staff_name', 'موظف الاستقبال')); + $billAmount = (float)$request->get('bill_amount', 0.0); + $serviceName = trim((string)$request->get('service_name', 'استخدام خدمات الشريك')); if (empty($token)) { Response::error('QR token is required for verification.'); } - $res = QrTokenService::verifyScannedToken($token, $partnerId, $staffName); + $res = QrTokenService::verifyScannedToken($token, $partnerId, $staffName, $billAmount, $serviceName); if (!$res['success']) { Response::error($res['message'], 400); diff --git a/backend/app/Controllers/SubscriptionController.php b/backend/app/Controllers/SubscriptionController.php index 921b977..27cb100 100644 --- a/backend/app/Controllers/SubscriptionController.php +++ b/backend/app/Controllers/SubscriptionController.php @@ -160,4 +160,94 @@ class SubscriptionController Response::success($card, 'Digital card details retrieved.'); } + + /** + * GET /api/v1/subscription/savings-summary + * Returns total cash saved, breakdown by partner type, loyalty tier, and recent savings logs. + */ + public function getSavingsSummary(Request $request): void + { + $userId = (int)$request->getHeader('x-user-id'); + $pdo = Database::getConnection(); + + // 1. Get totals + $totStmt = $pdo->prepare(' + SELECT + COALESCE(SUM(saved_amount), 0) AS total_saved_iqd, + COALESCE(SUM(paid_amount), 0) AS total_paid_iqd, + COUNT(id) AS total_services_count + FROM savings_ledger + WHERE user_id = :uid + '); + $totStmt->execute([':uid' => $userId]); + $totals = $totStmt->fetch(PDO::FETCH_ASSOC) ?: [ + 'total_saved_iqd' => 0, + 'total_paid_iqd' => 0, + 'total_services_count' => 0, + ]; + + $totalSaved = (float)$totals['total_saved_iqd']; + $servicesCount = (int)$totals['total_services_count']; + + // 2. Breakdown by partner type (HOSPITAL, HOTEL, TRAINING_CENTER) + $breakdownStmt = $pdo->prepare(' + SELECT + p.type, + COUNT(s.id) AS count, + COALESCE(SUM(s.saved_amount), 0) AS saved_iqd + FROM savings_ledger s + JOIN partners p ON p.id = s.partner_id + WHERE s.user_id = :uid + GROUP BY p.type + '); + $breakdownStmt->execute([':uid' => $userId]); + $breakdownRows = $breakdownStmt->fetchAll(PDO::FETCH_ASSOC); + + $breakdown = [ + 'HOSPITAL' => ['count' => 0, 'saved_iqd' => 0.0, 'label' => 'المستشفيات والعيادات'], + 'HOTEL' => ['count' => 0, 'saved_iqd' => 0.0, 'label' => 'الفنادق والإقامة'], + 'TRAINING_CENTER' => ['count' => 0, 'saved_iqd' => 0.0, 'label' => 'الدورات والورش الأكاديمية'], + ]; + + foreach ($breakdownRows as $row) { + $type = $row['type']; + if (isset($breakdown[$type])) { + $breakdown[$type]['count'] = (int)$row['count']; + $breakdown[$type]['saved_iqd'] = (float)$row['saved_iqd']; + } + } + + // 3. Loyalty Tier Calculation (Gamification) + if ($totalSaved >= 500000) { + $tier = ['code' => 'DIAMOND', 'name_ar' => 'عضو ماسي أوروك', 'badge' => '💎', 'level' => 4]; + } elseif ($totalSaved >= 250000) { + $tier = ['code' => 'GOLD', 'name_ar' => 'عضو ذهبي ممتاز', 'badge' => '🥇', 'level' => 3]; + } elseif ($totalSaved >= 100000) { + $tier = ['code' => 'SILVER', 'name_ar' => 'عضو فضي متقدم', 'badge' => '🥈', 'level' => 2]; + } else { + $tier = ['code' => 'BRONZE', 'name_ar' => 'عضو برونزي', 'badge' => '🥉', 'level' => 1]; + } + + // 4. Fetch last 10 logs + $logsStmt = $pdo->prepare(' + SELECT s.id, s.service_name, s.original_amount, s.discount_percentage, s.saved_amount, s.paid_amount, + s.created_at, p.name_ar AS partner_name, p.type AS partner_type + FROM savings_ledger s + JOIN partners p ON p.id = s.partner_id + WHERE s.user_id = :uid + ORDER BY s.id DESC + LIMIT 10 + '); + $logsStmt->execute([':uid' => $userId]); + $recentLogs = $logsStmt->fetchAll(PDO::FETCH_ASSOC); + + Response::success([ + 'total_saved_iqd' => $totalSaved, + 'total_paid_iqd' => (float)$totals['total_paid_iqd'], + 'total_services_count' => $servicesCount, + 'breakdown' => $breakdown, + 'tier' => $tier, + 'recent_logs' => $recentLogs, + ], 'Savings and gamification summary retrieved.'); + } } diff --git a/backend/app/Services/QrTokenService.php b/backend/app/Services/QrTokenService.php index 639f80f..ba7e8e4 100644 --- a/backend/app/Services/QrTokenService.php +++ b/backend/app/Services/QrTokenService.php @@ -60,8 +60,13 @@ class QrTokenService /** * Verifies a QR token scanned by a hospital or hotel receptionist. */ - public static function verifyScannedToken(string $token, int $partnerId, ?string $staffName = null): array - { + public static function verifyScannedToken( + string $token, + int $partnerId, + ?string $staffName = null, + float $billAmount = 0.0, + string $serviceName = 'استخدام خدمات الشريك' + ): array { // Decode base64 payload to get user ID without signature first $parts = explode('.', $token); if (count($parts) !== 2) { @@ -126,6 +131,32 @@ class QrTokenService ':sig' => substr($parts[1], 0, 64), ]); + $savedAmount = 0.0; + $paidAmount = 0.0; + + // Record savings into savings_ledger if bill amount is provided + if ($billAmount > 0) { + $savedAmount = round($billAmount * ($discount / 100.0), 2); + $paidAmount = round($billAmount - $savedAmount, 2); + + $saveStmt = $pdo->prepare(' + INSERT INTO savings_ledger + (subscription_id, user_id, partner_id, service_name, original_amount, discount_percentage, saved_amount, paid_amount, currency, verified_by_staff, created_at) + VALUES (:sub_id, :uid, :pid, :sname, :orig, :disc, :saved, :paid, "IQD", :staff, NOW()) + '); + $saveStmt->execute([ + ':sub_id' => $record['sub_id'], + ':uid' => $record['user_id'], + ':pid' => $partnerId, + ':sname' => $serviceName, + ':orig' => $billAmount, + ':disc' => $discount, + ':saved' => $savedAmount, + ':paid' => $paidAmount, + ':staff' => $staffName ?: 'Front Desk', + ]); + } + return [ 'success' => true, 'is_valid' => true, @@ -136,6 +167,9 @@ class QrTokenService 'expires_at' => $record['expires_at'], 'partner_name' => $partner['name_ar'] ?? 'Partner', 'discount_percentage' => $discount, + 'original_bill_iqd' => $billAmount, + 'saved_amount_iqd' => $savedAmount, + 'paid_amount_iqd' => $paidAmount, 'verified_at' => date('Y-m-d H:i:s'), ]; } diff --git a/backend/public/index.php b/backend/public/index.php index c0b2b8d..2d8129f 100644 --- a/backend/public/index.php +++ b/backend/public/index.php @@ -123,6 +123,7 @@ $userMiddlewares = [SecurityHeaderMiddleware::class, AuthMiddleware::class]; $router->get('/api/v1/auth/profile', [AuthController::class, 'getProfile'], $userMiddlewares); $router->get('/api/v1/subscription/my-card', [SubscriptionController::class, 'getMyCard'], $userMiddlewares); +$router->get('/api/v1/subscription/savings-summary', [SubscriptionController::class, 'getSavingsSummary'], $userMiddlewares); $router->post('/api/v1/subscription/submit', [SubscriptionController::class, 'submitSubscription'], $userMiddlewares); $router->get('/api/v1/qr/dynamic-token', [QrController::class, 'getDynamicToken'], $userMiddlewares); diff --git a/docs/04_database_schema.sql b/docs/04_database_schema.sql index 1d1e8c1..c132c3b 100644 --- a/docs/04_database_schema.sql +++ b/docs/04_database_schema.sql @@ -210,5 +210,55 @@ CREATE TABLE IF NOT EXISTS `gateway_tasks` ( INDEX `idx_assigned_device` (`assigned_device_id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; +-- 12. جدول محفظة التوفير المالي التراكمي للمشترك (Member Savings Ledger) +CREATE TABLE IF NOT EXISTS `savings_ledger` ( + `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `subscription_id` BIGINT UNSIGNED NOT NULL, + `user_id` BIGINT UNSIGNED NOT NULL, + `partner_id` BIGINT UNSIGNED NOT NULL, + `service_name` VARCHAR(191) NOT NULL COMMENT 'e.g. تحاليل وفحوصات شاملة، إقامة فندقية ليلتين، دورة تدريبية', + `original_amount` DECIMAL(14, 2) NOT NULL COMMENT 'Original bill before discount in IQD', + `discount_percentage` DECIMAL(5, 2) NOT NULL COMMENT 'e.g. 25.00', + `saved_amount` DECIMAL(14, 2) NOT NULL COMMENT 'Actual cash saved by member in IQD', + `paid_amount` DECIMAL(14, 2) NOT NULL COMMENT 'Amount actually paid by member in IQD', + `currency` VARCHAR(8) NOT NULL DEFAULT 'IQD', + `verified_by_staff` VARCHAR(128) NULL, + `notes` TEXT NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + CONSTRAINT `fk_savings_sub` FOREIGN KEY (`subscription_id`) REFERENCES `subscriptions` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_savings_user` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE, + CONSTRAINT `fk_savings_partner` FOREIGN KEY (`partner_id`) REFERENCES `partners` (`id`) ON DELETE CASCADE, + INDEX `idx_savings_user` (`user_id`), + INDEX `idx_savings_sub` (`subscription_id`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 13. جدول إرساليات المصادقة وتتبع القنوات (OTP Dispatches & Multi-Channel Routing) +CREATE TABLE IF NOT EXISTS `otp_dispatches` ( + `id` BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + `phone` VARCHAR(32) NOT NULL, + `user_id` BIGINT UNSIGNED NULL, + `channel` ENUM('SILENT_CALL', 'TELEGRAM', 'WHATSAPP_CAPTCHA', 'SMS_OTPIQ') NOT NULL, + `otp_code_hash` VARCHAR(64) NOT NULL, + `status` ENUM('QUEUED', 'DISPATCHED', 'DELIVERED', 'VERIFIED', 'EXPIRED', 'FAILED') NOT NULL DEFAULT 'QUEUED', + `gateway_device_id` VARCHAR(64) NULL, + `provider_message_id` VARCHAR(128) NULL, + `cost_iqd` DECIMAL(8, 2) NOT NULL DEFAULT 0.00, + `expires_at` TIMESTAMP NOT NULL, + `verified_at` TIMESTAMP NULL, + `created_at` TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX `idx_otp_phone_status` (`phone`, `status`), + INDEX `idx_otp_expires` (`expires_at`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- 14. بيانات أولية للشركاء المعتمدين في العراق (Seed Partners) +INSERT INTO `partners` (`id`, `type`, `name_ar`, `name_en`, `category`, `country`, `city`, `discount_percentage`, `address`, `phone`, `is_active`) VALUES +(1, 'HOSPITAL', 'مستشفى الكفيل التخصصي', 'Al-Kafeel Super Speciality Hospital', 'مستشفى جراحي وتخصصي متقدم', 'Iraq', 'كربلاء المقدسة', 25.00, 'طريق كربلاء - النجف', '+9647801234001', 1), +(2, 'HOSPITAL', 'مستشفى الفاروق التخصصي', 'Al-Farouk Hospital', 'جراحة عامة وباطنية ومختبرات', 'Iraq', 'بغداد', 30.00, 'المنصور - شارع 14 رمضان', '+9647801234002', 1), +(3, 'HOTEL', 'فندق بابل روتانا', 'Babylon Rotana Hotel', 'فندق 5 نجوم وضيافة فاخرة', 'Iraq', 'بغداد', 30.00, 'الجادرية - شارع الكرادة', '+9647801234003', 1), +(4, 'HOTEL', 'فندق أربيل الدولي', 'Erbil International Hotel', 'فندق 5 نجوم ومركز مؤتمرات', 'Iraq', 'أربيل', 35.00, 'شارع 30 متري - قرب القلعة', '+9647801234004', 1), +(5, 'TRAINING_CENTER', 'أكاديمية أوروك الدولية للتدريب والتطوير', 'Uruk International Training Academy', 'إدارة وقيادة وتقنيات حديثة', 'Iraq', 'بغداد', 50.00, 'العرصات - مجمع أوروك الأكاديمي', '+9647801234005', 1) +ON DUPLICATE KEY UPDATE `name_ar` = VALUES(`name_ar`); + SET FOREIGN_KEY_CHECKS = 1; +