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 @@
+