diff --git a/.DS_Store b/.DS_Store
index b2a6b3e..900d9a3 100644
Binary files a/.DS_Store and b/.DS_Store differ
diff --git a/app/core/AI.php b/app/core/AI.php
index 12fb641..7ef11a8 100644
--- a/app/core/AI.php
+++ b/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" => [
diff --git a/app/core/JoFotara.php b/app/core/JoFotara.php
index cc8bf1f..e25bf61 100644
--- a/app/core/JoFotara.php
+++ b/app/core/JoFotara.php
@@ -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 "This will be full UBL 2.1 XML";
+ $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 = <<
+
+
+ 2.1
+ urn:www.cen.eu:en16931:2017#compliant#urn:www.josefotara.jo:trns:ubl:3.0
+ reporting:1.0
+ {$invoice['invoice_number']}
+ {$issueDate}
+ {$issueTime}
+ {$typeCode}
+ JOD
+ JOD
+
+
+
+ {$supplierName}
+
+ {$supplierAddress}
+ JO
+
+
+ {$company['tax_identification_number']}
+ VAT
+
+
+ {$supplierName}
+
+
+
+
+
+
+ {$buyerName}
+
+ {$buyerId}
+ VAT
+
+
+
+
+
+ {$payMethod}
+
+
+
+ {$this->fmt($invoice['tax_amount'])}
+
+ {$this->fmt($invoice['subtotal'] - $invoice['discount_total'])}
+ {$this->fmt($invoice['tax_amount'])}
+
+ S
+ 16.000
+ VAT
+
+
+
+
+
+ {$this->fmt($invoice['subtotal'])}
+ {$this->fmt($invoice['subtotal'] - $invoice['discount_total'])}
+ {$this->fmt($invoice['grand_total'])}
+ {$this->fmt($invoice['discount_total'])}
+ {$this->fmt($invoice['grand_total'])}
+
+
+ {$this->buildInvoiceLines($invoice['items'])}
+
+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 .= <<
+ {$item['line_number']}
+ {$this->fmt($item['quantity'])}
+ {$this->fmt($item['line_total'])}
+
+ {$this->fmt($taxAmount)}
+
+ {$this->fmt($item['line_total'])}
+ {$this->fmt($taxAmount)}
+
+ {$taxCategory}
+ {$this->fmt($item['tax_rate'] * 100)}
+ VAT
+
+
+
+
+ {$this->xmlEscape($item['description'])}
+
+
+ {$this->fmt($item['unit_price'])}
+
+
+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
+ ];
}
}
diff --git a/app/modules_app/invoices/approve.php b/app/modules_app/invoices/approve.php
index 48c2395..d443820 100644
--- a/app/modules_app/invoices/approve.php
+++ b/app/modules_app/invoices/approve.php
@@ -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']) ?: '';
- $invoice['buyer_name'] = \App\Core\Encryption::decrypt($invoice['buyer_name']) ?: '';
- $invoice['buyer_tin'] = \App\Core\Encryption::decrypt($invoice['buyer_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']);
- // 4. Initialize JoFotara Core
+ 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 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) {
diff --git a/app/modules_app/invoices/upload.php b/app/modules_app/invoices/upload.php
index 100c59a..818c339 100644
--- a/app/modules_app/invoices/upload.php
+++ b/app/modules_app/invoices/upload.php
@@ -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
]);
}
}
diff --git a/scripts/PROJECT_DOCUMENTATION.md b/scripts/PROJECT_DOCUMENTATION.md
index 8653b04..8ef4719 100644
--- a/scripts/PROJECT_DOCUMENTATION.md
+++ b/scripts/PROJECT_DOCUMENTATION.md
@@ -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
+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
+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));
+
+```
+
diff --git a/scripts/schema.sql b/scripts/schema.sql
index 98b7050..76937d7 100644
--- a/scripts/schema.sql
+++ b/scripts/schema.sql
@@ -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,
- 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
-);
+ 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 (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 (