Update: 2026-05-04 14:40:41

This commit is contained in:
Hamza-Ayed
2026-05-04 14:40:41 +03:00
parent ebb70e657e
commit 863dabc069
7 changed files with 448 additions and 134 deletions

BIN
.DS_Store vendored

Binary file not shown.

View File

@@ -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.
$prompt = "أنت نظام متخصص في استخلاص بيانات الفواتير التجارية. مهمتك واحدة فقط: استخراج البيانات من الفاتورة المرفقة بدقة تامة.
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.
## قواعد صارمة:
**اللغة:**
- إذا كانت الفاتورة بالعربية: أبقِ جميع أسماء السلع والعناوين بالعربية بدون ترجمة
- إذا كانت بالإنجليزية: أبقِها بالإنجليزية بدون ترجمة
- الأرقام دائماً بالأرقام اللاتينية (0-9) بغض النظر عن لغة الفاتورة
- المبالغ بـ 3 أرقام عشرية (مثال: 15.000 وليس 15)
Required JSON Structure:
**الدقة:**
- لا تخترع أي بيانات غير موجودة في الفاتورة — أعد 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\": [
{
\"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,
\"line_number\": 1,
\"description\": \"string\",
\"quantity\": 0.000,
\"unit_price\": 0.000,
\"tax_amount\": 0.000,
\"total\": 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" => [

View File

@@ -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
];
}
}

View File

