diff --git a/.DS_Store b/.DS_Store index 900d9a3..983635b 100644 Binary files a/.DS_Store and b/.DS_Store differ diff --git a/app/Services/InvoiceExtractionService.php b/app/Services/InvoiceExtractionService.php new file mode 100644 index 0000000..b1cf3eb --- /dev/null +++ b/app/Services/InvoiceExtractionService.php @@ -0,0 +1,165 @@ +buildExtractionPrompt(); $payload = [ "contents" => [ diff --git a/app/core/Database.php b/app/core/Database.php index d4bdb9d..1131de2 100644 --- a/app/core/Database.php +++ b/app/core/Database.php @@ -43,4 +43,9 @@ final class Database return self::$instance; } + + public static function generateUuid(): string + { + return vsprintf('%s%s-%s-%s-%s-%s%s%s', str_split(bin2hex(random_bytes(16)), 4)); + } } diff --git a/app/middleware/AuthMiddleware.php b/app/middleware/AuthMiddleware.php index f1af32f..385e551 100644 --- a/app/middleware/AuthMiddleware.php +++ b/app/middleware/AuthMiddleware.php @@ -31,7 +31,17 @@ final class AuthMiddleware $decoded = JWT::decode($token, $secret); if (!$decoded) { - json_error('Unauthorized: Invalid or expired token', 401); + // Check if it's specifically expired if your JWT class supports it, + // otherwise just send the standard 401 with a code. + http_response_code(401); + header('Content-Type: application/json'); + echo json_encode([ + 'success' => false, + 'message' => 'انتهت صلاحية الجلسة', + 'code' => 'TOKEN_EXPIRED', + 'redirect'=> '/login.php' + ]); + exit; } return $decoded; diff --git a/app/modules_app/companies/connect_jofotara.php b/app/modules_app/companies/connect_jofotara.php new file mode 100644 index 0000000..b4e2d15 --- /dev/null +++ b/app/modules_app/companies/connect_jofotara.php @@ -0,0 +1,65 @@ +prepare("SELECT id FROM companies WHERE id = ? AND tenant_id = ?"); + $stmt->execute([$companyId, $tenantId]); + if (!$stmt->fetch()) json_error('Access denied', 403); + + // 3. Test Connection (Optional but recommended) + $jofotara = new JoFotara(); + // Here you would typically call a health check endpoint if JoFotara provides one, + // or just assume the credentials are correct for now. + + // 4. Update Company with Encrypted Credentials + $stmtUpdate = $db->prepare(" + UPDATE companies + SET + jofotara_client_id_encrypted = ?, + jofotara_secret_key_encrypted = ?, + jofotara_income_source_sequence = ?, + updated_at = NOW() + WHERE id = ? + "); + + $stmtUpdate->execute([ + Encryption::encrypt($clientId), + Encryption::encrypt($secretKey), + $sequence, + $companyId + ]); + + json_success(null, 'تم ربط الشركة بنظام جوفوترة بنجاح'); + +} catch (\Exception $e) { + error_log("JoFotara Connection Error: " . $e->getMessage()); + json_error('فشل في حفظ البيانات: ' . $e->getMessage(), 500); +} diff --git a/app/modules_app/companies/stats.php b/app/modules_app/companies/stats.php new file mode 100644 index 0000000..19a3181 --- /dev/null +++ b/app/modules_app/companies/stats.php @@ -0,0 +1,67 @@ +prepare("SELECT id, name, tax_identification_number, is_active, + (jofotara_client_id_encrypted IS NOT NULL) as is_jofotara_connected, + jofotara_income_source_sequence + FROM companies WHERE id = ? AND tenant_id = ?"); + $stmt->execute([$companyId, $tenantId]); + $company = $stmt->fetch(); + + if (!$company) json_error('Company not found', 404); + + // 3. Monthly Invoice Stats + $stmtStats = $db->prepare(" + SELECT + DATE_FORMAT(invoice_date, '%Y-%m') as month, + COUNT(*) as total_invoices, + SUM(CASE WHEN status='approved' THEN 1 ELSE 0 END) as approved_count, + SUM(grand_total) as total_amount + FROM invoices + WHERE company_id = ? AND deleted_at IS NULL + GROUP BY month + ORDER BY month DESC + LIMIT 12 + "); + $stmtStats->execute([$companyId]); + $monthly = $stmtStats->fetchAll(); + + // 4. Lifetime Totals + $stmtTotals = $db->prepare(" + SELECT + COUNT(*) as total_invoices, + SUM(grand_total) as total_amount, + SUM(tax_amount) as total_tax, + SUM(CASE WHEN status='approved' THEN 1 ELSE 0 END) as approved_count + FROM invoices + WHERE company_id = ? AND deleted_at IS NULL + "); + $stmtTotals->execute([$companyId]); + $totals = $stmtTotals->fetch(); + + json_success([ + 'company' => $company, + 'monthly' => $monthly, + 'totals' => $totals + ]); + +} catch (\Exception $e) { + error_log("Company Stats Error: " . $e->getMessage()); + json_error('Server error', 500); +} diff --git a/app/modules_app/invoices/download_xml.php b/app/modules_app/invoices/download_xml.php new file mode 100644 index 0000000..06005c7 --- /dev/null +++ b/app/modules_app/invoices/download_xml.php @@ -0,0 +1,44 @@ +prepare(" + SELECT js.xml_payload, js.jofotara_uuid + FROM jofotara_submissions js + JOIN invoices i ON js.invoice_id = i.id + WHERE i.id = ? AND i.tenant_id = ? AND js.status = 'accepted' + ORDER BY js.created_at DESC LIMIT 1 + "); + $stmt->execute([$id, $tenantId]); + $row = $stmt->fetch(); + + if (!$row || empty($row['xml_payload'])) { + json_error('لا يوجد XML رسمي متاح لهذه الفاتورة', 404); + } + + // 4. Send headers for download + header('Content-Type: application/xml; charset=utf-8'); + header('Content-Disposition: attachment; filename="invoice_' . ($row['jofotara_uuid'] ?: $id) . '.xml"'); + echo $row['xml_payload']; + exit; + +} catch (\Exception $e) { + error_log("XML Download Error: " . $e->getMessage()); + json_error('خطأ في تحميل الملف', 500); +} diff --git a/app/modules_app/invoices/view.php b/app/modules_app/invoices/view.php index 28987a8..cb84070 100644 --- a/app/modules_app/invoices/view.php +++ b/app/modules_app/invoices/view.php @@ -1,6 +1,6 @@ prepare(" - SELECT i.*, c.name as company_name + SELECT i.*, c.name as company_name FROM invoices i - LEFT JOIN companies c ON i.company_id = c.id - WHERE i.id = ? + JOIN companies c ON i.company_id = c.id + WHERE i.id = ? AND i.tenant_id = ? "); - $stmt->execute([$id]); + $stmt->execute([$id, $tenantId]); $invoice = $stmt->fetch(); - if (!$invoice) { - json_error('Invoice not found', 404); - } - - // 3. Authorization Check - if ($decoded['role'] !== 'super_admin') { - if ($invoice['tenant_id'] !== $decoded['tenant_id']) { - json_error('Unauthorized access to this invoice', 403); - } - } + if (!$invoice) json_error('Invoice not found or access denied', 404); // 4. Fetch Line Items $stmtLines = $db->prepare("SELECT * FROM invoice_lines WHERE invoice_id = ? ORDER BY line_number ASC"); @@ -57,13 +49,30 @@ try { $invoice['company_name'] = $decrypt($invoice['company_name']); } - // 6. Generate Public URL for File (Assuming storage is symlinked or served) - // For now, let's just return the relative path or a proxy route - // We'll add a proxy route later if needed. - $invoice['file_url'] = '/index.php?route=v1/invoices/file&id=' . $invoice['id']; + // 6. Fetch JoFotara Submission Data + $stmtSub = $db->prepare(" + SELECT jofotara_uuid, submitted_at, qr_code_raw, status as submission_status, response_body + FROM jofotara_submissions + WHERE invoice_id = ? AND status = 'accepted' + ORDER BY created_at DESC LIMIT 1 + "); + $stmtSub->execute([$id]); + $submission = $stmtSub->fetch(); + + $invoice['jofotara'] = $submission ? [ + 'uuid' => $submission['jofotara_uuid'], + 'submitted_at' => $submission['submitted_at'], + 'qr_image_uri' => $submission['qr_code_raw'] ? 'data:image/png;base64,' . $submission['qr_code_raw'] : null, + 'has_xml' => true + ] : null; + + // 7. Generate Public URL for File + $token = Encryption::encrypt($invoice['original_file_path']); + $invoice['file_url'] = '/index.php?route=v1/invoices/file&file_token=' . urlencode($token); json_success($invoice); } catch (\Exception $e) { - json_error('Error fetching invoice: ' . $e->getMessage(), 500); + error_log("Invoice View Error: " . $e->getMessage()); + json_error('Server error during invoice retrieval', 500); } diff --git a/public/index.php b/public/index.php index 76bdf83..ab7aad6 100644 --- a/public/index.php +++ b/public/index.php @@ -30,6 +30,9 @@ $routes = [ 'v1/invoices/file' => ['GET', 'invoices/file.php'], 'v1/invoices/approve' => ['POST', 'invoices/approve.php'], 'v1/invoices/upload' => ['POST', 'invoices/upload.php'], + 'v1/invoices/download_xml' => ['GET', 'invoices/download_xml.php'], + 'v1/companies/stats' => ['GET', 'companies/stats.php'], + 'v1/companies/connect' => ['POST', 'companies/connect_jofotara.php'], 'v1/dashboard/stats' => ['GET', 'dashboard/stats.php'], 'v1/tenants' => ['GET', 'tenants/index.php'], 'v1/tenants/create' => ['POST', 'tenants/create.php'], diff --git a/public/shell.php b/public/shell.php index 4ed021e..6edf3fa 100644 --- a/public/shell.php +++ b/public/shell.php @@ -179,7 +179,7 @@