diff --git a/services/nginx/app/routes/orderInvoicesRoute.php b/services/nginx/app/routes/orderInvoicesRoute.php index 01da8864..ce227024 100644 --- a/services/nginx/app/routes/orderInvoicesRoute.php +++ b/services/nginx/app/routes/orderInvoicesRoute.php @@ -1816,6 +1816,7 @@ class orderInvoicesRoute $warnings = []; $invoice = (new collected_order_invoices_o())->select($collected_invoice_id); $invoice->requireSelected(); + $this->requireCollectedInvoiceContextAccess($invoice); $draft_id = null; $booked_id = null; @@ -1935,6 +1936,56 @@ class orderInvoicesRoute ]; } + private function requireCollectedInvoiceContextAccess(collected_order_invoices_o $invoice): void + { + global $response; + + if ($this->hasPermission('superuser')) { + return; + } + + $invoice_customer_number = (int)$invoice->customer_number->value(); + if ($invoice_customer_number > 0 && $this->isOwnCustomerContext($invoice_customer_number)) { + return; + } + + if ($this->hasAccessToAllCollectedInvoiceDepartments((int)$invoice->id)) { + return; + } + + $response->error('Permission denied for requested collected invoice.', 403); + } + + private function hasAccessToAllCollectedInvoiceDepartments(int $collected_invoice_id): bool + { + $orders = new orders_o(); + $order_departments = $orders->getFieldsWhere( + [ + 'invoice_collection_id' => $collected_invoice_id, + 'deleted_at' => null, + ], + ['department_id'] + ); + + $department_ids = array_values(array_unique(array_filter(array_map(static function (array $order): int { + return (int)($order['department_id'] ?? 0); + }, $order_departments), static function (int $department_id): bool { + return $department_id > 0; + }))); + + if (empty($department_ids)) { + return false; + } + + foreach ($department_ids as $department_id) { + if (!$this->hasDepartmentAccess((string)$department_id)) { + return false; + } + } + + return true; + } + private function extractEconomicCustomerNumber(mixed $invoice_raw): ?int { if ($invoice_raw === null) { diff --git a/services/nginx/app/tests/Unit/Invoicing/EconomicV2RouteAndVersioningHooksTest.php b/services/nginx/app/tests/Unit/Invoicing/EconomicV2RouteAndVersioningHooksTest.php index edd28917..20bad8ef 100644 --- a/services/nginx/app/tests/Unit/Invoicing/EconomicV2RouteAndVersioningHooksTest.php +++ b/services/nginx/app/tests/Unit/Invoicing/EconomicV2RouteAndVersioningHooksTest.php @@ -77,3 +77,15 @@ it('keeps legacy compare endpoint path for backward compatibility', function (): expect($content)->toContain('/collected-invoices/economic/compare'); expect($content)->toContain("requirePermission('compare_collected_invoice_economic')"); }); + +it('enforces customer or department context before building v2 invoice details payload', function (): void { + $routeFile = app_path('routes/orderInvoicesRoute.php'); + $content = file_get_contents($routeFile); + + expect($content)->not->toBeFalse(); + expect($content)->toContain('private function requireCollectedInvoiceContextAccess'); + expect($content)->toContain('$this->requireCollectedInvoiceContextAccess($invoice);'); + expect($content)->toContain("$this->hasPermission('superuser')"); + expect($content)->toContain('$this->isOwnCustomerContext($invoice_customer_number)'); + expect($content)->toContain('$this->hasAccessToAllCollectedInvoiceDepartments((int)$invoice->id)'); +});