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 | |