Update: 2026-05-04 14:40:41
This commit is contained in:
102
app/core/AI.php
102
app/core/AI.php
@@ -20,45 +20,69 @@ class AI
|
||||
return null;
|
||||
}
|
||||
|
||||
$prompt = "You are an expert in Jordanian E-Invoicing (UBL 2.1).
|
||||
Extract all data from this invoice image/document into a JSON format.
|
||||
|
||||
CRITICAL RULES:
|
||||
1. DO NOT TRANSLATE ANY TEXT. Keep the exact original language (if Arabic, keep Arabic).
|
||||
2. ALL numbers and quantities MUST be in Latin numerals (0-9). Do not use Arabic/Indic numerals (٠-٩).
|
||||
3. Identify the Supplier TIN (Tax Identification Number) and Buyer TIN (if present).
|
||||
4. Identify if the invoice is 'Cash' or 'Credit'.
|
||||
5. Identify if it is 'Simplified' (B2C) or 'Standard' (B2B).
|
||||
6. Extract line items precisely.
|
||||
7. Return ONLY valid JSON, no markdown formatting.
|
||||
|
||||
Required JSON Structure:
|
||||
{
|
||||
\"invoice_number\": \"\",
|
||||
\"invoice_date\": \"YYYY-MM-DD\",
|
||||
\"invoice_type\": \"cash|credit\",
|
||||
\"invoice_category\": \"simplified|standard\",
|
||||
\"supplier_tin\": \"\",
|
||||
\"supplier_name\": \"\",
|
||||
\"supplier_address\": \"\",
|
||||
\"buyer_tin\": \"\",
|
||||
\"buyer_name\": \"\",
|
||||
\"buyer_national_id\": \"\",
|
||||
\"subtotal\": 0.000,
|
||||
\"tax_amount\": 0.000,
|
||||
\"discount_total\": 0.000,
|
||||
\"grand_total\": 0.000,
|
||||
\"currency\": \"JOD\",
|
||||
\"items\": [
|
||||
{
|
||||
\"description\": \"\",
|
||||
\"quantity\": 0,
|
||||
\"unit_price\": 0.000,
|
||||
\"tax_amount\": 0.000,
|
||||
\"total\": 0.000
|
||||
}
|
||||
]
|
||||
}";
|
||||
$prompt = "أنت نظام متخصص في استخلاص بيانات الفواتير التجارية. مهمتك واحدة فقط: استخراج البيانات من الفاتورة المرفقة بدقة تامة.
|
||||
|
||||
## قواعد صارمة:
|
||||
**اللغة:**
|
||||
- إذا كانت الفاتورة بالعربية: أبقِ جميع أسماء السلع والعناوين بالعربية بدون ترجمة
|
||||
- إذا كانت بالإنجليزية: أبقِها بالإنجليزية بدون ترجمة
|
||||
- الأرقام دائماً بالأرقام اللاتينية (0-9) بغض النظر عن لغة الفاتورة
|
||||
- المبالغ بـ 3 أرقام عشرية (مثال: 15.000 وليس 15)
|
||||
|
||||
**الدقة:**
|
||||
- لا تخترع أي بيانات غير موجودة في الفاتورة — أعد null إذا لم تجد المعلومة
|
||||
- تحقق رياضياً: subtotal = مجموع (quantity × unit_price - discount) لكل سطر
|
||||
- تحقق: grand_total = subtotal - discount_total + tax_amount
|
||||
- إذا وجدت تناقضاً بين الأرقام في الفاتورة، سجِّله في حقل \"validation_warnings\"
|
||||
|
||||
**الضريبة:**
|
||||
- في الأردن: ضريبة المبيعات العامة (GST) = 16% للسلع العامة
|
||||
- سلع معفاة من الضريبة: المواد الغذائية الأساسية، الأدوية، الكتب، بعض المعدات الطبية
|
||||
- سلع بضريبة مخفضة: قد تكون 4% أو 8% — استخرج النسبة الفعلية من الفاتورة
|
||||
- لكل سطر: حدد tax_rate الفعلي (0 للمعفاة، وإلا النسبة المئوية كعدد عشري مثل 0.16)
|
||||
|
||||
## البيانات المطلوبة (JSON فقط، بدون أي نص إضافي):
|
||||
|
||||
```json
|
||||
{
|
||||
\"invoice_number\": \"string | null\",
|
||||
\"invoice_date\": \"YYYY-MM-DD | null\",
|
||||
\"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\"
|
||||
},
|
||||
\"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\",
|
||||
\"line_total\": 0.000
|
||||
}
|
||||
],
|
||||
\"subtotal\": 0.000,
|
||||
\"discount_total\": 0.000,
|
||||
\"tax_amount\": 0.000,
|
||||
\"grand_total\": 0.000,
|
||||
\"currency_code\": \"JOD\",
|
||||
\"math_verified\": true,
|
||||
\"validation_warnings\": [],
|
||||
\"ai_confidence\": 0.95
|
||||
}
|
||||
```
|
||||
|
||||
أعد JSON فقط بدون أي شرح أو مقدمة أو علامات Markdown.";
|
||||
|
||||
$payload = [
|
||||
"contents" => [
|
||||
|
||||
@@ -8,35 +8,140 @@ namespace App\Core;
|
||||
*/
|
||||
class JoFotara
|
||||
{
|
||||
private string $clientId;
|
||||
private string $secretKey;
|
||||
private string $environment;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
// Load credentials from DB or Environment
|
||||
$this->clientId = env('JOFOTARA_CLIENT_ID', '');
|
||||
$this->secretKey = env('JOFOTARA_SECRET', '');
|
||||
$this->environment = env('JOFOTARA_ENV', 'sandbox'); // sandbox or production
|
||||
}
|
||||
private string $baseUrl = 'https://backend.jofotara.gov.jo/core/invoices/';
|
||||
|
||||
/**
|
||||
* 1. Generate UBL 2.1 XML for an invoice
|
||||
*/
|
||||
public function generateXML(array $invoiceData): string
|
||||
public function generateXML(array $invoice, array $company): string
|
||||
{
|
||||
// To be implemented: Full XML DOM Document generation based on UBL 2.1 schema
|
||||
// This will map $invoiceData (Supplier, Buyer, Lines, Taxes) to exact XML nodes.
|
||||
return "<Invoice><dummy>This will be full UBL 2.1 XML</dummy></Invoice>";
|
||||
$issueDate = $invoice['invoice_date'] ?? date('Y-m-d');
|
||||
$issueTime = date('H:i:s');
|
||||
$typeCode = $invoice['ubl_type_code'] ?? '388';
|
||||
$category = $invoice['invoice_category'] ?? 'simplified';
|
||||
|
||||
// Prepare data outside heredoc for clean interpolation
|
||||
$buyerName = $this->xmlEscape($invoice['buyer_name'] ?: 'عميل نقدي');
|
||||
$buyerId = $invoice['buyer_tin'] ?: $invoice['buyer_national_id'] ?: '000000000';
|
||||
$payMethod = $invoice['payment_method_code'] ?: '013';
|
||||
$supplierName = $this->xmlEscape($company['name']);
|
||||
$supplierAddress = $this->xmlEscape($company['address'] ?? '');
|
||||
|
||||
$xml = <<<XML
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Invoice xmlns="urn:oasis:names:specification:ubl:schema:xsd:Invoice-2"
|
||||
xmlns:cac="urn:oasis:names:specification:ubl:schema:xsd:CommonAggregateComponents-2"
|
||||
xmlns:cbc="urn:oasis:names:specification:ubl:schema:xsd:CommonBasicComponents-2">
|
||||
|
||||
<cbc:UBLVersionID>2.1</cbc:UBLVersionID>
|
||||
<cbc:CustomizationID>urn:www.cen.eu:en16931:2017#compliant#urn:www.josefotara.jo:trns:ubl:3.0</cbc:CustomizationID>
|
||||
<cbc:ProfileID>reporting:1.0</cbc:ProfileID>
|
||||
<cbc:ID>{$invoice['invoice_number']}</cbc:ID>
|
||||
<cbc:IssueDate>{$issueDate}</cbc:IssueDate>
|
||||
<cbc:IssueTime>{$issueTime}</cbc:IssueTime>
|
||||
<cbc:InvoiceTypeCode name="{$category}">{$typeCode}</cbc:InvoiceTypeCode>
|
||||
<cbc:DocumentCurrencyCode>JOD</cbc:DocumentCurrencyCode>
|
||||
<cbc:TaxCurrencyCode>JOD</cbc:TaxCurrencyCode>
|
||||
|
||||
<cac:AccountingSupplierParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName><cbc:Name>{$supplierName}</cbc:Name></cac:PartyName>
|
||||
<cac:PostalAddress>
|
||||
<cbc:StreetName>{$supplierAddress}</cbc:StreetName>
|
||||
<cac:Country><cbc:IdentificationCode>JO</cbc:IdentificationCode></cac:Country>
|
||||
</cac:PostalAddress>
|
||||
<cac:PartyTaxScheme>
|
||||
<cbc:CompanyID>{$company['tax_identification_number']}</cbc:CompanyID>
|
||||
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
|
||||
</cac:PartyTaxScheme>
|
||||
<cac:PartyLegalEntity>
|
||||
<cbc:RegistrationName>{$supplierName}</cbc:RegistrationName>
|
||||
</cac:PartyLegalEntity>
|
||||
</cac:Party>
|
||||
</cac:AccountingSupplierParty>
|
||||
|
||||
<cac:AccountingCustomerParty>
|
||||
<cac:Party>
|
||||
<cac:PartyName><cbc:Name>{$buyerName}</cbc:Name></cac:PartyName>
|
||||
<cac:PartyTaxScheme>
|
||||
<cbc:CompanyID>{$buyerId}</cbc:CompanyID>
|
||||
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
|
||||
</cac:PartyTaxScheme>
|
||||
</cac:Party>
|
||||
</cac:AccountingCustomerParty>
|
||||
|
||||
<cac:PaymentMeans>
|
||||
<cbc:PaymentMeansCode>{$payMethod}</cbc:PaymentMeansCode>
|
||||
</cac:PaymentMeans>
|
||||
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="JOD">{$this->fmt($invoice['tax_amount'])}</cbc:TaxAmount>
|
||||
<cac:TaxSubtotal>
|
||||
<cbc:TaxableAmount currencyID="JOD">{$this->fmt($invoice['subtotal'] - $invoice['discount_total'])}</cbc:TaxableAmount>
|
||||
<cbc:TaxAmount currencyID="JOD">{$this->fmt($invoice['tax_amount'])}</cbc:TaxAmount>
|
||||
<cac:TaxCategory>
|
||||
<cbc:ID>S</cbc:ID>
|
||||
<cbc:Percent>16.000</cbc:Percent>
|
||||
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
|
||||
</cac:TaxCategory>
|
||||
</cac:TaxSubtotal>
|
||||
</cac:TaxTotal>
|
||||
|
||||
<cac:LegalMonetaryTotal>
|
||||
<cbc:LineExtensionAmount currencyID="JOD">{$this->fmt($invoice['subtotal'])}</cbc:LineExtensionAmount>
|
||||
<cbc:TaxExclusiveAmount currencyID="JOD">{$this->fmt($invoice['subtotal'] - $invoice['discount_total'])}</cbc:TaxExclusiveAmount>
|
||||
<cbc:TaxInclusiveAmount currencyID="JOD">{$this->fmt($invoice['grand_total'])}</cbc:TaxInclusiveAmount>
|
||||
<cbc:AllowanceTotalAmount currencyID="JOD">{$this->fmt($invoice['discount_total'])}</cbc:AllowanceTotalAmount>
|
||||
<cbc:PayableAmount currencyID="JOD">{$this->fmt($invoice['grand_total'])}</cbc:PayableAmount>
|
||||
</cac:LegalMonetaryTotal>
|
||||
|
||||
{$this->buildInvoiceLines($invoice['items'])}
|
||||
</Invoice>
|
||||
XML;
|
||||
return $xml;
|
||||
}
|
||||
|
||||
private function buildInvoiceLines(array $items): string
|
||||
{
|
||||
$result = '';
|
||||
foreach ($items as $item) {
|
||||
$taxAmount = round($item['line_total'] * $item['tax_rate'], 3);
|
||||
$taxCategory = $item['tax_rate'] > 0 ? 'S' : 'Z';
|
||||
|
||||
$result .= <<<XML
|
||||
<cac:InvoiceLine>
|
||||
<cbc:ID>{$item['line_number']}</cbc:ID>
|
||||
<cbc:InvoicedQuantity unitCode="PCE">{$this->fmt($item['quantity'])}</cbc:InvoicedQuantity>
|
||||
<cbc:LineExtensionAmount currencyID="JOD">{$this->fmt($item['line_total'])}</cbc:LineExtensionAmount>
|
||||
<cac:TaxTotal>
|
||||
<cbc:TaxAmount currencyID="JOD">{$this->fmt($taxAmount)}</cbc:TaxAmount>
|
||||
<cac:TaxSubtotal>
|
||||
<cbc:TaxableAmount currencyID="JOD">{$this->fmt($item['line_total'])}</cbc:TaxableAmount>
|
||||
<cbc:TaxAmount currencyID="JOD">{$this->fmt($taxAmount)}</cbc:TaxAmount>
|
||||
<cac:TaxCategory>
|
||||
<cbc:ID>{$taxCategory}</cbc:ID>
|
||||
<cbc:Percent>{$this->fmt($item['tax_rate'] * 100)}</cbc:Percent>
|
||||
<cac:TaxScheme><cbc:ID>VAT</cbc:ID></cac:TaxScheme>
|
||||
</cac:TaxCategory>
|
||||
</cac:TaxSubtotal>
|
||||
</cac:TaxTotal>
|
||||
<cac:Item>
|
||||
<cbc:Description>{$this->xmlEscape($item['description'])}</cbc:Description>
|
||||
</cac:Item>
|
||||
<cac:Price>
|
||||
<cbc:PriceAmount currencyID="JOD">{$this->fmt($item['unit_price'])}</cbc:PriceAmount>
|
||||
</cac:Price>
|
||||
</cac:InvoiceLine>
|
||||
XML;
|
||||
}
|
||||
return $result;
|
||||
}
|
||||
|
||||
private function fmt(float $val): string { return number_format($val, 3, '.', ''); }
|
||||
private function xmlEscape(string $str): string { return htmlspecialchars($str, ENT_XML1, 'UTF-8'); }
|
||||
|
||||
/**
|
||||
* 2. Generate Base64 TLV QR Code (required by Jordan Tax Authority)
|
||||
* Tag 1: Seller Name
|
||||
* Tag 2: Tax Number
|
||||
* Tag 3: Timestamp
|
||||
* Tag 4: Invoice Total
|
||||
* Tag 5: VAT Total
|
||||
* 2. Generate Base64 TLV QR Code (Local Fallback)
|
||||
*/
|
||||
public function generateQRCode(array $invoiceData): string
|
||||
{
|
||||
@@ -63,10 +168,46 @@ class JoFotara
|
||||
/**
|
||||
* 3. Submit Invoice to JoFotara API
|
||||
*/
|
||||
public function submitInvoice(string $xmlContent): array
|
||||
public function submitInvoice(string $xmlContent, string $clientId, string $secretKey): array
|
||||
{
|
||||
// To be implemented: cURL request to JoFotara Core API
|
||||
// Requires ECDSA signing of the XML before submission
|
||||
return ['success' => true, 'uuid' => 'dummy-jofotara-id'];
|
||||
// For production, we must encode XML in Base64 and wrap in JSON
|
||||
$payload = json_encode([
|
||||
'invoice' => base64_encode($xmlContent)
|
||||
]);
|
||||
|
||||
$ch = curl_init($this->baseUrl);
|
||||
curl_setopt_array($ch, [
|
||||
CURLOPT_RETURNTRANSFER => true,
|
||||
CURLOPT_POST => true,
|
||||
CURLOPT_POSTFIELDS => $payload,
|
||||
CURLOPT_HTTPHEADER => [
|
||||
'Content-Type: application/json',
|
||||
"ClientId: $clientId",
|
||||
"SecretKey: $secretKey"
|
||||
],
|
||||
CURLOPT_TIMEOUT => 30
|
||||
]);
|
||||
|
||||
$response = curl_exec($ch);
|
||||
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
||||
curl_close($ch);
|
||||
|
||||
$decoded = json_decode($response, true) ?? [];
|
||||
$decoded['_http_code'] = $httpCode;
|
||||
|
||||
if ($httpCode === 200) {
|
||||
return [
|
||||
'success' => true,
|
||||
'uuid' => $decoded['invoiceUUID'] ?? $decoded['uuid'] ?? 'mock-' . uniqid(),
|
||||
'qrCode' => $decoded['qrCode'] ?? $decoded['QRCode'] ?? null,
|
||||
'raw' => $decoded
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'error' => $decoded['errorMessage'] ?? 'API Connection Failed',
|
||||
'raw' => $decoded
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user