diff --git a/app/modules_app/auth/mobile_request_otp.php b/app/modules_app/auth/mobile_request_otp.php new file mode 100644 index 0000000..f77cfd5 --- /dev/null +++ b/app/modules_app/auth/mobile_request_otp.php @@ -0,0 +1,137 @@ + 'required', +]); + +if ($errors) { + json_error('رقم الهاتف مطلوب', 422, $errors); +} + +$phone = preg_replace('/[^0-9+]/', '', $data['phone']); +$phoneHash = hash('sha256', $phone); + +// 2. Find user by phone hash +$db = Database::getInstance(); +$stmt = $db->prepare("SELECT id, tenant_id, name, is_active FROM users WHERE phone_hash = ? LIMIT 1"); +$stmt->execute([$phoneHash]); +$user = $stmt->fetch(); + +if (!$user) { + // Don't reveal if phone exists — generic message + json_success(null, 'إذا كان الرقم مسجلاً، سيتم إرسال رمز التحقق'); + exit; +} + +if (!$user['is_active']) { + json_error('الحساب معطّل. تواصل مع المسؤول.', 403); +} + +// 3. Generate OTP (6 digits) +$otp = str_pad((string)random_int(100000, 999999), 6, '0', STR_PAD_LEFT); +$otpHash = password_hash($otp, PASSWORD_DEFAULT); +$expiresAt = date('Y-m-d H:i:s', time() + 300); // 5 minutes + +// 4. Store OTP in database (or Redis if available) +// Using a simple approach: store in a cache file per phone +$cacheDir = STORAGE_PATH . '/cache/otp'; +if (!is_dir($cacheDir)) { + mkdir($cacheDir, 0755, true); +} + +$otpData = [ + 'hash' => $otpHash, + 'user_id' => $user['id'], + 'attempts' => 0, + 'max_attempts' => 5, + 'expires_at' => time() + 300, + 'created_at' => time(), +]; + +$fp = fopen($cacheDir . '/otp_' . $phoneHash . '.json', 'w'); +if ($fp) { + flock($fp, LOCK_EX); + fwrite($fp, json_encode($otpData)); + flock($fp, LOCK_UN); + fclose($fp); +} + +// 5. Send OTP via SMS +// TODO: Replace with your actual SMS provider +$smsSent = sendOtpSms($phone, $otp); + +if (!$smsSent) { + error_log("WARN: Failed to send OTP SMS to phone hash: {$phoneHash}"); + // Still return success to not reveal info, but log the issue +} + +// Log for development (REMOVE IN PRODUCTION!) +if (env('APP_DEBUG', 'false') === 'true') { + error_log("DEV OTP for {$phone}: {$otp}"); +} + +json_success(null, 'إذا كان الرقم مسجلاً، سيتم إرسال رمز التحقق'); + +// ─── SMS Helper ────────────────────────────────────────── +function sendOtpSms(string $phone, string $otp): bool +{ + $smsProvider = env('SMS_PROVIDER', 'log'); // 'log', 'twilio', 'jordan_sms', 'custom' + + $message = "رمز التحقق لتطبيق مُصادَق: {$otp}\nصالح لمدة 5 دقائق."; + + switch ($smsProvider) { + case 'custom': + // Custom SMS API (your own provider) + $apiUrl = env('SMS_API_URL'); + $apiKey = env('SMS_API_KEY'); + if (!$apiUrl || !$apiKey) return false; + + try { + $ch = curl_init($apiUrl); + curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode([ + 'to' => $phone, + 'message' => $message, + 'api_key' => $apiKey, + ]), + CURLOPT_HTTPHEADER => ['Content-Type: application/json'], + CURLOPT_RETURNTRANSFER => true, + CURLOPT_TIMEOUT => 10, + ]); + $response = curl_exec($ch); + $httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + curl_close($ch); + return $httpCode >= 200 && $httpCode < 300; + } catch (\Exception $e) { + error_log("SMS send error: " . $e->getMessage()); + return false; + } + + case 'log': + default: + // Development: just log the OTP + error_log("SMS OTP [{$phone}]: {$otp}"); + return true; + } +} diff --git a/app/modules_app/auth/mobile_verify_otp.php b/app/modules_app/auth/mobile_verify_otp.php new file mode 100644 index 0000000..ea6a0c1 --- /dev/null +++ b/app/modules_app/auth/mobile_verify_otp.php @@ -0,0 +1,174 @@ + 'required', + 'otp' => 'required', +]); + +if ($errors) { + json_error('رقم الهاتف ورمز التحقق مطلوبان', 422, $errors); +} + +$phone = preg_replace('/[^0-9+]/', '', $data['phone']); +$phoneHash = hash('sha256', $phone); +$deviceId = $data['device_id'] ?? ''; +$deviceName = $data['device_name'] ?? 'Unknown Device'; +$platform = $data['platform'] ?? 'android'; +$appVersion = $data['app_version'] ?? '1.0.0'; +$pushToken = $data['push_token'] ?? null; + +if (empty($deviceId)) { + json_error('معرّف الجهاز مطلوب', 422); +} + +// 2. Load OTP from cache +$cacheFile = STORAGE_PATH . '/cache/otp/otp_' . $phoneHash . '.json'; +if (!file_exists($cacheFile)) { + json_error('رمز التحقق غير صالح أو منتهي الصلاحية', 401); +} + +$fp = fopen($cacheFile, 'r+'); +if (!$fp) { + json_error('خطأ في النظام', 500); +} + +flock($fp, LOCK_EX); +$content = stream_get_contents($fp); +$otpData = json_decode($content, true); + +if (!$otpData || $otpData['expires_at'] < time()) { + flock($fp, LOCK_UN); + fclose($fp); + @unlink($cacheFile); + json_error('رمز التحقق منتهي الصلاحية. اطلب رمزاً جديداً.', 401); +} + +// Check attempts +if ($otpData['attempts'] >= $otpData['max_attempts']) { + flock($fp, LOCK_UN); + fclose($fp); + @unlink($cacheFile); + json_error('تجاوزت عدد المحاولات المسموحة. اطلب رمزاً جديداً.', 429); +} + +// Verify OTP +if (!password_verify($data['otp'], $otpData['hash'])) { + $otpData['attempts']++; + ftruncate($fp, 0); + rewind($fp); + fwrite($fp, json_encode($otpData)); + flock($fp, LOCK_UN); + fclose($fp); + + $remaining = $otpData['max_attempts'] - $otpData['attempts']; + json_error("رمز التحقق غير صحيح. المحاولات المتبقية: {$remaining}", 401); +} + +// OTP is valid — clean up +flock($fp, LOCK_UN); +fclose($fp); +@unlink($cacheFile); + +// 3. Fetch user +$db = Database::getInstance(); +$userId = $otpData['user_id']; + +$stmt = $db->prepare("SELECT id, tenant_id, name, email, role, is_active FROM users WHERE id = ? LIMIT 1"); +$stmt->execute([$userId]); +$user = $stmt->fetch(); + +if (!$user || !$user['is_active']) { + json_error('الحساب غير موجود أو معطّل', 403); +} + +// 4. Generate device secret for HMAC +$deviceSecret = hash('sha256', $userId . $deviceId . bin2hex(random_bytes(16))); + +// 5. Register/Update device +$stmt = $db->prepare(" + INSERT INTO user_devices (id, user_id, device_fingerprint, device_name, platform, app_version, push_token, device_secret, is_trusted, last_seen_at) + VALUES (UUID(), ?, ?, ?, ?, ?, ?, ?, TRUE, NOW()) + ON DUPLICATE KEY UPDATE + device_name = VALUES(device_name), + platform = VALUES(platform), + app_version = VALUES(app_version), + push_token = VALUES(push_token), + device_secret = VALUES(device_secret), + is_trusted = TRUE, + last_seen_at = NOW(), + updated_at = NOW() +"); +$stmt->execute([ + $userId, + $deviceId, + $deviceName, + $platform, + $appVersion, + $pushToken, + password_hash($deviceSecret, PASSWORD_DEFAULT), // Store hashed +]); + +// 6. Generate JWT (30 days for mobile) +$secret = env('JWT_SECRET'); +if (!$secret || strlen($secret) < 32) { + error_log('FATAL: JWT_SECRET is missing or too short in .env'); + json_error('Server configuration error', 500); +} + +$payload = [ + 'user_id' => $user['id'], + 'tenant_id' => $user['tenant_id'], + 'role' => $user['role'], + 'device_id' => $deviceId, + 'source' => 'mobile', + 'exp' => time() + (30 * 24 * 3600), // 30 days +]; + +$token = JWT::encode($payload, $secret); + +// 7. Generate refresh token +$refreshToken = bin2hex(random_bytes(32)); +$refreshTokenHash = hash('sha256', $refreshToken); +$stmt = $db->prepare("UPDATE users SET refresh_token_hash = ?, last_login_at = NOW() WHERE id = ?"); +$stmt->execute([$refreshTokenHash, $userId]); + +// 8. Decrypt name for response +$userName = $user['name']; +try { + $decrypted = \App\Core\Encryption::decrypt($user['name']); + if ($decrypted !== false) $userName = $decrypted; +} catch (\Exception $e) { + // Keep encrypted name +} + +json_success([ + 'access_token' => $token, + 'refresh_token' => $refreshToken, + 'device_secret' => $deviceSecret, // Client stores this securely for HMAC + 'user' => [ + 'id' => $user['id'], + 'name' => $userName, + 'role' => $user['role'], + 'tenant_id' => $user['tenant_id'], + ], +], 'تم التحقق بنجاح. مرحباً بك في مُصادَق!'); diff --git a/app/modules_app/auth/register_device.php b/app/modules_app/auth/register_device.php new file mode 100644 index 0000000..f972225 --- /dev/null +++ b/app/modules_app/auth/register_device.php @@ -0,0 +1,60 @@ +prepare($sql); +$stmt->execute($params); + +json_success(null, 'تم تحديث بيانات الجهاز'); diff --git a/app/modules_app/batches/create.php b/app/modules_app/batches/create.php new file mode 100644 index 0000000..50879cd --- /dev/null +++ b/app/modules_app/batches/create.php @@ -0,0 +1,72 @@ + 'required', +]); + +if ($errors) { + json_error('رقم الشركة مطلوب', 422, $errors); +} + +$companyId = $data['company_id']; +$source = $data['source'] ?? 'mobile_scan'; +$expectedImages = (int)($data['expected_images'] ?? 0); + +// 2. Permission check +$db = Database::getInstance(); +$stmt = $db->prepare("SELECT id FROM companies WHERE id = ? AND tenant_id = ? AND deleted_at IS NULL"); +$stmt->execute([$companyId, $tenantId]); + +if (!$stmt->fetch()) { + json_error('الوصول مرفوض لهذه الشركة', 403); +} + +// 3. Check quota (preview — don't increment yet) +try { + QuotaMiddleware::checkInvoiceQuota($tenantId); +} catch (\Exception $e) { + json_error('تم استنفاد رصيد الفواتير لهذا الشهر. قم بترقية باقتك.', 429); +} + +// 4. Generate batch ID +$batchId = vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex(random_bytes(16)), 4)); + +// 5. Create batch record +$stmt = $db->prepare(" + INSERT INTO invoice_batches (id, tenant_id, company_id, uploaded_by, total_images, source, status) + VALUES (?, ?, ?, ?, ?, ?, 'uploading') +"); +$stmt->execute([$batchId, $tenantId, $companyId, $userId, $expectedImages, $source]); + +// 6. Create upload directory +$uploadDir = STORAGE_PATH . '/invoices/' . $tenantId . '/' . $companyId . '/batches/' . $batchId; +if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0755, true); +} + +json_success([ + 'batch_id' => $batchId, + 'upload_url' => 'v1/batches/upload-image', +], 'تم إنشاء الدفعة بنجاح. ابدأ برفع الصور.'); diff --git a/app/modules_app/batches/finalize.php b/app/modules_app/batches/finalize.php new file mode 100644 index 0000000..8663527 --- /dev/null +++ b/app/modules_app/batches/finalize.php @@ -0,0 +1,65 @@ +prepare(" + SELECT id, status, total_images + FROM invoice_batches + WHERE id = ? AND tenant_id = ? AND uploaded_by = ? +"); +$stmt->execute([$batchId, $tenantId, $userId]); +$batch = $stmt->fetch(); + +if (!$batch) { + json_error('الدفعة غير موجودة', 404); +} + +if ($batch['status'] !== 'uploading') { + json_error('تم إنهاء هذه الدفعة مسبقاً', 400); +} + +if ($batch['total_images'] == 0) { + json_error('لا يمكن إنهاء دفعة فارغة', 400); +} + +// 2. Mark as processing +$stmt = $db->prepare(" + UPDATE invoice_batches + SET status = 'processing', updated_at = NOW() + WHERE id = ? +"); +$stmt->execute([$batchId]); + +// In a real production environment, you would dispatch a job to a queue worker here. +// For now, the queue worker is a cron job that checks the `invoice_processing_queue` table. + +json_success([ + 'batch_id' => $batchId, + 'status' => 'processing', + 'total_images' => $batch['total_images'] +], 'تم إنهاء الدفعة بنجاح وإرسالها للمعالجة'); diff --git a/app/modules_app/batches/status.php b/app/modules_app/batches/status.php new file mode 100644 index 0000000..f9a1230 --- /dev/null +++ b/app/modules_app/batches/status.php @@ -0,0 +1,54 @@ +prepare(" + SELECT id, status, total_images, processed_images, failed_images, created_at, completed_at + FROM invoice_batches + WHERE id = ? AND tenant_id = ? +"); +$stmt->execute([$batchId, $tenantId]); +$batch = $stmt->fetch(); + +if (!$batch) { + json_error('الدفعة غير موجودة', 404); +} + +// 2. Get items +$stmt = $db->prepare(" + SELECT id, invoice_id, image_order, status, error_message, created_at, processed_at + FROM invoice_processing_queue + WHERE batch_id = ? + ORDER BY image_order ASC +"); +$stmt->execute([$batchId]); +$items = $stmt->fetchAll(); + +json_success([ + 'batch' => $batch, + 'items' => $items +], 'تم جلب حالة الدفعة'); diff --git a/app/modules_app/batches/upload_image.php b/app/modules_app/batches/upload_image.php new file mode 100644 index 0000000..5d376b0 --- /dev/null +++ b/app/modules_app/batches/upload_image.php @@ -0,0 +1,97 @@ +prepare(" + SELECT id, company_id, status, total_images + FROM invoice_batches + WHERE id = ? AND tenant_id = ? AND uploaded_by = ? +"); +$stmt->execute([$batchId, $tenantId, $userId]); +$batch = $stmt->fetch(); + +if (!$batch) { + json_error('الدفعة غير موجودة أو ليس لديك صلاحية', 404); +} + +if ($batch['status'] !== 'uploading') { + json_error('لا يمكن إضافة صور لدفعة تمت معالجتها', 400); +} + +// 3. Validate file type +$allowedTypes = ['image/jpeg', 'image/png', 'image/webp', 'image/heic', 'image/heif']; +$mimeType = $_FILES['image']['type']; +if (!in_array($mimeType, $allowedTypes)) { + json_error('نوع الملف غير مدعوم. المسموح: JPEG, PNG, WebP, HEIC', 422); +} + +// 4. Validate file size (max 10MB) +$maxSize = 10 * 1024 * 1024; +if ($_FILES['image']['size'] > $maxSize) { + json_error('حجم الصورة أكبر من 10 ميغابايت', 422); +} + +// 5. Save file +$companyId = $batch['company_id']; +$uploadDir = STORAGE_PATH . '/invoices/' . $tenantId . '/' . $companyId . '/batches/' . $batchId; +if (!is_dir($uploadDir)) { + mkdir($uploadDir, 0755, true); +} + +$extension = pathinfo($_FILES['image']['name'], PATHINFO_EXTENSION) ?: 'jpg'; +$fileName = sprintf('img_%03d_%s.%s', $imageOrder, bin2hex(random_bytes(4)), $extension); +$targetPath = $uploadDir . '/' . $fileName; + +if (!move_uploaded_file($_FILES['image']['tmp_name'], $targetPath)) { + json_error('فشل في حفظ الصورة', 500); +} + +// 6. Add to processing queue +$stmt = $db->prepare(" + INSERT INTO invoice_processing_queue (batch_id, tenant_id, company_id, image_path, image_order, status) + VALUES (?, ?, ?, ?, ?, 'pending') +"); +$stmt->execute([$batchId, $tenantId, $companyId, $targetPath, $imageOrder]); + +// 7. Update batch image count +$stmt = $db->prepare(" + UPDATE invoice_batches + SET total_images = total_images + 1, updated_at = NOW() + WHERE id = ? +"); +$stmt->execute([$batchId]); + +// Count uploaded so far +$stmt = $db->prepare("SELECT COUNT(*) FROM invoice_processing_queue WHERE batch_id = ?"); +$stmt->execute([$batchId]); +$uploadedCount = (int)$stmt->fetchColumn(); + +json_success([ + 'uploaded' => $uploadedCount, + 'file_name' => $fileName, +], "تم رفع الصورة بنجاح ({$uploadedCount} صور في الدفعة)"); diff --git a/app/modules_app/voice/parse_intent.php b/app/modules_app/voice/parse_intent.php new file mode 100644 index 0000000..aba9271 --- /dev/null +++ b/app/modules_app/voice/parse_intent.php @@ -0,0 +1,103 @@ + 'required']); +if ($errors) { + json_error('النص مطلوب', 422); +} + +$apiKey = env('GEMINI_API_KEY'); +if (!$apiKey) { + json_error('Gemini API Key غير متوفر', 500); +} + +$text = $data['text']; + +$systemPrompt = << [ + ['parts' => [['text' => $text]]] + ], + 'systemInstruction' => [ + 'parts' => [['text' => $systemPrompt]] + ], + 'generationConfig' => [ + 'responseMimeType' => 'application/json', + 'temperature' => 0.2 + ] +]; + +// Determine appropriate endpoint based on env +$model = env('GEMINI_MODEL', 'gemini-1.5-flash'); +$url = "https://generativelanguage.googleapis.com/v1beta/models/{$model}:generateContent?key={$apiKey}"; + +$ch = curl_init($url); +curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => json_encode($payload), + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => ['Content-Type: application/json'] +]); + +$response = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$error = curl_error($ch); +curl_close($ch); + +if ($httpCode !== 200) { + error_log("Gemini Error: $response | $error"); + json_error('فشل في تحليل الأمر', 500); +} + +$respData = json_decode($response, true); +if (!isset($respData['candidates'][0]['content']['parts'][0]['text'])) { + json_error('رد غير متوقع من AI', 500); +} + +$jsonText = $respData['candidates'][0]['content']['parts'][0]['text']; +$parsed = json_decode($jsonText, true); + +if (!$parsed) { + json_error('فشل في تحليل الرد كـ JSON', 500); +} + +json_success($parsed, 'تم تحليل الأمر'); diff --git a/app/modules_app/voice/transcribe.php b/app/modules_app/voice/transcribe.php new file mode 100644 index 0000000..373bb10 --- /dev/null +++ b/app/modules_app/voice/transcribe.php @@ -0,0 +1,61 @@ + $cfile, + 'model' => 'whisper-large-v3', + 'language' => 'ar', + 'response_format' => 'json' +]; + +$ch = curl_init('https://api.groq.com/openai/v1/audio/transcriptions'); +curl_setopt_array($ch, [ + CURLOPT_POST => true, + CURLOPT_POSTFIELDS => $postData, + CURLOPT_RETURNTRANSFER => true, + CURLOPT_HTTPHEADER => [ + 'Authorization: Bearer ' . $apiKey + ] +]); + +$response = curl_exec($ch); +$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); +$error = curl_error($ch); +curl_close($ch); + +if ($httpCode !== 200) { + error_log("Groq Error: $response | $error"); + json_error('فشل في تحويل الصوت إلى نص', 500); +} + +$data = json_decode($response, true); +json_success(['text' => $data['text'] ?? ''], 'تم التحويل بنجاح'); diff --git a/musadaq_strategic_ideas.md b/musadaq_strategic_ideas.md new file mode 100644 index 0000000..32789fd --- /dev/null +++ b/musadaq_strategic_ideas.md @@ -0,0 +1,287 @@ +# 💡 أفكار استراتيجية لتطبيق مُصادَق — خارج الصندوق +## رفع الميزة التنافسية والقيمة التسويقية + +--- + +> [!IMPORTANT] +> هذه الأفكار مرتبة حسب **التأثير التسويقي × سهولة التنفيذ**. الأفكار في الأعلى هي الأسرع تأثيراً. + +--- + +## 🏆 الفئة 1: أفكار تسويقية قاتلة (High Impact — Quick Win) + +### 1. 🟢 تكامل WhatsApp Business API — "فاتورتك على واتساب" + +**الفكرة:** بدل ما المحاسب يفتح التطبيق كل مرة، يرسل صورة الفاتورة على رقم واتساب مُصادَق → النظام يستخرج البيانات تلقائياً → يرد عليه بملخص + حالة الفاتورة. + +**لماذا قاتلة؟** +- 95% من المحاسبين الأردنيين يستخدمون واتساب يومياً +- لا يحتاج تحميل تطبيق جديد للبدء (يقلل حاجز الدخول) +- **شعار تسويقي:** *"صوّر الفاتورة وأرسلها على واتساب — وإحنا بنكمل الباقي"* + +**التنفيذ:** +- WhatsApp Business API (عبر Twilio أو 360dialog) +- Webhook يستقبل الصور → يمررها لنفس `InvoiceExtractionService` +- يرد برسالة فيها: رقم الفاتورة، المبلغ، حالة JoFotara + +**التأثير التسويقي:** ⭐⭐⭐⭐⭐ + +--- + +### 2. 🏅 نظام Gamification للمحاسبين — "المحاسب المتميز" + +**الفكرة:** نقاط ومستويات وشارات للمحاسبين بناءً على أدائهم: + +| الإنجاز | الشارة | النقاط | +|---------|--------|--------| +| أول 100 فاتورة بدون أخطاء | 🥇 محاسب دقيق | 500 | +| رفع 50 فاتورة في يوم واحد | ⚡ محاسب سريع | 300 | +| 0% رفض من JoFotara لمدة شهر | ✅ متوافق 100% | 1000 | +| استخدام المساعد الصوتي 30 مرة | 🎤 محاسب ذكي | 200 | + +**لماذا؟** +- يخلق **ولاء** (Retention) — المحاسب ما بيترك التطبيق لأنه بنى "سمعة" +- يمكن ربطها بخصومات على الاشتراك أو ميزات إضافية +- **شعار:** *"مُصادَق مش بس يسهّل شغلك — بيكافئك كمان"* + +**التأثير التسويقي:** ⭐⭐⭐⭐ + +--- + +### 3. 📊 "تقرير صحة الشركة" الشهري التلقائي — AI Health Report + +**الفكرة:** كل نهاية شهر، النظام يولّد تقرير PDF تلقائي فيه: +- نسبة الامتثال الضريبي (JoFotara Compliance Score) +- عدد الفواتير المرفوضة وأسبابها +- توقعات الضريبة المستحقة +- مقارنة مع الشهر السابق (تحسن/تراجع) +- توصيات AI لتحسين الأداء + +**لماذا؟** +- المحاسب يرسل هذا التقرير لصاحب الشركة → **صاحب الشركة يشوف قيمة مُصادَق مباشرة** +- يخلق "لحظة WOW" شهرية تجدد الالتزام بالاشتراك +- **شعار:** *"مُصادَق يعطيك تقرير صحة شركتك — قبل ما المدقق يوصل"* + +**التأثير التسويقي:** ⭐⭐⭐⭐⭐ + +--- + +### 4. 🔗 نظام الإحالة (Referral Program) — "ادعُ محاسب واحصل على شهر مجاني" + +**الفكرة:** +- كل مستخدم عنده رابط/كود إحالة فريد +- إذا سجّل محاسب جديد عبر الرابط → المُحيل يحصل على **شهر مجاني** أو **500 فاتورة إضافية** +- المُحال يحصل على **أسبوعين تجربة مجانية** بدل أسبوع + +**لماذا؟** +- المحاسبين في الأردن مجتمع صغير ومتواصل — الكلام ينتشر بسرعة +- تكلفة اكتساب العميل (CAC) تنخفض بشكل كبير +- **شعار:** *"شارك مُصادَق مع زملائك — وخلّي اشتراكك يدفع حاله"* + +**التأثير التسويقي:** ⭐⭐⭐⭐⭐ + +--- + +## 🚀 الفئة 2: ميزات تقنية تنافسية (Competitive Moat) + +### 5. 🔔 Smart Notifications — "التطبيق اللي بيحكيك قبل ما تسأل" + +**الفكرة:** نظام إشعارات ذكي مبني على AI: + +- ⏰ **"عندك 12 فاتورة ما انرفعت من 3 أيام — بدك أرفعهم؟"** +- ⚠️ **"شركة X عندها 5 فواتير مرفوضة — اضغط لمراجعتها"** +- 📅 **"باقي 3 أيام على نهاية الشهر — عندك 8 فواتير بانتظار الإرسال"** +- 💰 **"رصيدك المتبقي 50 فاتورة — ترقّي باقتك الآن بخصم 20%"** +- 🎉 **"مبروك! هذا الشهر نسبة امتثالك 98% — أعلى من 85% من المستخدمين"** + +**لماذا؟** +- تحول التطبيق من "أداة" إلى "مساعد شخصي" — وهذا فرق تسويقي ضخم +- تزيد الـ Daily Active Users بشكل كبير + +--- + +### 6. 📸 AR Invoice Scanner — "وجّه الكاميرا وخلّص" + +**الفكرة:** بدل ما المحاسب يصور ويضغط → الكاميرا تكشف الفاتورة تلقائياً وترسم إطار حولها → تستخرج البيانات **لحظياً** على الشاشة (Augmented Reality overlay). + +**التنفيذ:** +- ML Kit (Google) أو Apple Vision Framework +- عرض البيانات المستخرجة فوق الصورة مباشرة (اسم المورد، المبلغ، الضريبة) +- المحاسب يأكد بضغطة واحدة + +**لماذا؟** +- **لحظة WOW** عند العرض التجريبي (Demo) — تبيع نفسها +- لا يوجد منافس أردني يقدم هذه الميزة +- **شعار:** *"وجّه الكاميرا — وشوف الفاتورة تنقرأ قدام عينك"* + +**التأثير التسويقي:** ⭐⭐⭐⭐⭐ (للعروض التقديمية والتسويق) + +--- + +### 7. 📱 Offline-First مع مزامنة ذكية — "اشتغل بدون إنترنت" + +**الفكرة (موجودة في خطتك لكن نوسّعها تسويقياً):** +- المحاسب يصور 100 فاتورة في الميدان بدون إنترنت +- كل شي يتخزن محلياً مع تشفير +- بمجرد توفر WiFi → مزامنة تلقائية مع شريط تقدم + +**الإضافة التسويقية:** +- عدّاد على الشاشة الرئيسية: **"15 فاتورة بانتظار المزامنة ⏳"** +- إشعار: **"تم مزامنة 47 فاتورة بنجاح ✅ — صرلك شغال من 3 ساعات بدون إنترنت"** +- **شعار:** *"مُصادَق يشتغل حتى لو الإنترنت ما اشتغل"* + +--- + +### 8. 🤖 Chatbot محاسبي داخل التطبيق — "اسأل مُصادَق" + +**الفكرة:** بدل المساعد الصوتي فقط، chatbot نصي داخل التطبيق: +- "كم ضريبة المبيعات على الأجهزة الإلكترونية؟" → **16%** +- "شو الحد الأقصى للإعفاء الضريبي للأدوية؟" → **معفاة بالكامل** +- "كيف أسجل فاتورة مرتجع؟" → خطوات + رابط مباشر +- "شو حالة آخر دفعة لشركة X؟" → يعرض التفاصيل + +**لماذا؟** +- يقلل الحاجة للدعم الفني بشكل كبير +- يجعل المحاسب المبتدئ يشعر بالثقة +- مبني على Gemini — نفس البنية التحتية اللي عندنا + +--- + +## 🌟 الفئة 3: أفكار للنمو والتوسع (Growth & Scale) + +### 9. 🏢 "Marketplace" للمحاسبين — ربط أصحاب الشركات بالمحاسبين + +**الفكرة:** قسم داخل المنصة يسمح لـ: +- **أصحاب الشركات** بنشر طلب "أحتاج محاسب لإدارة فواتيري" +- **المحاسبين المسجلين** بعرض خدماتهم مع تقييماتهم (من نظام Gamification) + +**لماذا؟** +- يحول مُصادَق من "أداة" إلى **"منصة" (Platform)** — وهذا يرفع التقييم (Valuation) بشكل كبير +- يخلق Network Effect — كل مستخدم جديد يزيد قيمة المنصة لكل المستخدمين +- **شعار:** *"مُصادَق — المنصة اللي بتوصلك بأفضل محاسب"* + +**التأثير التسويقي:** ⭐⭐⭐⭐⭐ (طويل المدى) + +--- + +### 10. 📊 لوحة "المقارنة المعيارية" (Benchmarking Dashboard) + +**الفكرة:** تعرض للمحاسب/صاحب الشركة: +- "شركتك ترفع فواتير أسرع من **73%** من الشركات المشابهة" +- "نسبة الرفض عندك **3%** — المعدل العام **8%**" +- "أنت تستخدم **6 من 10** ميزات مُصادَق — جرّب الباقي" + +**لماذا؟** +- يخلق شعور بالمنافسة والتحسين المستمر +- بيانات مجمّعة ومجهولة الهوية (anonymized) — لا تكشف بيانات أحد +- تحفّز الترقية لباقات أعلى + +--- + +### 11. 🔐 "ختم مُصادَق" الرقمي (Musadaq Verified Seal) + +**الفكرة:** شارة/ختم رقمي يظهر على فواتير الشركة: +- **"هذه الفاتورة تمت معالجتها والتحقق منها عبر منصة مُصادَق ✅"** +- QR Code على الفاتورة المطبوعة يوصل لصفحة تحقق +- يعمل كـ **Trust Badge** مثل SSL Certificate للمواقع + +**لماذا؟** +- يعطي الشركة مصداقية إضافية أمام عملائها +- تسويق مجاني — كل فاتورة مطبوعة عليها شعار مُصادَق +- **شعار:** *"فاتورة مختومة بـ مُصادَق = فاتورة موثوقة"* + +--- + +### 12. 📱 Widget للشاشة الرئيسية — "فواتيرك على اللوك سكرين" + +**الفكرة:** Widget لـ iOS و Android على الشاشة الرئيسية يعرض: +- عدد الفواتير المعلقة +- رصيد الباقة المتبقي +- زر "صوّر فاتورة" مباشر + +**لماذا؟** +- يبقي التطبيق "حاضر" دائماً أمام المستخدم +- يقلل الخطوات لبدء العمل (من 4 نقرات إلى 1) + +--- + +### 13. 🎓 "أكاديمية مُصادَق" — محتوى تعليمي مدمج + +**الفكرة:** قسم تعليمي داخل التطبيق: +- فيديوهات قصيرة (60 ثانية) عن: "كيف تتجنب أخطاء JoFotara الشائعة" +- اختبارات سريعة (Quiz) مع شهادات +- تحديثات عن التغييرات الضريبية الأردنية + +**لماذا؟** +- يبني **Authority** — مُصادَق يصير المرجع الأول للفوترة في الأردن +- SEO + Content Marketing مجاني +- يجذب محاسبين جدد عبر المحتوى التعليمي + +--- + +### 14. 🔄 تكامل مع البنوك الأردنية — "مطابقة تلقائية" + +**الفكرة:** ربط مع APIs البنوك (مثل بنك الأردن، العربي) لـ: +- مطابقة الفواتير مع حركات الحساب البنكي تلقائياً +- تنبيه: "فاتورة #1234 بمبلغ 500 دينار — تطابق حوالة بتاريخ 15/4" +- تسهيل التسوية البنكية (Bank Reconciliation) + +**لماذا؟** +- يوفر ساعات عمل يومية على المحاسب +- ميزة لا يقدمها أي منافس محلي حالياً +- يتطلب Open Banking API (اللي بدأ ينتشر في الأردن) + +--- + +### 15. 📋 "قوالب الفواتير الذكية" — Smart Invoice Templates + +**الفكرة:** مكتبة قوالب جاهزة حسب القطاع: +- قالب مطاعم (مع بنود طعام + ضريبة خاصة) +- قالب مقاولات (مع دفعات جزئية + ضمانات) +- قالب عيادات (مع إعفاءات صحية) +- القوالب تتعلم من فواتير المستخدم وتقترح بنود تلقائياً + +--- + +## 📈 الفئة 4: استراتيجيات تسويقية ميدانية + +### 16. 🎯 "أسبوع مُصادَق المجاني" — حملة إطلاق + +**الخطة:** +1. اختيار 50 مكتب محاسبة في عمّان +2. زيارة ميدانية مع iPad يعرض التطبيق +3. تفعيل حساب مجاني لمدة شهر مع 200 فاتورة +4. متابعة بعد أسبوع بالنتائج +5. طلب شهادة (Testimonial) فيديو قصيرة + +### 17. 🤝 شراكة مع نقابة المحاسبين الأردنيين + +- تقديم مُصادَق كـ "الأداة المعتمدة" من النقابة +- خصم خاص لأعضاء النقابة +- ورشات عمل مشتركة عن الفوترة الإلكترونية + +### 18. 📱 حملة "صوّر وأرسل" على السوشال ميديا + +- المحاسب يصور عملية مسح فاتورة (10 ثوانٍ) +- ينشرها على TikTok/Instagram مع هاشتاغ **#مُصادَق_بثانية** +- أفضل فيديو كل أسبوع يفوز بـ 3 أشهر مجانية + +--- + +## 🗺️ ملخص الأولويات المقترحة + +| الأولوية | الفكرة | السبب | +|----------|--------|-------| +| 🔴 فوري | WhatsApp Integration | أقل حاجز دخول + أعلى انتشار | +| 🔴 فوري | Referral Program | نمو عضوي بتكلفة صفر تقريباً | +| 🟡 قريب | Smart Notifications | يرفع Retention بشكل مباشر | +| 🟡 قريب | AI Health Report | يعطي قيمة ملموسة لصاحب القرار | +| 🟢 متوسط | Gamification | يبني ولاء طويل المدى | +| 🟢 متوسط | AR Scanner | ميزة WOW للعروض التقديمية | +| 🔵 طويل المدى | Marketplace | يحول المنصة لـ Platform | +| 🔵 طويل المدى | Bank Integration | ميزة تنافسية لا تُقلَّد بسهولة | + +--- + +> [!TIP] +> **النصيحة الذهبية:** ابدأ بـ WhatsApp + Referral Program — هذولا الاثنين لحالهم ممكن يجيبوا أول 100 عميل بدون ميزانية تسويق كبيرة. بعدين اشتغل على Smart Notifications و AI Health Report لتثبيت العملاء (Retention). الباقي يتبنى تدريجياً مع نمو الإيرادات. diff --git a/newplan.md b/newplan.md new file mode 100644 index 0000000..7db159e --- /dev/null +++ b/newplan.md @@ -0,0 +1,892 @@ +# خطة تنفيذ مُصادَق — Flutter Mobile App +## خارطة الطريق الشاملة بالأولويات والمكتبات والتفاصيل + +--- + +## أولاً: قرارات المكتبات النهائية + +### هيكل `pubspec.yaml` الكامل + +```yaml +dependencies: + flutter: + sdk: flutter + + # ─── State Management ─────────────────────────────── + get: ^4.6.6 # GetX — state + routing + DI + + # ─── Networking ───────────────────────────────────── + dio: ^5.4.0 # HTTP client + flutter_secure_storage: ^9.0.0 # تخزين JWT + secrets آمن + + # ─── Local Database (Isar > Hive للمشروع هذا) ────── + isar: ^3.1.0+1 # قاعدة بيانات محلية — أسرع من Hive + isar_flutter_libs: ^3.1.0+1 # Flutter bindings + path_provider: ^2.1.2 # مسارات الملفات + + # ─── Authentication & Security ────────────────────── + local_auth: ^2.1.8 # Fingerprint + FaceID + device_info_plus: ^10.1.0 # Device fingerprinting + crypto: ^3.0.3 # HMAC-SHA256 + + # ─── Camera & Scanning ────────────────────────────── + camerawesome: ^2.0.0 # كاميرا متقدمة مع تحكم كامل + cunning_document_scanner: ^0.2.0 # Edge detection + Auto-crop (iOS/Android) + image_picker: ^1.0.7 # Gallery fallback + + # ─── Image Processing ─────────────────────────────── + image: ^4.1.7 # معالجة صور Dart-native (offline) + opencv_dart: ^1.3.2 # Adaptive thresholding + advanced ops + flutter_image_compress: ^2.1.0 # ضغط JPEG بجودة قابلة للضبط + + # ─── PDF Generation ───────────────────────────────── + pdf: ^3.10.8 # إنشاء PDF بدعم العربية + printing: ^5.12.0 # مشاركة + طباعة + preview + + # ─── Voice & Audio ────────────────────────────────── + record: ^5.1.0 # تسجيل صوتي (OGG/WAV) — أخف من flutter_sound + permission_handler: ^11.3.0 # صلاحيات Mic + Camera + Storage + + # ─── Connectivity & Background ────────────────────── + connectivity_plus: ^6.0.3 # كشف الإنترنت + workmanager: ^0.5.2 # Background sync jobs + + # ─── UI & UX ──────────────────────────────────────── + cached_network_image: ^3.3.1 # صور من الشبكة + shimmer: ^3.0.0 # Loading skeleton + lottie: ^3.1.0 # Animations + + # ─── Utilities ────────────────────────────────────── + uuid: ^4.3.3 # Batch IDs + intl: ^0.19.0 # تنسيق التواريخ والأرقام العربية + package_info_plus: ^8.0.0 # App version + +dev_dependencies: + isar_generator: ^3.1.0+1 + build_runner: ^2.4.8 + flutter_test: + sdk: flutter +``` + +### لماذا Isar وليس Hive؟ + +| المعيار | Hive | Isar | +|---------|------|------| +| السرعة | جيد | أسرع 10x على الـ queries | +| Type Safety | يدوي (TypeAdapters) | تلقائي عبر code generation | +| Queries | محدود | Full query engine مع indexes | +| الحجم | خفيف | أكبر قليلاً | +| الاستخدام | بسيط جداً | يحتاج build_runner مرة واحدة | +| الاختيار | MVP بسيط | **مُصادَق** — لأننا نحتاج queries معقدة على الفواتير المحلية | + +--- + +## المرحلة 1 — أساس المشروع والأمان (الأسبوع 1-3) + +### 1.1 هيكل مجلدات Flutter + +``` +lib/ +├── main.dart +├── app/ +│ ├── bindings/ # GetX bindings +│ ├── routes/ # AppPages + AppRoutes +│ └── theme/ # AppColors, AppStyles +├── core/ +│ ├── network/ +│ │ ├── dio_client.dart # Dio instance +│ │ └── hmac_interceptor.dart # HMAC signing +│ ├── storage/ +│ │ ├── isar_service.dart # Isar instance singleton +│ │ └── secure_storage.dart # flutter_secure_storage wrapper +│ ├── services/ +│ │ ├── auth_service.dart +│ │ ├── biometric_service.dart +│ │ └── device_service.dart # fingerprint + hardware check +│ └── constants/ +│ ├── app_link.dart # API endpoints +│ ├── app_color.dart +│ └── box_name.dart # Isar collection names +├── features/ +│ ├── auth/ # Login + OTP + Biometric +│ ├── scanner/ # Batch camera + image processing +│ ├── invoices/ # Invoice list + detail +│ ├── voice/ # Voice assistant +│ └── dashboard/ # Main dashboard +└── shared/ + ├── widgets/ + └── models/ +``` + +### 1.2 HMAC Interceptor (أمان الـ API) + +```dart +// core/network/hmac_interceptor.dart +class HmacInterceptor extends Interceptor { + @override + void onRequest(RequestOptions options, RequestInterceptorHandler handler) { + final timestamp = DateTime.now().millisecondsSinceEpoch.toString(); + final body = options.data != null ? jsonEncode(options.data) : ''; + final apiSecret = SecureStorage.read(BoxName.apiSecret); + + // HMAC-SHA256: timestamp + method + path + body + final message = '$timestamp|${options.method}|${options.path}|$body'; + final hmac = Hmac(sha256, utf8.encode(apiSecret ?? '')); + final signature = hmac.convert(utf8.encode(message)).toString(); + + options.headers['X-Timestamp'] = timestamp; + options.headers['X-Signature'] = signature; + options.headers['Authorization'] = 'Bearer ${SecureStorage.read(BoxName.jwt)}'; + + handler.next(options); + } +} +``` + +### 1.3 نظام المصادقة — SMS OTP + Biometric + +**التدفق الكامل:** + +``` +[المدير يُضيف محاسب + رقم هاتفه في Web Dashboard] + ↓ +[المحاسب يفتح التطبيق → يدخل رقم الهاتف] + ↓ +[Backend يرسل OTP عبر SMS (Twilio أو منصة محلية أردنية)] + ↓ +[المحاسب يدخل OTP → Backend يتحقق → يرجع JWT] + ↓ +[التطبيق يحفظ JWT + يطلب تفعيل البصمة/FaceID] + ↓ +[عمليات الدخول التالية: بصمة فقط (أو PIN كـ fallback)] +``` + +```dart +// features/auth/controllers/auth_controller.dart +class AuthController extends GetxController { + final _biometricService = Get.find(); + final _authService = Get.find(); + final _deviceService = Get.find(); + + Future requestOtp(String phone) async { + final deviceFingerprint = await _deviceService.getFingerprint(); + // إرسال OTP مع fingerprint للتحقق منه في Backend + await _authService.requestOtp(phone: phone, deviceId: deviceFingerprint); + } + + Future verifyOtp(String phone, String otp) async { + final deviceFingerprint = await _deviceService.getFingerprint(); + final result = await _authService.verifyOtp( + phone: phone, + otp: otp, + deviceId: deviceFingerprint, + ); + // حفظ JWT في secure storage + await SecureStorage.write(BoxName.jwt, result.token); + await SecureStorage.write(BoxName.apiSecret, result.apiSecret); + + // عرض شاشة تفعيل البصمة + if (await _biometricService.isAvailable()) { + Get.toNamed(AppRoutes.biometricSetup); + } + } + + Future loginWithBiometric() async { + final authenticated = await _biometricService.authenticate(); + if (authenticated) { + // التحقق من JWT مخزن + تجديده إذا انتهت صلاحيته + final token = await SecureStorage.read(BoxName.jwt); + if (token != null) Get.offAllNamed(AppRoutes.dashboard); + } + } +} +``` + +```dart +// core/services/device_service.dart +class DeviceService extends GetxService { + Future getFingerprint() async { + final info = DeviceInfoPlugin(); + if (Platform.isAndroid) { + final android = await info.androidInfo; + // Combination of stable device identifiers + final raw = '${android.brand}|${android.model}|${android.id}'; + return sha256.convert(utf8.encode(raw)).toString(); + } else { + final ios = await info.iosInfo; + final raw = '${ios.model}|${ios.identifierForVendor}'; + return sha256.convert(utf8.encode(raw)).toString(); + } + } + + Future checkCapability() async { + final info = DeviceInfoPlugin(); + if (Platform.isAndroid) { + final android = await info.androidInfo; + final sdkInt = android.version.sdkInt; + // Android 10+ (API 29) = متوافق + if (sdkInt >= 29) return DeviceCapability.full; + if (sdkInt >= 26) return DeviceCapability.limited; + return DeviceCapability.unsupported; + } + // iOS 14+ = متوافق + return DeviceCapability.full; + } +} + +enum DeviceCapability { full, limited, unsupported } +``` + +**Backend PHP — التحقق من OTP + Device:** + +```php +// api/auth/verify_otp.php +function verifyOtpAndIssueToken($phone, $otp, $deviceId) { + // 1. تحقق من OTP + $storedOtp = Redis::get("otp:$phone"); + if ($storedOtp !== $otp) return ['error' => 'OTP غير صحيح']; + + // 2. جلب المستخدم + $user = User::where('phone', $phone)->first(); + if (!$user) return ['error' => 'المستخدم غير موجود']; + + // 3. تسجيل/تحديث الجهاز + UserDevice::updateOrCreate( + ['user_id' => $user->id, 'device_fingerprint' => $deviceId], + ['last_seen' => now(), 'is_trusted' => true] + ); + + // 4. توليد JWT مع device_id مضمّن + $payload = [ + 'user_id' => $user->id, + 'tenant_id' => $user->tenant_id, + 'device_id' => $deviceId, + 'exp' => time() + (30 * 24 * 3600) // 30 يوم + ]; + $token = JWT::encode($payload, config('jwt_secret'), 'HS256'); + + // 5. توليد API Secret خاص بهذا الجهاز للـ HMAC + $apiSecret = hash('sha256', $user->id . $deviceId . config('app_key')); + + Redis::del("otp:$phone"); + return ['token' => $token, 'api_secret' => $apiSecret]; +} +``` + +**جدول `user_devices` المطلوب إضافته:** + +```sql +CREATE TABLE user_devices ( + id INT AUTO_INCREMENT PRIMARY KEY, + user_id INT NOT NULL, + device_fingerprint VARCHAR(64) NOT NULL, + device_name VARCHAR(100), + is_trusted TINYINT(1) DEFAULT 0, + last_seen TIMESTAMP, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id), + UNIQUE KEY unique_device (user_id, device_fingerprint) +); +``` + +--- + +## المرحلة 2 — الماسح الذكي ومعالجة الصور (الأسبوع 3-7) + +### 2.1 وضع الدفعة (Batch Scan Mode) + +```dart +// features/scanner/controllers/batch_scanner_controller.dart +class BatchScannerController extends GetxController { + final images = [].obs; + final isProcessing = false.obs; + final processedCount = 0.obs; + + // Pipeline: Scan → Process → Queue → Upload + Future addAndProcessImage(File rawImage) async { + final capability = await DeviceService().checkCapability(); + + File processed; + if (capability == DeviceCapability.full) { + // معالجة كاملة على الجهاز + processed = await ImageProcessor.processLocally(rawImage); + } else { + // ضغط فقط — المعالجة على السيرفر + processed = await ImageProcessor.compressOnly(rawImage); + } + + images.add(processed); + processedCount.value = images.length; + } + + Future generateAndUpload(String companyId) async { + isProcessing.value = true; + + // 1. دمج الصور في PDF + final pdf = await PdfGenerator.fromImages(images, companyName: companyId); + + // 2. حفظ في Isar كـ pending batch + final batch = InvoiceBatch( + id: Uuid().v4(), + companyId: companyId, + pdfPath: pdf.path, + status: BatchStatus.pending, + imageCount: images.length, + createdAt: DateTime.now(), + ); + await IsarService.instance.writeTxn(() async { + await IsarService.instance.invoiceBatchs.put(batch); + }); + + // 3. محاولة رفع فورية إذا في إنترنت + final hasConnection = await ConnectivityService.isConnected(); + if (hasConnection) { + await _uploadBatch(batch); + } + // وإلا WorkManager سيتولى الرفع لاحقاً + + isProcessing.value = false; + } +} +``` + +### 2.2 معالجة الصور — Pipeline كامل + +```dart +// core/services/image_processor.dart +class ImageProcessor { + /// Full pipeline: Edge → Perspective → Grayscale → Threshold → Compress + static Future processLocally(File input) async { + // 1. تحميل الصورة + final bytes = await input.readAsBytes(); + img.Image image = img.decodeImage(bytes)!; + + // 2. تدرج الرمادي + image = img.grayscale(image); + + // 3. زيادة التباين (يساعد OCR) + image = img.adjustColor(image, contrast: 1.5); + + // 4. تحسين الحدة + image = img.sharpen(image, amount: 1.0); + + // 5. Binarization بسيط عبر image package + image = _applyThreshold(image, threshold: 128); + + // ملاحظة: لـ Adaptive Thresholding الحقيقي → استخدم opencv_dart + // final mat = await opencv.imread(input.path); + // final processed = await opencv.adaptiveThreshold(mat, 255, + // opencv.ADAPTIVE_THRESH_GAUSSIAN_C, opencv.THRESH_BINARY, 11, 2); + + // 6. ضغط JPEG بجودة 85% + final compressed = img.encodeJpg(image, quality: 85); + + // حجم متوقع: 4MB → ~200KB + final output = File('${input.parent.path}/processed_${input.path.split('/').last}'); + await output.writeAsBytes(compressed); + return output; + } + + static img.Image _applyThreshold(img.Image src, {int threshold = 128}) { + return img.adjustColor(src, + hueRotation: 0, + saturation: 0, + exposure: 0, + gamma: threshold / 128.0, + ); + } + + static Future compressOnly(File input) async { + final compressed = await FlutterImageCompress.compressAndGetFile( + input.path, + '${input.parent.path}/compressed_${input.path.split('/').last}', + quality: 70, + minWidth: 1200, + minHeight: 1600, + ); + return compressed ?? input; + } +} +``` + +### 2.3 توليد PDF من الصور + +```dart +// core/services/pdf_generator.dart +class PdfGenerator { + static Future fromImages( + List images, { + required String companyName, + }) async { + final pdf = pw.Document(); + + for (final imageFile in images) { + final imageBytes = await imageFile.readAsBytes(); + final pdfImage = pw.MemoryImage(imageBytes); + + pdf.addPage(pw.Page( + pageFormat: PdfPageFormat.a4, + margin: const pw.EdgeInsets.all(0), + build: (ctx) => pw.Image(pdfImage, fit: pw.BoxFit.contain), + )); + } + + final date = DateTime.now().toString().split(' ')[0]; + final filename = 'فواتير_${companyName}_$date.pdf'; + final dir = await getApplicationDocumentsDirectory(); + final file = File('${dir.path}/$filename'); + await file.writeAsBytes(await pdf.save()); + return file; + } +} +``` + +### 2.4 Isar Schema للـ Offline Queue + +```dart +// shared/models/invoice_batch.dart +@collection +class InvoiceBatch { + Id get isarId => fastHash(id); + + late String id; // UUID + late String companyId; + late String pdfPath; + late int imageCount; + + @enumerated + late BatchStatus status; // pending | uploading | done | failed + + late DateTime createdAt; + DateTime? uploadedAt; + int retryCount = 0; + String? errorMessage; +} + +@collection +class LocalInvoice { + Id get isarId => fastHash(id); + + late String id; + late String batchId; + late String imagePath; + late String companyId; + + @enumerated + late InvoiceStatus status; // pending | extracted | validated | submitted + + // بيانات مستخرجة من AI + String? invoiceNumber; + double? totalAmount; + double? taxAmount; + String? supplierName; + + late DateTime createdAt; + bool isSynced = false; +} +``` + +--- + +## المرحلة 3 — المساعد الصوتي (الأسبوع 7-10) + +### 3.1 معمارية كاملة: Record → Groq → Gemini → Command + +```dart +// features/voice/controllers/voice_controller.dart +class VoiceController extends GetxController { + final _recorder = AudioRecorder(); + final isRecording = false.obs; + final lastCommand = ''.obs; + final commandResult = Rxn(); + + static const maxDurationSeconds = 15; + Timer? _autoStopTimer; + + Future startRecording() async { + if (!await _recorder.hasPermission()) return; + + final dir = await getTemporaryDirectory(); + final path = '${dir.path}/voice_${DateTime.now().millisecondsSinceEpoch}.m4a'; + + await _recorder.start( + const RecordConfig(encoder: AudioEncoder.aacLc, sampleRate: 16000), + path: path, + ); + isRecording.value = true; + + // إيقاف تلقائي بعد 15 ثانية + _autoStopTimer = Timer(Duration(seconds: maxDurationSeconds), stopAndProcess); + } + + Future stopAndProcess() async { + _autoStopTimer?.cancel(); + if (!isRecording.value) return; + + final path = await _recorder.stop(); + isRecording.value = false; + + if (path == null) return; + + final file = File(path); + final result = await VoiceService.processCommand(file); + commandResult.value = result; + + await _executeCommand(result); + } + + Future _executeCommand(VoiceCommandResult result) async { + switch (result.action) { + case VoiceAction.listInvoices: + Get.toNamed(AppRoutes.invoices, arguments: result.params); + break; + case VoiceAction.checkQuota: + Get.toNamed(AppRoutes.subscription); + break; + case VoiceAction.openScanner: + Get.toNamed(AppRoutes.scanner, arguments: result.params); + break; + // ... باقي الأوامر + } + } +} +``` + +### 3.2 Voice Service — Groq STT + Gemini Intent + +```dart +// features/voice/services/voice_service.dart +class VoiceService { + static Future processCommand(File audioFile) async { + // Step 1: STT via Groq Whisper + final text = await _groqTranscribe(audioFile); + if (text.isEmpty) return VoiceCommandResult.failed('لم أفهم الأمر'); + + // Step 2: Intent parsing via Gemini Flash Lite Latest + final intent = await _parseIntent(text); + return intent; + } + + static Future _groqTranscribe(File audio) async { + final dio = DioClient.instance; // بدون HMAC (Groq خارجي) + + final formData = FormData.fromMap({ + 'file': await MultipartFile.fromFile(audio.path, filename: 'voice.m4a'), + 'model': 'whisper-large-v3-turbo', + 'language': 'ar', + 'response_format': 'text', + }); + + final response = await Dio().post( + 'https://api.groq.com/openai/v1/audio/transcriptions', + data: formData, + options: Options(headers: { + 'Authorization': 'Bearer ${AppConfig.groqApiKey}', + }), + ); + + return response.data.toString().trim(); + } + + static Future _parseIntent(String text) async { + const systemPrompt = ''' +أنت محلل أوامر لنظام مُصادَق للفوترة الأردني. +استخرج النية والمعاملات من النص وأرجع JSON فقط. + +الأوامر المتاحة: +- list_invoices: { company?: string, from?: date, to?: date, status?: string } +- check_quota: {} +- open_scanner: { company?: string } +- search_invoice: { amount?: number, company?: string, number?: string } +- get_report: { type: "tax"|"monthly", period?: string } +- check_status: { invoice_id?: string, company?: string } +- export_pdf: { invoice_id?: string, company?: string } +- navigate: { screen: string } + +أرجع: { "action": "...", "params": {...}, "confirmation": "نص قصير تأكيد" } +'''; + + final response = await Dio().post( + 'https://generativelanguage.googleapis.com/v1beta/models/gemini-2.0-flash-lite-latest:generateContent', + queryParameters: {'key': AppConfig.geminiApiKey}, + data: { + 'contents': [{'parts': [{'text': text}]}], + 'systemInstruction': {'parts': [{'text': systemPrompt}]}, + 'generationConfig': { + 'responseMimeType': 'application/json', + 'maxOutputTokens': 200, + }, + }, + ); + + final json = jsonDecode( + response.data['candidates'][0]['content']['parts'][0]['text'] + ); + + return VoiceCommandResult( + action: VoiceAction.fromString(json['action']), + params: json['params'] ?? {}, + confirmation: json['confirmation'] ?? '', + rawText: text, + ); + } +} +``` + +### 3.3 الأوامر الصوتية الـ 10 + +```dart +enum VoiceAction { + listInvoices, // "فواتير شركة X لشهر كذا" + checkQuota, // "كم باقي من رصيدي" + openScanner, // "صور فاتورة لشركة X" + searchInvoice, // "ابحث عن فاتورة 150 دينار" + getReport, // "تقرير ضريبة الشهر الماضي" + checkStatus, // "حالة فاتورة شركة X" + exportPdf, // "حوّل فاتورة 300 لـ PDF" + listRejected, // "الفواتير المرفوضة هذا الأسبوع" + subscriptionInfo,// "حالة اشتراكي" + navigate, // "افتح لوحة التحكم" + unknown, +} +``` + +--- + +## المرحلة 4 — AI Pre-Audit الضريبي (الأسبوع 10-13) + +### 4.1 تحديث Backend — نظام Queue للفواتير + +**المطلوب إضافته في الـ schema:** + +```sql +-- جدول جديد: invoice_processing_queue +CREATE TABLE invoice_processing_queue ( + id INT AUTO_INCREMENT PRIMARY KEY, + batch_id VARCHAR(36) NOT NULL, + invoice_id INT, -- بعد الاستخراج + tenant_id INT NOT NULL, + image_path VARCHAR(500) NOT NULL, + status ENUM('pending','processing','done','failed') DEFAULT 'pending', + attempts INT DEFAULT 0, + error_message TEXT, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + processed_at TIMESTAMP NULL, + INDEX idx_status_tenant (status, tenant_id), + INDEX idx_batch (batch_id) +); + +-- جدول جديد: invoice_batches +CREATE TABLE invoice_batches ( + id VARCHAR(36) PRIMARY KEY, + tenant_id INT NOT NULL, + company_id INT NOT NULL, + pdf_path VARCHAR(500), + total_images INT, + processed_images INT DEFAULT 0, + status ENUM('uploading','processing','done','partial_fail') DEFAULT 'uploading', + created_by INT NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + completed_at TIMESTAMP NULL +); +``` + +### 4.2 تحديث InvoiceExtractionService.php + +```php +// services/InvoiceExtractionService.php +class InvoiceExtractionService { + // قواعد الضريبة الأردنية + private array $taxRules = [ + // معفيات صفر + 'zero_rated' => ['أدوية', 'دواء', 'خبز', 'طحين', 'سكر', 'أرز'], + // ضريبة خاصة + 'special' => ['سيارات' => 0.15, 'سجائر' => 0.32, 'كحول' => 0.30], + // معدل عام + 'standard_rate' => 0.16, // 16% ضريبة مبيعات أردن + ]; + + public function extractAndAudit(string $imagePath, int $tenantId): array { + // 1. استخراج البيانات من Gemini Vision + $extracted = $this->extractWithVision($imagePath); + + // 2. AI Pre-Audit + $audit = $this->performPreAudit($extracted); + + // 3. حساب Hash لمنع التكرار + $hash = $this->calculateInvoiceHash($extracted); + + // 4. التحقق من التكرار + $duplicate = Invoice::where('invoice_hash', $hash) + ->where('tenant_id', $tenantId)->first(); + + if ($duplicate) { + $audit['warnings'][] = [ + 'code' => 'DUPLICATE_INVOICE', + 'message' => 'هذه الفاتورة مرفوعة مسبقاً (رقم ' . $duplicate->id . ')', + 'severity' => 'critical' + ]; + } + + return [ + 'extracted' => $extracted, + 'audit' => $audit, + 'hash' => $hash, + 'jofotara_readiness' => $this->assessJoFotaraReadiness($extracted, $audit), + ]; + } + + private function performPreAudit(array $data): array { + $warnings = []; + $errors = []; + + // فحص الضريبة + if (isset($data['items'])) { + foreach ($data['items'] as $item) { + $expectedTax = $this->calculateExpectedTax($item['name'], $item['amount']); + if (abs($item['tax'] - $expectedTax) > 0.01) { + $warnings[] = [ + 'code' => 'TAX_MISMATCH', + 'message' => "ضريبة غير صحيحة لـ '{$item['name']}': المتوقع {$expectedTax} الموجود {$item['tax']}", + 'severity' => 'high' + ]; + } + } + } + + // فحص اكتمال البيانات الإلزامية + $required = ['supplier_name', 'invoice_number', 'invoice_date', 'total_amount']; + foreach ($required as $field) { + if (empty($data[$field])) { + $errors[] = ['code' => 'MISSING_FIELD', 'field' => $field, 'severity' => 'critical']; + } + } + + return ['warnings' => $warnings, 'errors' => $errors]; + } + + private function assessJoFotaraReadiness(array $data, array $audit): array { + $errorCount = count($audit['errors']); + $highWarnings = array_filter($audit['warnings'], fn($w) => $w['severity'] === 'high'); + + $score = 100 - ($errorCount * 30) - (count($highWarnings) * 15); + return [ + 'score' => max(0, $score), + 'ready' => $score >= 70, + 'message' => $score >= 70 ? 'جاهزة للإرسال' : 'تحتاج مراجعة قبل الإرسال' + ]; + } +} +``` + +--- + +## المرحلة 5 — فحص الجهاز + Offline Architecture (الأسبوع 12-14) + +### 5.1 Device Capability Check عند أول تشغيل + +```dart +// features/auth/views/device_check_screen.dart +class DeviceCheckScreen extends GetView { + @override + Widget build(BuildContext context) { + return Scaffold( + body: Obx(() { + if (controller.isChecking.value) { + return _buildCheckingUI(); + } + return _buildResultUI(controller.capability.value); + }), + ); + } + + Widget _buildResultUI(DeviceCapability cap) { + return switch (cap) { + DeviceCapability.full => _buildFullModeUI(), + DeviceCapability.limited => _buildLimitedModeUI(), + DeviceCapability.unsupported => _buildUnsupportedUI(), + }; + } +} + +class DeviceCheckController extends GetxController { + final isChecking = true.obs; + final capability = DeviceCapability.full.obs; + + @override + void onInit() { + super.onInit(); + _runCheck(); + } + + Future _runCheck() async { + final result = await DeviceService().checkCapability(); + + // حفظ النتيجة محلياً + GetStorage().write(BoxName.deviceCapability, result.name); + + capability.value = result; + isChecking.value = false; + } +} +``` + +### 5.2 WorkManager للـ Background Sync + +```dart +// main.dart +void callbackDispatcher() { + Workmanager().executeTask((task, inputData) async { + switch (task) { + case 'syncPendingBatches': + await _syncPendingBatches(); + break; + case 'retryFailedUploads': + await _retryFailedUploads(); + break; + } + return Future.value(true); + }); +} + +// في main(): +Workmanager().initialize(callbackDispatcher); +Workmanager().registerPeriodicTask( + 'syncTask', + 'syncPendingBatches', + frequency: const Duration(minutes: 15), + constraints: Constraints(networkType: NetworkType.connected), +); +``` + +--- + +## الملخص التنفيذي بالأسابيع + +| الأسبوع | التركيز | المخرجات | +|---------|---------|----------| +| 1-2 | هيكل المشروع + HMAC + Isar setup | بنية مجلدات نظيفة، DioClient محمي | +| 2-3 | SMS OTP + Biometric Auth | تسجيل دخول آمن كامل | +| 3-5 | Batch Scanner + camerawesome | تصوير متسلسل يعمل | +| 5-7 | Image Processing (image + opencv) | صور مضغوطة ومحسّنة | +| 7-8 | PDF Generator + Offline Queue | دفعات محفوظة محلياً | +| 8-10 | Voice: Record → Groq → Gemini | 10 أوامر صوتية تعمل | +| 10-12 | AI Pre-Audit في Backend | تحذيرات ضريبية قبل JoFotara | +| 12-13 | Device Check + WorkManager Sync | مزامنة خلفية موثوقة | +| 13-14 | Testing + Beta (10 مكاتب عمان) | إطلاق تجريبي | + +--- + +## قرارات نهائية سريعة + +| القرار | الاختيار | السبب | +|--------|---------|-------| +| STT | Groq Whisper | الأرخص 9x + الأسرع | +| Intent LLM | Gemini 2.0 Flash Lite Latest | ما ينوقف، مجاني بحد معقول | +| Local DB | Isar | Queries معقدة على الفواتير | +| Camera | camerawesome | تحكم أفضل في Batch Mode | +| Image Processing | image (Dart) + opencv_dart | مرونة: Dart للبسيط، OpenCV للمعقد | +| PDF | pdf (pub.dev) | دعم RTL + عربية | +| Recording | record | أخف من flutter_sound، كافٍ للمهمة | +| Background Sync | WorkManager | الأكثر موثوقية لـ iOS + Android | +| Security Storage | flutter_secure_storage | Keychain/Keystore native | \ No newline at end of file diff --git a/public/index.php b/public/index.php index fa95ff6..01321f7 100644 --- a/public/index.php +++ b/public/index.php @@ -42,6 +42,21 @@ $routes = [ 'v1/subscriptions/current' => ['GET', 'subscriptions/current.php'], 'v1/subscriptions/assign' => ['POST', 'subscriptions/assign.php'], 'v1/subscriptions/usage' => ['GET', 'subscriptions/usage.php'], + + // Mobile Auth & Device Routes + 'v1/auth/mobile/request-otp' => ['POST', 'auth/mobile_request_otp.php'], + 'v1/auth/mobile/verify-otp' => ['POST', 'auth/mobile_verify_otp.php'], + 'v1/auth/mobile/register-device' => ['POST', 'auth/register_device.php'], + + // Batch Scanning Routes + 'v1/batches/create' => ['POST', 'batches/create.php'], + 'v1/batches/upload-image' => ['POST', 'batches/upload_image.php'], + 'v1/batches/finalize' => ['POST', 'batches/finalize.php'], + 'v1/batches/status' => ['GET', 'batches/status.php'], + + // Voice Assistant Proxies + 'v1/voice/transcribe' => ['POST', 'voice/transcribe.php'], + 'v1/voice/parse-intent' => ['POST', 'voice/parse_intent.php'], ]; if (isset($routes[$route])) { diff --git a/scripts/migrate_phase3_mobile.php b/scripts/migrate_phase3_mobile.php new file mode 100644 index 0000000..afc1b6c --- /dev/null +++ b/scripts/migrate_phase3_mobile.php @@ -0,0 +1,153 @@ + " + CREATE TABLE IF NOT EXISTS user_devices ( + id CHAR(36) PRIMARY KEY DEFAULT (UUID()), + user_id CHAR(36) NOT NULL, + device_fingerprint VARCHAR(64) NOT NULL, + device_name VARCHAR(100) NULL, + platform ENUM('android','ios','web') NOT NULL DEFAULT 'android', + app_version VARCHAR(20) NULL, + push_token TEXT NULL, + device_secret VARCHAR(128) NULL, + is_trusted BOOLEAN DEFAULT FALSE, + last_seen_at DATETIME NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE, + UNIQUE KEY uq_user_device (user_id, device_fingerprint), + INDEX idx_device_fingerprint (device_fingerprint) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + ", + + // ─── 2. Users table: Add phone + mobile fields ───────── + 'add_users_phone' => "ALTER TABLE users ADD COLUMN phone VARCHAR(20) NULL AFTER email", + 'add_users_phone_hash' => "ALTER TABLE users ADD COLUMN phone_hash VARCHAR(64) NULL AFTER phone", + 'add_users_pin_hash' => "ALTER TABLE users ADD COLUMN pin_hash VARCHAR(255) NULL AFTER password_hash", + 'add_users_biometric' => "ALTER TABLE users ADD COLUMN biometric_enabled BOOLEAN DEFAULT FALSE AFTER pin_hash", + 'add_users_phone_index' => "CREATE INDEX idx_phone_hash ON users(phone_hash)", + + // ─── 3. Invoice Batches (Mobile Scanner) ─────────────── + 'create_invoice_batches' => " + CREATE TABLE IF NOT EXISTS invoice_batches ( + id CHAR(36) PRIMARY KEY, + tenant_id CHAR(36) NOT NULL, + company_id CHAR(36) NOT NULL, + uploaded_by CHAR(36) NOT NULL, + total_images INT NOT NULL DEFAULT 0, + processed_images INT NOT NULL DEFAULT 0, + failed_images INT NOT NULL DEFAULT 0, + status ENUM('uploading','processing','done','partial_fail','failed') DEFAULT 'uploading', + source ENUM('mobile_scan','web_upload','whatsapp') DEFAULT 'mobile_scan', + pdf_path VARCHAR(500) NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + completed_at DATETIME NULL, + INDEX idx_tenant_status (tenant_id, status), + INDEX idx_company (company_id), + INDEX idx_uploaded_by (uploaded_by), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE, + FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + ", + + // ─── 4. Invoice Processing Queue ─────────────────────── + 'create_processing_queue' => " + CREATE TABLE IF NOT EXISTS invoice_processing_queue ( + id INT AUTO_INCREMENT PRIMARY KEY, + batch_id CHAR(36) NOT NULL, + invoice_id CHAR(36) NULL, + tenant_id CHAR(36) NOT NULL, + company_id CHAR(36) NOT NULL, + image_path VARCHAR(500) NOT NULL, + image_order INT NOT NULL DEFAULT 0, + status ENUM('pending','processing','done','failed') DEFAULT 'pending', + attempts INT DEFAULT 0, + max_attempts INT DEFAULT 3, + error_message TEXT NULL, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + processed_at DATETIME NULL, + INDEX idx_status_tenant (status, tenant_id), + INDEX idx_batch (batch_id), + INDEX idx_pending (status, attempts) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + ", + + // ─── 5. Add batch_id to invoices table ───────────────── + 'add_invoices_batch_id' => "ALTER TABLE invoices ADD COLUMN batch_id CHAR(36) NULL AFTER company_id", + 'add_invoices_batch_index' => "CREATE INDEX idx_batch_id ON invoices(batch_id)", + + // ─── 6. Notifications Table ──────────────────────────── + 'create_notifications' => " + CREATE TABLE IF NOT EXISTS notifications ( + id CHAR(36) PRIMARY KEY DEFAULT (UUID()), + tenant_id CHAR(36) NOT NULL, + user_id CHAR(36) NULL, + type VARCHAR(50) NOT NULL, + title VARCHAR(255) NOT NULL, + body TEXT NULL, + data JSON NULL, + is_read BOOLEAN DEFAULT FALSE, + read_at DATETIME NULL, + push_sent BOOLEAN DEFAULT FALSE, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP, + INDEX idx_user_unread (user_id, is_read), + INDEX idx_tenant (tenant_id), + INDEX idx_type (type), + FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE, + FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 + ", +]; + +$success = 0; +$skipped = 0; +$failed = 0; + +foreach ($migrations as $name => $sql) { + try { + $db->exec($sql); + echo " ✅ {$name}\n"; + $success++; + } catch (\PDOException $e) { + $msg = $e->getMessage(); + if (str_contains($msg, 'Duplicate column') || str_contains($msg, 'Duplicate key name') || str_contains($msg, 'already exists')) { + echo " ⏭️ {$name} (already exists)\n"; + $skipped++; + } else { + echo " ❌ {$name}: {$msg}\n"; + $failed++; + } + } +} + +echo "\n═══════════════════════════════════════════\n"; +echo " Migration Complete!\n"; +echo " ✅ Success: {$success} | ⏭️ Skipped: {$skipped} | ❌ Failed: {$failed}\n"; +echo "═══════════════════════════════════════════\n";