Files
musadaq-saas/newplan.md
2026-05-06 01:38:39 +03:00

892 lines
30 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# خطة تنفيذ مُصادَق — 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<BiometricService>();
final _authService = Get.find<AuthService>();
final _deviceService = Get.find<DeviceService>();
Future<void> requestOtp(String phone) async {
final deviceFingerprint = await _deviceService.getFingerprint();
// إرسال OTP مع fingerprint للتحقق منه في Backend
await _authService.requestOtp(phone: phone, deviceId: deviceFingerprint);
}
Future<void> 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<void> 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<String> 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<DeviceCapability> 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 = <File>[].obs;
final isProcessing = false.obs;
final processedCount = 0.obs;
// Pipeline: Scan → Process → Queue → Upload
Future<void> 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<void> 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<File> 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<File> 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<File> fromImages(
List<File> 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<VoiceCommandResult>();
static const maxDurationSeconds = 15;
Timer? _autoStopTimer;
Future<void> 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<void> 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<void> _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<VoiceCommandResult> 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<String> _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<VoiceCommandResult> _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<DeviceCheckController> {
@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<void> _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 |