Update: 2026-05-04 16:06:15
This commit is contained in:
@@ -4,6 +4,7 @@ namespace App\Core;
|
||||
|
||||
/**
|
||||
* Gemini AI Integration for Invoice Extraction
|
||||
* Optimized for Jordan UBL 2.1 Compliance
|
||||
*/
|
||||
class AI
|
||||
{
|
||||
@@ -20,54 +21,44 @@ class AI
|
||||
return null;
|
||||
}
|
||||
|
||||
$prompt = "أنت نظام متخصص في استخلاص بيانات الفواتير التجارية. مهمتك واحدة فقط: استخراج البيانات من الفاتورة المرفقة بدقة تامة.
|
||||
$prompt = "أنت نظام خبير في استخراج البيانات الضريبية للفواتير في الأردن.
|
||||
يجب أن تلتزم بالقواعد التالية بصرامة حسابية مطلقة.
|
||||
|
||||
## قواعد صارمة:
|
||||
**اللغة:**
|
||||
- إذا كانت الفاتورة بالعربية: أبقِ جميع أسماء السلع والعناوين بالعربية بدون ترجمة
|
||||
- إذا كانت بالإنجليزية: أبقِها بالإنجليزية بدون ترجمة
|
||||
- الأرقام دائماً بالأرقام اللاتينية (0-9) بغض النظر عن لغة الفاتورة
|
||||
- المبالغ بـ 3 أرقام عشرية (مثال: 15.000 وليس 15)
|
||||
### 1. القواعد الحسابية الصارمة (إلزامي):
|
||||
يجب أن توازن الفاتورة حسابياً قبل إرجاع النتيجة. المعادلة الأساسية هي:
|
||||
`Grand Total = Subtotal - Discount Total + Tax Amount`
|
||||
|
||||
**الدقة:**
|
||||
- لا تخترع أي بيانات غير موجودة في الفاتورة — أعد null إذا لم تجد المعلومة
|
||||
- تحقق رياضياً: subtotal = مجموع (quantity × unit_price - discount) لكل سطر
|
||||
- تحقق: grand_total = subtotal - discount_total + tax_amount
|
||||
- إذا وجدت تناقضاً بين الأرقام في الفاتورة، سجِّله في حقل \"validation_warnings\"
|
||||
**مثال للتوضيح:**
|
||||
إذا كانت البنود هي:
|
||||
1. صنف أ: 12.000 (1 × 12.000)
|
||||
2. صنف ب: 175.000 (35 × 5.000)
|
||||
فإن المجموع الفرعي (Subtotal) هو 187.000.
|
||||
إذا كانت الضريبة 16% على صنف أ فقط، فإن Tax Amount = 1.920.
|
||||
إذاً الإجمالي (Grand Total) يجب أن يكون 188.920.
|
||||
|
||||
**الضريبة:**
|
||||
- في الأردن: ضريبة المبيعات العامة (GST) = 16% للسلع العامة
|
||||
- سلع معفاة من الضريبة: المواد الغذائية الأساسية، الأدوية، الكتب، بعض المعدات الطبية
|
||||
- سلع بضريبة مخفضة: قد تكون 4% أو 8% — استخرج النسبة الفعلية من الفاتورة
|
||||
- لكل سطر: حدد tax_rate الفعلي (0 للمعفاة، وإلا النسبة المئوية كعدد عشري مثل 0.16)
|
||||
**تنبيه:** إذا وجدت في الفاتورة رقماً مكتوباً كإجمالي (Grand Total) ولكنه لا يطابق مجموع البنود والضريبة، قم بتصحيح البيانات المستخرجة للبنود لتتوافق مع المجموع الصحيح أو اتبع المجموع الرياضي الأدق. لا تخرج إجمالياً (مثلاً 15.000) بينما مجموع البنود (311.000).
|
||||
|
||||
## البيانات المطلوبة (JSON فقط، بدون أي نص إضافي):
|
||||
### 2. قواعد استخراج البيانات:
|
||||
- **اللغة:** لا تترجم. إذا كان الوصف 'صنف أول' أبقه 'صنف أول'.
|
||||
- **الأرقام:** استخدم الأرقام اللاتينية (0-9).
|
||||
- **الدقة:** استخدم 3 أرقام عشرية للمبالغ (مثال: 0.500).
|
||||
- **الضريبة:** في الأردن، الضريبة العامة هي 16% (0.160). حدد لكل بند النسبة الفعلية.
|
||||
|
||||
```json
|
||||
### 3. هيكل البيانات (JSON فقط):
|
||||
{
|
||||
\"invoice_number\": \"string | null\",
|
||||
\"invoice_date\": \"YYYY-MM-DD | null\",
|
||||
\"invoice_number\": \"string\",
|
||||
\"invoice_date\": \"YYYY-MM-DD\",
|
||||
\"invoice_type\": \"cash | credit\",
|
||||
\"payment_method_code\": \"013 | 010 | 001\",
|
||||
\"supplier\": {
|
||||
\"name\": \"string | null\",
|
||||
\"tin\": \"string | null\",
|
||||
\"address\": \"string | null\"
|
||||
},
|
||||
\"buyer\": {
|
||||
\"name\": \"string | null\",
|
||||
\"tin\": \"string | null\",
|
||||
\"national_id\": \"string | null\"
|
||||
},
|
||||
\"invoice_category\": \"simplified | standard\",
|
||||
\"supplier\": { \"name\": \"string\", \"tin\": \"string\", \"address\": \"string\" },
|
||||
\"buyer\": { \"name\": \"string\", \"tin\": \"string\", \"national_id\": \"string\" },
|
||||
\"lines\": [
|
||||
{
|
||||
\"line_number\": 1,
|
||||
\"description\": \"string\",
|
||||
\"quantity\": 0.000,
|
||||
\"unit_price\": 0.000,
|
||||
\"discount\": 0.000,
|
||||
\"tax_rate\": 0.16,
|
||||
\"tax_exempt_reason\": \"string | null\",
|
||||
\"tax_rate\": 0.160,
|
||||
\"line_total\": 0.000
|
||||
}
|
||||
],
|
||||
@@ -75,14 +66,10 @@ class AI
|
||||
\"discount_total\": 0.000,
|
||||
\"tax_amount\": 0.000,
|
||||
\"grand_total\": 0.000,
|
||||
\"currency_code\": \"JOD\",
|
||||
\"math_verified\": true,
|
||||
\"validation_warnings\": [],
|
||||
\"ai_confidence\": 0.95
|
||||
\"currency_code\": \"JOD\"
|
||||
}
|
||||
```
|
||||
|
||||
أعد JSON فقط بدون أي شرح أو مقدمة أو علامات Markdown.";
|
||||
أعد كود JSON فقط بدون أي علامات Markdown أو نصوص إضافية.";
|
||||
|
||||
$payload = [
|
||||
"contents" => [
|
||||
|
||||
@@ -32,7 +32,7 @@ if (preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) {
|
||||
|
||||
if (!$token) outputErrorImage('Forbidden: No token');
|
||||
|
||||
$decoded = \App\Core\JWT::decode($token);
|
||||
$decoded = \App\Core\JWT::decode($token, env('JWT_SECRET', ''));
|
||||
if (!$decoded) outputErrorImage('Forbidden: Invalid token');
|
||||
|
||||
$db = Database::getInstance();
|
||||
|
||||
@@ -59,17 +59,18 @@ try {
|
||||
|
||||
$invoices = $stmt->fetchAll();
|
||||
|
||||
// 3. Decrypt sensitive fields for display
|
||||
// 3. Decrypt sensitive fields for display (Robustly)
|
||||
$decrypt = fn($val) => Encryption::decrypt($val ?? '') ?: ($val ?? '-');
|
||||
foreach ($invoices as &$inv) {
|
||||
$inv['supplier_name'] = Encryption::decrypt($inv['supplier_name'] ?? '') ?: ($inv['supplier_name'] ?? '-');
|
||||
$inv['supplier_tin'] = Encryption::decrypt($inv['supplier_tin'] ?? '') ?: ($inv['supplier_tin'] ?? '-');
|
||||
$inv['buyer_name'] = Encryption::decrypt($inv['buyer_name'] ?? '') ?: ($inv['buyer_name'] ?? '-');
|
||||
$inv['supplier_name'] = $decrypt($inv['supplier_name']);
|
||||
$inv['supplier_tin'] = $decrypt($inv['supplier_tin']);
|
||||
$inv['buyer_name'] = $decrypt($inv['buyer_name']);
|
||||
|
||||
if (!empty($inv['company_name'])) {
|
||||
$inv['company_name'] = Encryption::decrypt($inv['company_name']) ?: $inv['company_name'];
|
||||
$inv['company_name'] = $decrypt($inv['company_name']);
|
||||
}
|
||||
if (!empty($inv['tenant_name'])) {
|
||||
$inv['tenant_name'] = Encryption::decrypt($inv['tenant_name']) ?: $inv['tenant_name'];
|
||||
$inv['tenant_name'] = $decrypt($inv['tenant_name']);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -43,16 +43,18 @@ try {
|
||||
$stmtLines->execute([$id]);
|
||||
$invoice['items'] = $stmtLines->fetchAll();
|
||||
|
||||
// 5. Decrypt Fields
|
||||
$invoice['supplier_tin'] = Encryption::decrypt($invoice['supplier_tin'] ?? '') ?: $invoice['supplier_tin'];
|
||||
$invoice['supplier_name'] = Encryption::decrypt($invoice['supplier_name'] ?? '') ?: $invoice['supplier_name'];
|
||||
$invoice['supplier_address'] = Encryption::decrypt($invoice['supplier_address'] ?? '') ?: $invoice['supplier_address'];
|
||||
$invoice['buyer_tin'] = Encryption::decrypt($invoice['buyer_tin'] ?? '') ?: $invoice['buyer_tin'];
|
||||
$invoice['buyer_name'] = Encryption::decrypt($invoice['buyer_name'] ?? '') ?: $invoice['buyer_name'];
|
||||
$invoice['buyer_national_id'] = Encryption::decrypt($invoice['buyer_national_id'] ?? '') ?: $invoice['buyer_national_id'];
|
||||
// 5. Decrypt Fields (Robustly)
|
||||
$decrypt = fn($val) => Encryption::decrypt($val ?? '') ?: $val;
|
||||
|
||||
$invoice['supplier_tin'] = $decrypt($invoice['supplier_tin']);
|
||||
$invoice['supplier_name'] = $decrypt($invoice['supplier_name']);
|
||||
$invoice['supplier_address'] = $decrypt($invoice['supplier_address']);
|
||||
$invoice['buyer_tin'] = $decrypt($invoice['buyer_tin']);
|
||||
$invoice['buyer_name'] = $decrypt($invoice['buyer_name']);
|
||||
$invoice['buyer_national_id'] = $decrypt($invoice['buyer_national_id']);
|
||||
|
||||
if (!empty($invoice['company_name'])) {
|
||||
$invoice['company_name'] = Encryption::decrypt($invoice['company_name']) ?: $invoice['company_name'];
|
||||
$invoice['company_name'] = $decrypt($invoice['company_name']);
|
||||
}
|
||||
|
||||
// 6. Generate Public URL for File (Assuming storage is symlinked or served)
|
||||
|
||||
Reference in New Issue
Block a user