diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..b2a6b3e Binary files /dev/null and b/.DS_Store differ diff --git a/app/Modules/Invoices/Actions/DownloadInvoiceFileAction.php b/app/Modules/Invoices/Actions/DownloadInvoiceFileAction.php new file mode 100644 index 0000000..35c7b45 --- /dev/null +++ b/app/Modules/Invoices/Actions/DownloadInvoiceFileAction.php @@ -0,0 +1,30 @@ +prepare("SELECT original_file_path, company_id FROM invoices WHERE id = ? AND tenant_id = ? AND deleted_at IS NULL LIMIT 1"); + $stmt->execute([$invoiceId, $tenantId]); + $invoice = $stmt->fetch(); + + if (!$invoice || !file_exists($invoice['original_file_path'])) { + throw new Exception('الملف غير موجود', 404); + } + + $role = $user->role ?? 'viewer'; + if ($role !== 'super_admin' && $invoice['company_id'] !== ($user->assigned_company_id ?? null)) { + throw new Exception('غير مصرح لك بمشاهدة هذا الملف', 403); + } + + return [ + 'path' => $invoice['original_file_path'], + 'mime' => mime_content_type($invoice['original_file_path']), + 'name' => basename($invoice['original_file_path']) + ]; + } +} diff --git a/app/Modules/Invoices/Actions/GetInvoiceDetailAction.php b/app/Modules/Invoices/Actions/GetInvoiceDetailAction.php new file mode 100644 index 0000000..599ed04 --- /dev/null +++ b/app/Modules/Invoices/Actions/GetInvoiceDetailAction.php @@ -0,0 +1,31 @@ +prepare("SELECT * FROM invoices WHERE id = ? AND tenant_id = ? AND deleted_at IS NULL LIMIT 1"); + $stmt->execute([$invoiceId, $tenantId]); + $invoice = $stmt->fetch(); + + if (!$invoice) { + throw new Exception('الفاتورة غير موجودة أو تم حذفها', 404); + } + + $role = $user->role ?? 'viewer'; + if ($role !== 'super_admin' && $invoice['company_id'] !== ($user->assigned_company_id ?? null)) { + throw new Exception('غير مصرح لك بالوصول لهذه الفاتورة', 403); + } + + $stmt = $db->prepare("SELECT * FROM invoice_lines WHERE invoice_id = ? ORDER BY line_number ASC"); + $stmt->execute([$invoiceId]); + $invoice['lines'] = $stmt->fetchAll() ?: []; + + return $invoice; + } +} diff --git a/app/Modules/Invoices/Actions/ListInvoicesAction.php b/app/Modules/Invoices/Actions/ListInvoicesAction.php new file mode 100644 index 0000000..bff9e17 --- /dev/null +++ b/app/Modules/Invoices/Actions/ListInvoicesAction.php @@ -0,0 +1,31 @@ +role ?? 'viewer'; + $assignedCompanyId = $user->assigned_company_id ?? null; + + if ($role === 'super_admin' || $role === 'admin') { + $stmt = $db->prepare("SELECT i.*, c.name as company_name + FROM invoices i + JOIN companies c ON i.company_id = c.id + WHERE i.tenant_id = ? AND i.deleted_at IS NULL + ORDER BY i.created_at DESC"); + $stmt->execute([$tenantId]); + } else { + $stmt = $db->prepare("SELECT i.*, c.name as company_name + FROM invoices i + JOIN companies c ON i.company_id = c.id + WHERE i.tenant_id = ? AND i.company_id = ? AND i.deleted_at IS NULL + ORDER BY i.created_at DESC"); + $stmt->execute([$tenantId, $assignedCompanyId]); + } + + return $stmt->fetchAll() ?: []; + } +} diff --git a/app/Modules/Invoices/Actions/SubmitInvoiceAction.php b/app/Modules/Invoices/Actions/SubmitInvoiceAction.php new file mode 100644 index 0000000..8ee42d7 --- /dev/null +++ b/app/Modules/Invoices/Actions/SubmitInvoiceAction.php @@ -0,0 +1,23 @@ +prepare("SELECT id FROM invoices WHERE id = ? AND tenant_id = ? AND deleted_at IS NULL LIMIT 1"); + $stmt->execute([$invoiceId, $tenantId]); + + if (!$stmt->fetch()) { + throw new Exception('الفاتورة غير موجودة', 404); + } + + QueueService::push('submit_jofotara', [ + 'invoice_id' => $invoiceId + ]); + } +} diff --git a/app/Modules/Invoices/Actions/UploadInvoiceAction.php b/app/Modules/Invoices/Actions/UploadInvoiceAction.php new file mode 100644 index 0000000..a9e3f9b --- /dev/null +++ b/app/Modules/Invoices/Actions/UploadInvoiceAction.php @@ -0,0 +1,49 @@ +storage->store($files['invoice'], $tenantId, $companyId); + $fileHash = $this->storage->getHash($filePath); + + $invoiceId = Uuid::uuid4()->toString(); + $this->invoiceModel->create([ + 'id' => $invoiceId, + 'tenant_id' => $tenantId, + 'company_id' => $companyId, + 'uploaded_by' => $user->user_id ?? null, + 'status' => 'uploaded', + 'original_file_path' => $filePath, + 'original_file_hash' => $fileHash, + 'idempotency_key' => bin2hex(random_bytes(16)) + ]); + + QueueService::push('invoice_extraction', [ + 'invoice_id' => $invoiceId, + 'file_path' => $filePath, + 'mime_type' => mime_content_type($filePath) + ]); + + return $invoiceId; + } +} diff --git a/app/Modules/Invoices/InvoiceController.php b/app/Modules/Invoices/InvoiceController.php index c708d00..ac91e34 100644 --- a/app/Modules/Invoices/InvoiceController.php +++ b/app/Modules/Invoices/InvoiceController.php @@ -6,164 +6,90 @@ namespace App\Modules\Invoices; use App\Core\{Request, Response}; use App\Services\FileStorageService; -use App\Services\AiExtractionService; use App\Modules\Invoices\InvoiceModel; +use App\Modules\Invoices\Actions\{ + ListInvoicesAction, + UploadInvoiceAction, + GetInvoiceDetailAction, + SubmitInvoiceAction, + DownloadInvoiceFileAction +}; use Throwable; final class InvoiceController { public function __construct( private readonly InvoiceModel $invoiceModel, - private readonly FileStorageService $storage, - private readonly AiExtractionService $aiExtraction + private readonly FileStorageService $storage ) {} public function list(Request $request): void { - $tenantId = $request->tenantId; - $role = $request->user->role ?? 'viewer'; - $assignedCompanyId = $request->user->assigned_company_id ?? null; - - $db = \App\Core\Database::getInstance(); - if ($role === 'super_admin' || $role === 'admin') { - $stmt = $db->prepare("SELECT i.*, c.name as company_name FROM invoices i JOIN companies c ON i.company_id = c.id WHERE i.tenant_id = ? AND i.deleted_at IS NULL ORDER BY i.created_at DESC"); - $stmt->execute([$tenantId]); - $invoices = $stmt->fetchAll(); - } else { - $stmt = $db->prepare("SELECT i.*, c.name as company_name FROM invoices i JOIN companies c ON i.company_id = c.id WHERE i.tenant_id = ? AND i.company_id = ? AND i.deleted_at IS NULL ORDER BY i.created_at DESC"); - $stmt->execute([$tenantId, $assignedCompanyId]); - $invoices = $stmt->fetchAll(); + try { + $action = new ListInvoicesAction(); + $invoices = $action->execute($request->tenantId, $request->user); + Response::json(['success' => true, 'data' => $invoices]); + } catch (Throwable $e) { + Response::error($e->getMessage(), 'LIST_ERROR', (int)($e->getCode() ?: 500)); } - - Response::json([ - 'success' => true, - 'data' => $invoices - ]); } public function upload(Request $request): void { - $files = $request->getFiles(); - if (empty($files['invoice'])) { - Response::error('يرجى اختيار ملف الفاتورة', 'MISSING_FILE', 422); - return; - } - - $companyId = $request->input('company_id'); - if (!$companyId) { - Response::error('يرجى تحديد الشركة', 'MISSING_COMPANY', 422); - return; - } - try { - $tenantId = $request->tenantId; - $filePath = $this->storage->store($files['invoice'], $tenantId, $companyId); - $fileHash = $this->storage->getHash($filePath); - - // Create invoice record - $invoiceId = \Ramsey\Uuid\Uuid::uuid4()->toString(); - $this->invoiceModel->create([ - 'id' => $invoiceId, - 'tenant_id' => $tenantId, - 'company_id' => $companyId, - 'uploaded_by' => $request->user->user_id, - 'status' => 'uploaded', // Match schema ENUM - 'original_file_path' => $filePath, - 'original_file_hash' => $fileHash, - 'idempotency_key' => bin2hex(random_bytes(16)) - ]); - - // Push to Queue for AI Extraction - \App\Services\QueueService::push('invoice_extraction', [ - 'invoice_id' => $invoiceId, - 'file_path' => $filePath, - 'mime_type' => mime_content_type($filePath) - ]); + $action = new UploadInvoiceAction($this->storage, $this->invoiceModel); + $invoiceId = $action->execute( + $request->getFiles(), + (string)$request->input('company_id'), + $request->tenantId, + $request->user + ); Response::json([ 'success' => true, 'data' => ['invoice_id' => $invoiceId], 'message' => 'تم رفع الفاتورة بنجاح وجاري استخراج البيانات بالذكاء الاصطناعي' ], 202); - } catch (Throwable $e) { - Response::error($e->getMessage(), 'UPLOAD_FAILED', 500); + Response::error($e->getMessage(), 'UPLOAD_ERROR', (int)($e->getCode() ?: 500)); } } public function detail(Request $request, string $id): void { - $tenantId = $request->tenantId; - $invoiceId = $id; - - $db = \App\Core\Database::getInstance(); - $stmt = $db->prepare("SELECT * FROM invoices WHERE id = ? AND tenant_id = ? AND deleted_at IS NULL LIMIT 1"); - $stmt->execute([$invoiceId, $tenantId]); - $invoice = $stmt->fetch(); - - if (!$invoice) { - Response::error('الفاتورة غير موجودة', 'NOT_FOUND', 404); - return; + try { + $action = new GetInvoiceDetailAction(); + $invoice = $action->execute($id, $request->tenantId, $request->user); + Response::json(['success' => true, 'data' => $invoice]); + } catch (Throwable $e) { + Response::error($e->getMessage(), 'DETAIL_ERROR', (int)($e->getCode() ?: 500)); } - - // Additional authorization check based on assigned company if needed - $role = $request->user->role ?? 'viewer'; - if ($role !== 'super_admin' && $invoice['company_id'] !== $request->user->assigned_company_id) { - Response::error('غير مصرح لك بمشاهدة هذه الفاتورة', 'FORBIDDEN', 403); - return; - } - - // Fetch lines - $stmt = $db->prepare("SELECT * FROM invoice_lines WHERE invoice_id = ? ORDER BY id ASC"); - $stmt->execute([$invoiceId]); - $invoice['lines'] = $stmt->fetchAll(); - - Response::json([ - 'success' => true, - 'data' => $invoice - ]); } public function submit(Request $request, string $id): void { - $tenantId = $request->tenantId; - $invoiceId = $id; + try { + $action = new SubmitInvoiceAction(); + $action->execute($id, $request->tenantId); + Response::json(['success' => true, 'message' => 'Invoice submission queued.']); + } catch (Throwable $e) { + Response::error($e->getMessage(), 'SUBMIT_ERROR', (int)($e->getCode() ?: 500)); + } + } - // Push to Queue for JoFotara Submission - \App\Services\QueueService::push('submit_jofotara', [ - 'invoice_id' => $invoiceId - ]); - - Response::json([ - 'success' => true, - 'message' => 'Invoice submission queued.' - ]); public function downloadFile(Request $request, string $id): void { - $tenantId = $request->tenantId; - $db = \App\Core\Database::getInstance(); - $stmt = $db->prepare("SELECT original_file_path, company_id FROM invoices WHERE id = ? AND tenant_id = ? AND deleted_at IS NULL LIMIT 1"); - $stmt->execute([$id, $tenantId]); - $invoice = $stmt->fetch(); + try { + $action = new DownloadInvoiceFileAction(); + $file = $action->execute($id, $request->tenantId, $request->user); - if (!$invoice || !file_exists($invoice['original_file_path'])) { - Response::error('الملف غير موجود', 'NOT_FOUND', 404); - return; + header("Content-Type: {$file['mime']}"); + header("Content-Disposition: inline; filename=\"{$file['name']}\""); + header("Content-Length: " . filesize($file['path'])); + readfile($file['path']); + exit; + } catch (Throwable $e) { + Response::error($e->getMessage(), 'DOWNLOAD_ERROR', (int)($e->getCode() ?: 500)); } - - $role = $request->user->role ?? 'viewer'; - if ($role !== 'super_admin' && $invoice['company_id'] !== $request->user->assigned_company_id) { - Response::error('غير مصرح لك بمشاهدة هذا الملف', 'FORBIDDEN', 403); - return; - } - - $path = $invoice['original_file_path']; - $mime = mime_content_type($path); - - header("Content-Type: $mime"); - header("Content-Disposition: inline; filename=\"" . basename($path) . "\""); - header("Content-Length: " . filesize($path)); - readfile($path); - exit; } } diff --git a/public/shell.php b/public/shell.php index 9e0755a..9dcd710 100644 --- a/public/shell.php +++ b/public/shell.php @@ -80,6 +80,14 @@
+ + +