From 8cfd1a4b1d233e14f521502eb91866aed671b3f2 Mon Sep 17 00:00:00 2001 From: objecttothis <17935339+objecttothis@users.noreply.github.com> Date: Mon, 31 Aug 2026 13:08:33 +0400 Subject: [PATCH] fix(sales): enforce reports_sales grant on search endpoint (#4673) - Sales::getSearch now enforces reports_sales grant before running search, returns 403 with lang message when missing - Rename snake_case helpers/methods to camelCase across Sales controller, Sale model, and tabular_helper (get_sale_data_row -> getSaleDataRow, get_payments_summary -> getPaymentsSummary, sales_headers -> salesHeaders, etc.) - Config/OSPOS: reset DB data cache before checking app_config table existence to avoid stale schema cache in tests - TestDatabaseBootstrapSeeder: expose static reset() so tests can rebuild schema once per class instead of only via seeder run() - SalesControllerTest: bootstrap DB once per class, seed once, refresh app settings each setUp, add tests for search endpoint authorization (cashier denied, supervisor allowed), move createTestItem into shared ItemFixtureTrait Signed-off-by: objecttothis <17935339+objecttothis@users.noreply.github.com> --- app/Config/OSPOS.php | 1 + app/Controllers/Sales.php | 32 +++--- .../Seeds/TestDatabaseBootstrapSeeder.php | 7 +- app/Helpers/tabular_helper.php | 10 +- app/Models/Sale.php | 2 +- tests/Controllers/SalesControllerTest.php | 103 +++++++++++++----- 6 files changed, 105 insertions(+), 50 deletions(-) diff --git a/app/Config/OSPOS.php b/app/Config/OSPOS.php index 1b89532b2..a3b4ecb1b 100644 --- a/app/Config/OSPOS.php +++ b/app/Config/OSPOS.php @@ -39,6 +39,7 @@ class OSPOS extends BaseConfig try { $db = Database::connect(); + $db->resetDataCache(); if (!$db->tableExists('app_config')) { $this->settings = $this->getDefaultSettings(); diff --git a/app/Controllers/Sales.php b/app/Controllers/Sales.php index aa9730cad..28627379c 100644 --- a/app/Controllers/Sales.php +++ b/app/Controllers/Sales.php @@ -137,20 +137,26 @@ class Sales extends Secure_Controller } $saleInfo = $this->sale->get_info($rowId)->getRow(); - $dataRow = get_sale_data_row($saleInfo); + $dataRow = getSaleDataRow($saleInfo); return $this->response->setJSON($dataRow); } /** - * @return void + * @return ResponseInterface */ public function getSearch(): ResponseInterface { + $personId = $this->session->get('person_id'); + + if (!$this->employee->has_grant('reports_sales', $personId)) { + return $this->response->setStatusCode(403)->setJSON(['success' => false, 'message' => lang('Sales.not_authorized')]); + } + $search = $this->request->getGet('search', FILTER_SANITIZE_FULL_SPECIAL_CHARS); $limit = $this->request->getGet('limit', FILTER_SANITIZE_NUMBER_INT); $offset = $this->request->getGet('offset', FILTER_SANITIZE_NUMBER_INT); - $sort = $this->sanitizeSortColumn(sales_headers(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'sale_id'); + $sort = $this->sanitizeSortColumn(salesHeaders(), $this->request->getGet('sort', FILTER_SANITIZE_FULL_SPECIAL_CHARS), 'sale_id'); $order = $this->request->getGet('order', FILTER_SANITIZE_FULL_SPECIAL_CHARS); $filters = [ @@ -171,24 +177,24 @@ class Sales extends Secure_Controller ]; // Check if any filter is set in the multiselect dropdown - $request_filters = array_fill_keys($this->request->getGet('filters', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? [], true); - $filters = array_merge($filters, $request_filters); + $requestFilters = array_fill_keys($this->request->getGet('filters', FILTER_SANITIZE_FULL_SPECIAL_CHARS) ?? [], true); + $filters = array_merge($filters, $requestFilters); $sales = $this->sale->search($search, $filters, $limit, $offset, $sort, $order); - $total_rows = $this->sale->get_found_rows($search, $filters); - $payments = $this->sale->get_payments_summary($search, $filters); - $payment_summary = get_sales_manage_payments_summary($payments); + $totalRows = $this->sale->get_found_rows($search, $filters); + $payments = $this->sale->getPaymentsSummary($search, $filters); + $paymentSummary = getSalesManagePaymentsSummary($payments); - $data_rows = []; + $dataRows = []; foreach ($sales->getResult() as $sale) { - $data_rows[] = get_sale_data_row($sale); + $dataRows[] = getSaleDataRow($sale); } - if ($total_rows > 0) { - $data_rows[] = get_sale_data_last_row($sales); + if ($totalRows > 0) { + $dataRows[] = getSaleDataLastRow($sales); } - return $this->response->setJSON(['total' => $total_rows, 'rows' => $data_rows, 'payment_summary' => $payment_summary]); + return $this->response->setJSON(['total' => $totalRows, 'rows' => $dataRows, 'payment_summary' => $paymentSummary]); } /** diff --git a/app/Database/Seeds/TestDatabaseBootstrapSeeder.php b/app/Database/Seeds/TestDatabaseBootstrapSeeder.php index 335ba1021..a1e156541 100644 --- a/app/Database/Seeds/TestDatabaseBootstrapSeeder.php +++ b/app/Database/Seeds/TestDatabaseBootstrapSeeder.php @@ -7,7 +7,7 @@ use Config\Database; class TestDatabaseBootstrapSeeder extends Seeder { - public function run(): void + public static function reset(): void { if (ENVIRONMENT !== 'testing') { throw new \RuntimeException('TestDatabaseBootstrapSeeder can only run in the testing environment.'); @@ -34,4 +34,9 @@ class TestDatabaseBootstrapSeeder extends Seeder $serverConn->query("DROP DATABASE IF EXISTS `{$dbName}`"); $serverConn->query("CREATE DATABASE IF NOT EXISTS `{$dbName}`"); } + + public function run(): void + { + self::reset(); + } } diff --git a/app/Helpers/tabular_helper.php b/app/Helpers/tabular_helper.php index 3ed5144ca..129e6c921 100644 --- a/app/Helpers/tabular_helper.php +++ b/app/Helpers/tabular_helper.php @@ -61,7 +61,7 @@ function transform_headers(array $headers, bool $readonly = false, bool $editabl } -function sales_headers(): array +function salesHeaders(): array { return [ ['sale_id' => lang('Common.id')], @@ -79,7 +79,7 @@ function sales_headers(): array */ function get_sales_manage_table_headers(): string { - $headers = sales_headers(); + $headers = salesHeaders(); $config = config(OSPOS::class)->settings; if ($config['invoice_enable']) { @@ -95,7 +95,7 @@ function get_sales_manage_table_headers(): string /** * Get the html data row for the sales */ -function get_sale_data_row(object $sale): array +function getSaleDataRow(object $sale): array { $uri = current_url(true); $controller = $uri->getSegment(1); @@ -145,7 +145,7 @@ function get_sale_data_row(object $sale): array /** * Get the html data last row for the sales */ -function get_sale_data_last_row(ResultInterface $sales): array +function getSaleDataLastRow(ResultInterface $sales): array { $sum_amount_due = 0; $sum_amount_tendered = 0; @@ -169,7 +169,7 @@ function get_sale_data_last_row(ResultInterface $sales): array /** * Get the sales payments summary */ -function get_sales_manage_payments_summary(array $payments): string +function getSalesManagePaymentsSummary(array $payments): string { $table = '