From b2ed480d93a896ada1d4bd353fba21d8068b28e2 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Tue, 5 May 2026 01:19:43 +0300 Subject: [PATCH] Update: 2026-05-05 01:19:43 --- app/modules_app/invoices/upload.php | 27 ++++++++++++-- app/modules_app/invoices/view.php | 7 ++++ scripts/migrate_phase2.php | 56 +++++++++++++++++++++++++++++ 3 files changed, 88 insertions(+), 2 deletions(-) create mode 100644 scripts/migrate_phase2.php diff --git a/app/modules_app/invoices/upload.php b/app/modules_app/invoices/upload.php index d7c6827..2dcddc6 100644 --- a/app/modules_app/invoices/upload.php +++ b/app/modules_app/invoices/upload.php @@ -80,6 +80,24 @@ if (move_uploaded_file($_FILES['invoice']['tmp_name'], $targetFile)) { json_success(['id' => $db->lastInsertId()], 'تم رفع الفاتورة ولكن فشل استخراج البيانات تلقائياً'); } + // 5.5 Duplicate Prevention Check + $supplierTin = $extracted['supplier']['tin'] ?? ''; + $invoiceNum = $extracted['invoice_number'] ?? ''; + $invoiceDate = $extracted['invoice_date'] ?? ''; + + // Only hash if we have the critical data to avoid false duplicate collisions + $invoiceHash = null; + if ($supplierTin && $invoiceNum && $invoiceDate) { + $rawHashString = $companyId . '_' . $supplierTin . '_' . $invoiceNum . '_' . $invoiceDate; + $invoiceHash = hash('sha256', strtolower($rawHashString)); + + $checkStmt = $db->prepare("SELECT id FROM invoices WHERE company_id = ? AND invoice_hash = ? AND deleted_at IS NULL"); + $checkStmt->execute([$companyId, $invoiceHash]); + if ($checkStmt->fetch()) { + json_error('هذه الفاتورة تم رفعها مسبقاً لهذه الشركة (رقم الفاتورة مكرر لنفس المورد والتاريخ).', 409); + } + } + // 6. Save Extracted Data with Encryption try { $db->beginTransaction(); @@ -97,11 +115,14 @@ if (move_uploaded_file($_FILES['invoice']['tmp_name'], $targetFile)) { supplier_tin, supplier_name, supplier_address, buyer_tin, buyer_name, buyer_national_id, subtotal, tax_amount, discount_total, grand_total, currency_code, + invoice_hash, validation_warnings, created_at ) VALUES ( :id, :tenant_id, :company_id, :uploaded_by, :path, 'extracted', :num, :date, :type, :cat, :s_tin, :s_name, :s_addr, :b_tin, :b_name, :b_nid, - :sub, :tax, :disc, :total, :cur, NOW() + :sub, :tax, :disc, :total, :cur, + :hash, :warnings, + NOW() ) "); @@ -125,7 +146,9 @@ if (move_uploaded_file($_FILES['invoice']['tmp_name'], $targetFile)) { 'tax' => $extracted['tax_amount'] ?? 0, 'disc' => $extracted['discount_total'] ?? 0, 'total' => $extracted['grand_total'] ?? 0, - 'cur' => $extracted['currency_code'] ?? 'JOD' + 'cur' => $extracted['currency_code'] ?? 'JOD', + 'hash' => $invoiceHash, + 'warnings' => isset($extracted['validation_warnings']) && !empty($extracted['validation_warnings']) ? json_encode($extracted['validation_warnings']) : null ]); // Save Line Items diff --git a/app/modules_app/invoices/view.php b/app/modules_app/invoices/view.php index 48d50f1..1f77a64 100644 --- a/app/modules_app/invoices/view.php +++ b/app/modules_app/invoices/view.php @@ -61,6 +61,13 @@ try { // company_name is stored plaintext in the companies table — no decryption needed // $invoice['company_name'] is already plaintext from the JOIN + // 3.5 Parse Validation Warnings + if (isset($invoice['validation_warnings']) && $invoice['validation_warnings']) { + $invoice['validation_warnings'] = json_decode($invoice['validation_warnings'], true); + } else { + $invoice['validation_warnings'] = []; + } + // 4. Fetch JoFotara Submission Data (latest accepted submission) $stmtSub = $db->prepare(" SELECT jofotara_uuid, submitted_at, qr_code_raw, response_body diff --git a/scripts/migrate_phase2.php b/scripts/migrate_phase2.php new file mode 100644 index 0000000..4b99b66 --- /dev/null +++ b/scripts/migrate_phase2.php @@ -0,0 +1,56 @@ + "ALTER TABLE invoices ADD COLUMN invoice_hash VARCHAR(64) NULL", + + // 2. Add validation_warnings for AI Pre-Audit + 'add_validation_warnings' => "ALTER TABLE invoices ADD COLUMN validation_warnings JSON NULL", + + // 3. Create Unique Index to prevent duplicates within the same tenant & company + // Using a regular index for now, application logic will handle uniqueness to allow nulls + 'add_hash_index' => "CREATE INDEX idx_invoice_hash ON invoices(tenant_id, company_id, invoice_hash)", +]; + +$success = 0; +$skipped = 0; +$failed = 0; + +foreach ($migrations as $name => $sql) { + try { + $db->exec($sql); + echo " ✅ {$name}\n"; + $success++; + } catch (\PDOException $e) { + $msg = $e->getMessage(); + // Ignore "duplicate column" (1060), "duplicate key name" (1061), or "already exists" errors + if (str_contains($msg, 'Duplicate column') || str_contains($msg, 'Duplicate key name') || str_contains($msg, 'already exists')) { + echo " ⏭️ {$name} (already exists)\n"; + $skipped++; + } else { + echo " ❌ {$name}: {$msg}\n"; + $failed++; + } + } +} + +echo "\n═══════════════════════════════════════════\n"; +echo " Migration Complete!\n"; +echo " ✅ Success: {$success} | ⏭️ Skipped: {$skipped} | ❌ Failed: {$failed}\n"; +echo "═══════════════════════════════════════════\n";