892 lines
30 KiB
Markdown
892 lines
30 KiB
Markdown
# خطة تنفيذ مُصادَق — 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 | |