@@ -20,68 +20,91 @@ if (!$id) {
try {
$db->beginTransaction();
// 1. Fetch Invoice
$stmt = $db->prepare("SELECT * FROM invoices WHERE id = ? FOR UPDATE");
// 1. Fetch Invoice & Company
$stmt = $db->prepare("
SELECT i.*, c.name as company_name, c.tax_identification_number as company_tin,
c.address as company_address, c.jofotara_client_id_encrypted, c.jofotara_secret_key_encrypted
FROM invoices i
JOIN companies c ON i.company_id = c.id
WHERE i.id = ? FOR UPDATE
");
$stmt->execute([$id]);
$invoice = $stmt->fetch();
if (!$invoice) {
json_error('Invoice not found', 404);
}
if ($invoice['status'] === 'approved') {
json_error('Invoice is already approved', 400);
}
// Authorization
if ($decoded['role'] !== 'super_admin' && $invoice['tenant_id'] !== $decoded['tenant_id']) {
json_error('Unauthorized', 403);
}
if (!$invoice) json_error('Invoice not found', 404);
if ($invoice['status'] === 'approved') json_error('Already approved', 400);
// 2. Fetch Line Items
$stmtLines = $db->prepare("SELECT * FROM invoice_lines WHERE invoice_id = ?");
$stmtLines->execute([$id]);
$invoice['items'] = $stmtLines->fetchAll();
// 3. Decrypt Sensitive Data for XML Generation
$invoice['supplier_name'] = \App\Core\Encryption::decrypt($invoice['supplier_name']) ?: '';
$invoice['supplier_tin'] = \App\Core\Encryption::decrypt($invoice['supplier_tin']) ?: '';
// 3. Decrypt Company Keys for JoFotara
$clientId = \App\Core\Encryption::decrypt($invoice['jofotara_client_id_encrypted']);
$secretKey = \App\Core\Encryption::decrypt($invoice['jofotara_secret_key_encrypted']);
if (!$clientId || !$secretKey) {
throw new \Exception("JoFotara credentials missing for company: " . $invoice['company_name']);
}
// Decrypt Buyer Info
$invoice['buyer_name'] = \App\Core\Encryption::decrypt($invoice['buyer_name']) ?: '';
$invoice['buyer_tin'] = \App\Core\Encryption::decrypt($invoice['buyer_tin']) ?: '';
// 4. Initialize JoFotara Core
// 4. Initialize JoFotara Service
$jofotara = new JoFotara();
// 5. Generate TLV QR Code Base64
$qrBase64 = $jofotara->generateQRCode($invoice);
// 5. Generate UBL 2.1 XML
$companyData = [
'name' => $invoice['company_name'],
'tax_identification_number' => $invoice['company_tin'],
'address' => $invoice['company_address']
];
$xmlContent = $jofotara->generateXML($invoice, $companyData);
// 6. Generate UBL 2.1 XML
$xmlContent = $jofotara->generateXML($invoice);
// 6. Submit to JoFotara API
$apiResponse = $jofotara->submitInvoice($xmlContent, $clientId, $secretKey);
// 7. Submit to JoFotara API (Simulation for now)
$apiResponse = $jofotara->submitInvoice($xmlContent);
// 7. Record Submission (Audit Log)
$submissionId = \App\Core\Database::generateUuid();
$stmtSub = $db->prepare("
INSERT INTO jofotara_submissions
(id, invoice_id, company_id, tenant_id, xml_payload, xml_hash,
jofotara_uuid, qr_code_raw, response_code, response_body, status, submitted_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, NOW())
");
$status = $apiResponse['success'] ? 'accepted' : 'rejected';
$stmtSub->execute([
$submissionId,
$id,
$invoice['company_id'],
$invoice['tenant_id'],
$xmlContent,
hash('sha256', $xmlContent),
$apiResponse['uuid'] ?? null,
$apiResponse['qrCode'] ?? null,
$apiResponse['_http_code'] ?? '0',
json_encode($apiResponse['raw'] ?? []),
$status
]);
if (!$apiResponse['success']) {
throw new \Exception("JoFotara Rejection: " . ($apiResponse['error'] ?? 'Unknown Error'));
}
// 8. Update Invoice Status & Save JoFotara UUID/QR
// 8. Update Invoice
$updateStmt = $db->prepare("
UPDATE invoices
SET status = 'approved',
jofotara_uuid = ?,
qr_code = ?,
updated_at = NOW()
WHERE id = ?
UPDATE invoices SET status = 'approved', jofotara_uuid = ?, qr_code = ?, updated_at = NOW() WHERE id = ?
");
$updateStmt->execute([$apiResponse['uuid'] ?? 'mock-uuid', $qrBase64, $id]);
$updateStmt->execute([$apiResponse['uuid'], $apiResponse['qrCode'], $id]);
$db->commit();
json_success([
'message' => 'Invoice approved and submitted to JoFotara successfully.',
'jofotara_uuid' => $apiResponse['uuid'] ?? 'mock-uuid',
'qr_code' => $qrBase64
'message' => 'Approved and submitted to JoFotara.',
'uuid' => $apiResponse['uuid'],
'qr_code' => $apiResponse['qrCode']
]);
} catch (\Exception $e) {

View File

@@ -108,41 +108,34 @@ if (move_uploaded_file($_FILES['invoice']['tmp_name'], $targetFile)) {
'date' => $extracted['invoice_date'] ?? null,
'type' => $extracted['invoice_type'] ?? 'cash',
'cat' => $extracted['invoice_category'] ?? 'simplified',
's_tin' => \App\Core\Encryption::encrypt($extracted['supplier_tin'] ?? ''),
's_name' => \App\Core\Encryption::encrypt($extracted['supplier_name'] ?? ''),
's_addr' => \App\Core\Encryption::encrypt($extracted['supplier_address'] ?? ''),
'b_tin' => \App\Core\Encryption::encrypt($extracted['buyer_tin'] ?? ''),
'b_name' => \App\Core\Encryption::encrypt($extracted['buyer_name'] ?? ''),
'b_nid' => \App\Core\Encryption::encrypt($extracted['buyer_national_id'] ?? ''),
's_tin' => \App\Core\Encryption::encrypt($extracted['supplier']['tin'] ?? ''),
's_name' => \App\Core\Encryption::encrypt($extracted['supplier']['name'] ?? ''),
's_addr' => \App\Core\Encryption::encrypt($extracted['supplier']['address'] ?? ''),
'b_tin' => \App\Core\Encryption::encrypt($extracted['buyer']['tin'] ?? ''),
'b_name' => \App\Core\Encryption::encrypt($extracted['buyer']['name'] ?? ''),
'b_nid' => \App\Core\Encryption::encrypt($extracted['buyer']['national_id'] ?? ''),
'sub' => $extracted['subtotal'] ?? 0,
'tax' => $extracted['tax_amount'] ?? 0,
'disc' => $extracted['discount_total'] ?? 0,
'total' => $extracted['grand_total'] ?? 0,
'cur' => $extracted['currency'] ?? 'JOD'
'cur' => $extracted['currency_code'] ?? 'JOD'
]);
// Save Line Items
if (!empty($extracted['items'])) {
if (!empty($extracted['lines'])) {
$lineStmt = $db->prepare("
INSERT INTO invoice_lines (invoice_id, line_number, description, quantity, unit_price, tax_rate, line_total)
VALUES (?, ?, ?, ?, ?, ?, ?)
");
$lineNo = 1;
foreach ($extracted['items'] as $item) {
// Calculate tax rate if not provided (fallback to 0.16 for Jordan)
$taxRate = 0.16;
if (!empty($item['unit_price']) && !empty($item['tax_amount'])) {
$taxRate = round($item['tax_amount'] / ($item['unit_price'] * ($item['quantity'] ?: 1)), 4);
}
foreach ($extracted['lines'] as $item) {
$lineStmt->execute([
$invoiceId,
$lineNo++,
$item['line_number'] ?? 1,
$item['description'] ?? 'N/A',
$item['quantity'] ?? 1,
$item['unit_price'] ?? 0,
$taxRate,
$item['total'] ?? 0
$item['tax_rate'] ?? 0.16,
$item['line_total'] ?? 0
]);
}
}

View File

@@ -153,22 +153,24 @@ CREATE TABLE invoices (
invoice_type ENUM('cash','credit') DEFAULT 'cash',
ubl_type_code CHAR(3) DEFAULT '388',
payment_method_code CHAR(3) DEFAULT '013',
supplier_tin VARCHAR(20) NULL,
supplier_name VARCHAR(255) NULL,
supplier_tin TEXT NULL,
supplier_name TEXT NULL,
supplier_address TEXT NULL,
buyer_tin VARCHAR(20) NULL,
buyer_national_id VARCHAR(20) NULL,
buyer_name VARCHAR(255) NULL,
buyer_tin TEXT NULL,
buyer_national_id TEXT NULL,
buyer_name TEXT NULL,
subtotal DECIMAL(15,3) DEFAULT 0,
discount_total DECIMAL(15,3) DEFAULT 0,
tax_amount DECIMAL(15,3) DEFAULT 0,
grand_total DECIMAL(15,3) DEFAULT 0,
currency_code CHAR(3) DEFAULT 'JOD',
status ENUM('uploaded','extracting','extracted','validated','validation_failed','submitting','approved','rejected') DEFAULT 'uploaded',
status ENUM('extracted', 'approved', 'rejected') DEFAULT 'extracted',
jofotara_uuid VARCHAR(255) NULL,
qr_code TEXT NULL,
invoice_number VARCHAR(50) NULL,
original_file_path TEXT NULL,
invoice_category VARCHAR(20) DEFAULT 'simplified',
validation_errors JSON NULL,
qr_code TEXT NULL,
ai_confidence_score DECIMAL(4,3) NULL,
ai_prompt_tokens INT DEFAULT 0,
ai_completion_tokens INT DEFAULT 0,
@@ -309,3 +311,110 @@ echo "--- Migration Complete ---\n";
```
## File: `seed_super_admin.php`
```php
<?php
/**
* Seed Super Admin Script
* Run this from CLI: php scripts/seed_super_admin.php
*/
require_once __DIR__ . '/../app/bootstrap/init.php';
use App\Core\Database;
use App\Core\Encryption;
$db = Database::getInstance();
echo "--- Starting Super Admin Seeding ---\n";
try {
$db->beginTransaction();
// 1. We must create a "System Tenant" for the Super Admin to satisfy the Foreign Key constraint
$systemTenantId = '00000000-0000-0000-0000-000000000000';
// Check if system tenant exists
$stmt = $db->prepare("SELECT id FROM tenants WHERE id = ?");
$stmt->execute([$systemTenantId]);
if (!$stmt->fetch()) {
$stmt = $db->prepare("INSERT INTO tenants (id, name, email, status, created_at) VALUES (?, 'System Administration', 'system@musadaq.com', 'active', NOW())");
$stmt->execute([$systemTenantId]);
echo "[OK] System Tenant created.\n";
}
// 2. Setup Super Admin details
$adminEmail = 'admin@musadaq.app';
$adminName = 'Hamza';
$adminPassword = 'password123'; // Default password
// Check if user already exists
$emailHash = hash('sha256', strtolower($adminEmail));
$stmt = $db->prepare("SELECT id FROM users WHERE email_hash = ?");
$stmt->execute([$emailHash]);
if ($stmt->fetch()) {
echo "[INFO] Super Admin already exists with this email.\n";
} else {
$adminId = sprintf('%04x%04x-%04x-%04x-%04x-%04x%04x%04x',
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff),
mt_rand(0, 0x0fff) | 0x4000, mt_rand(0, 0x3fff) | 0x8000,
mt_rand(0, 0xffff), mt_rand(0, 0xffff), mt_rand(0, 0xffff)
);
$encryptedName = Encryption::encrypt($adminName);
$encryptedEmail = Encryption::encrypt($adminEmail);
$passwordHash = password_hash($adminPassword, PASSWORD_DEFAULT);
$stmt = $db->prepare("INSERT INTO users (id, tenant_id, name, email, email_hash, password_hash, role, is_active, created_at) VALUES (?, ?, ?, ?, ?, ?, 'super_admin', 1, NOW())");
$stmt->execute([
$adminId,
$systemTenantId,
$encryptedName,
$encryptedEmail,
$emailHash,
$passwordHash
]);
echo "[OK] Super Admin created successfully!\n";
echo "----------------------------------------\n";
echo "Email: $adminEmail\n";
echo "Password: $adminPassword\n";
echo "Role: super_admin\n";
echo "----------------------------------------\n";
}
$db->commit();
echo "--- Seeding Complete ---\n";
} catch (\Exception $e) {
$db->rollBack();
echo "[ERROR] Seeding failed: " . $e->getMessage() . "\n";
}
```
## File: `debug_data.php`
```php
<?php
require_once __DIR__ . '/../app/bootstrap/init.php';
use App\Core\Database;
$db = Database::getInstance();
echo "--- TENANTS ---\n";
$stmt = $db->query("SELECT * FROM tenants");
print_r($stmt->fetchAll(PDO::FETCH_ASSOC));
echo "\n--- USERS ---\n";
$stmt = $db->query("SELECT u.id, u.name, u.role, u.tenant_id, t.name as tenant_name FROM users u LEFT JOIN tenants t ON u.tenant_id = t.id");
print_r($stmt->fetchAll(PDO::FETCH_ASSOC));
echo "\n--- COMPANIES ---\n";
$stmt = $db->query("SELECT * FROM companies");
print_r($stmt->fetchAll(PDO::FETCH_ASSOC));
```

View File

@@ -109,7 +109,6 @@ CREATE TABLE invoices (
status ENUM('extracted', 'approved', 'rejected') DEFAULT 'extracted',
jofotara_uuid VARCHAR(255) NULL,
qr_code TEXT NULL,
invoice_number VARCHAR(50) NULL,
original_file_path TEXT NULL,
invoice_category VARCHAR(20) DEFAULT 'simplified',
validation_errors JSON NULL,
@@ -119,12 +118,37 @@ CREATE TABLE invoices (
ai_total_cost DECIMAL(10,6) DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
INDEX idx_tenant (tenant_id),
INDEX idx_company (company_id),
INDEX idx_status (status),
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE,
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE
);
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
FOREIGN KEY (uploaded_by) REFERENCES users(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- JoFotara Submissions (Audit Trail)
CREATE TABLE jofotara_submissions (
id CHAR(36) PRIMARY KEY DEFAULT (UUID()),
invoice_id CHAR(36) NOT NULL,
company_id CHAR(36) NOT NULL,
tenant_id CHAR(36) NOT NULL,
xml_payload LONGTEXT NULL,
xml_hash VARCHAR(64) NULL,
jofotara_uuid VARCHAR(255) NULL,
qr_code_raw TEXT NULL,
response_code VARCHAR(20) NULL,
response_body JSON NULL,
status ENUM('pending','submitted','accepted','rejected','error') DEFAULT 'pending',
error_message TEXT NULL,
retry_count TINYINT DEFAULT 0,
submitted_at DATETIME NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (invoice_id) REFERENCES invoices(id) ON DELETE CASCADE,
FOREIGN KEY (company_id) REFERENCES companies(id) ON DELETE CASCADE,
FOREIGN KEY (tenant_id) REFERENCES tenants(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
-- Invoice Lines
CREATE TABLE invoice_lines (