From ebb70e657e574996a68e0ec3a1c611d216f73f26 Mon Sep 17 00:00:00 2001 From: Hamza-Ayed Date: Mon, 4 May 2026 02:53:16 +0300 Subject: [PATCH] Update: 2026-05-04 02:53:16 --- app/modules_app/invoices/approve.php | 91 ++++++++++++++++++++++++++++ app/modules_app/invoices/file.php | 36 +++++++---- public/index.php | 1 + public/shell.php | 48 ++++++++++++++- scripts/schema.sql | 7 ++- 5 files changed, 165 insertions(+), 18 deletions(-) create mode 100644 app/modules_app/invoices/approve.php diff --git a/app/modules_app/invoices/approve.php b/app/modules_app/invoices/approve.php new file mode 100644 index 0000000..48c2395 --- /dev/null +++ b/app/modules_app/invoices/approve.php @@ -0,0 +1,91 @@ +beginTransaction(); + + // 1. Fetch Invoice + $stmt = $db->prepare("SELECT * FROM invoices WHERE 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); + } + + // 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']) ?: ''; + + // 4. Initialize JoFotara Core + $jofotara = new JoFotara(); + + // 5. Generate TLV QR Code Base64 + $qrBase64 = $jofotara->generateQRCode($invoice); + + // 6. Generate UBL 2.1 XML + $xmlContent = $jofotara->generateXML($invoice); + + // 7. Submit to JoFotara API (Simulation for now) + $apiResponse = $jofotara->submitInvoice($xmlContent); + + if (!$apiResponse['success']) { + throw new \Exception("JoFotara Rejection: " . ($apiResponse['error'] ?? 'Unknown Error')); + } + + // 8. Update Invoice Status & Save JoFotara UUID/QR + $updateStmt = $db->prepare(" + UPDATE invoices + SET status = 'approved', + jofotara_uuid = ?, + qr_code = ?, + updated_at = NOW() + WHERE id = ? + "); + $updateStmt->execute([$apiResponse['uuid'] ?? 'mock-uuid', $qrBase64, $id]); + + $db->commit(); + + json_success([ + 'message' => 'Invoice approved and submitted to JoFotara successfully.', + 'jofotara_uuid' => $apiResponse['uuid'] ?? 'mock-uuid', + 'qr_code' => $qrBase64 + ]); + +} catch (\Exception $e) { + $db->rollBack(); + error_log("JoFotara Approve Error: " . $e->getMessage()); + json_error('Failed to approve invoice: ' . $e->getMessage(), 500); +} diff --git a/app/modules_app/invoices/file.php b/app/modules_app/invoices/file.php index 498c079..7edd8c5 100644 --- a/app/modules_app/invoices/file.php +++ b/app/modules_app/invoices/file.php @@ -6,6 +6,19 @@ use App\Core\Database; use App\Middleware\AuthMiddleware; +// Helper to output error as an image for debugging +function outputErrorImage($message) { + header('Content-Type: image/png'); + $im = imagecreatetruecolor(400, 100); + $bg = imagecolorallocate($im, 20, 20, 20); + $tc = imagecolorallocate($im, 255, 50, 50); + imagefilledrectangle($im, 0, 0, 400, 100, $bg); + imagestring($im, 5, 10, 40, $message, $tc); + imagepng($im); + imagedestroy($im); + exit; +} + // Extract token from header OR query string $headers = getallheaders(); $authHeader = $headers['Authorization'] ?? $headers['authorization'] ?? ''; @@ -17,44 +30,41 @@ if (preg_match('/Bearer\s(\S+)/', $authHeader, $matches)) { $token = $_GET['token']; } -if (!$token) die('Forbidden: No token provided'); +if (!$token) outputErrorImage('Forbidden: No token'); $decoded = \App\Core\JWT::decode($token); -if (!$decoded) die('Forbidden: Invalid token'); +if (!$decoded) outputErrorImage('Forbidden: Invalid token'); $db = Database::getInstance(); - $id = input('id'); -if (!$id) die('Forbidden'); +if (!$id) outputErrorImage('Forbidden: No ID'); $stmt = $db->prepare("SELECT tenant_id, original_file_path FROM invoices WHERE id = ?"); $stmt->execute([$id]); $invoice = $stmt->fetch(); -if (!$invoice) die('Not found'); +if (!$invoice) outputErrorImage('Error: Invoice not found'); // Authorization if ($decoded['role'] !== 'super_admin' && $invoice['tenant_id'] !== $decoded['tenant_id']) { - die('Unauthorized'); + outputErrorImage('Error: Unauthorized'); } $filePath = $invoice['original_file_path']; if (!file_exists($filePath)) { - error_log("FILE PROXY ERROR: File not found at " . $filePath); - header("HTTP/1.0 404 Not Found"); - exit('File missing'); + outputErrorImage('Error: File missing on disk'); } if (!is_readable($filePath)) { - error_log("FILE PROXY ERROR: File not readable at " . $filePath); - header("HTTP/1.0 403 Forbidden"); - exit('Permission denied'); + outputErrorImage('Error: Permission denied'); } $mime = mime_content_type($filePath); +if (!$mime) $mime = 'application/octet-stream'; + header("Content-Type: $mime"); header("Content-Length: " . filesize($filePath)); -header("Cache-Control: public, max-age=3600"); // Add caching for speed +header("Cache-Control: public, max-age=3600"); readfile($filePath); exit; diff --git a/public/index.php b/public/index.php index c0e518c..76bdf83 100644 --- a/public/index.php +++ b/public/index.php @@ -28,6 +28,7 @@ $routes = [ 'v1/invoices' => ['GET', 'invoices/index.php'], 'v1/invoices/view' => ['GET', 'invoices/view.php'], 'v1/invoices/file' => ['GET', 'invoices/file.php'], + 'v1/invoices/approve' => ['POST', 'invoices/approve.php'], 'v1/invoices/upload' => ['POST', 'invoices/upload.php'], 'v1/dashboard/stats' => ['GET', 'dashboard/stats.php'], 'v1/tenants' => ['GET', 'tenants/index.php'], diff --git a/public/shell.php b/public/shell.php index c23ff08..b0131ab 100644 --- a/public/shell.php +++ b/public/shell.php @@ -7,6 +7,7 @@ +