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 (