diff --git a/app/Controllers/Cashups.php b/app/Controllers/Cashups.php index 0e9b06f23..5ecc9c441 100644 --- a/app/Controllers/Cashups.php +++ b/app/Controllers/Cashups.php @@ -211,20 +211,29 @@ class Cashups extends Secure_Controller $close_date = $this->request->getPost('close_date'); $close_date_formatter = date_create_from_format($this->config['dateformat'] . ' ' . $this->config['timeformat'], $close_date); + $open_amount_cash = parse_decimals($this->request->getPost('open_amount_cash')); + $transfer_amount_cash = parse_decimals($this->request->getPost('transfer_amount_cash')); + $closed_amount_cash = parse_decimals($this->request->getPost('closed_amount_cash')); + $closed_amount_due = parse_decimals($this->request->getPost('closed_amount_due')); + $closed_amount_card = parse_decimals($this->request->getPost('closed_amount_card')); + $closed_amount_check = parse_decimals($this->request->getPost('closed_amount_check')); + + $logged_in_employee = $this->employee->get_logged_in_employee_info(); + $cash_up_data = [ 'open_date' => $open_date_formatter->format('Y-m-d H:i:s'), 'close_date' => $close_date_formatter->format('Y-m-d H:i:s'), - 'open_amount_cash' => parse_decimals($this->request->getPost('open_amount_cash')), - 'transfer_amount_cash' => parse_decimals($this->request->getPost('transfer_amount_cash')), - 'closed_amount_cash' => parse_decimals($this->request->getPost('closed_amount_cash')), - 'closed_amount_due' => parse_decimals($this->request->getPost('closed_amount_due')), - 'closed_amount_card' => parse_decimals($this->request->getPost('closed_amount_card')), - 'closed_amount_check' => parse_decimals($this->request->getPost('closed_amount_check')), - 'closed_amount_total' => parse_decimals($this->request->getPost('closed_amount_total')), + 'open_amount_cash' => $open_amount_cash, + 'transfer_amount_cash' => $transfer_amount_cash, + 'closed_amount_cash' => $closed_amount_cash, + 'closed_amount_due' => $closed_amount_due, + 'closed_amount_card' => $closed_amount_card, + 'closed_amount_check' => $closed_amount_check, + 'closed_amount_total' => $this->_calculate_total($open_amount_cash, $transfer_amount_cash, $closed_amount_due, $closed_amount_cash, $closed_amount_card, $closed_amount_check), 'note' => $this->request->getPost('note') != null, 'description' => $this->request->getPost('description', FILTER_SANITIZE_FULL_SPECIAL_CHARS), - 'open_employee_id' => $this->request->getPost('open_employee_id', FILTER_SANITIZE_NUMBER_INT), - 'close_employee_id' => $this->request->getPost('close_employee_id', FILTER_SANITIZE_NUMBER_INT), + 'open_employee_id' => $logged_in_employee->person_id, + 'close_employee_id' => $logged_in_employee->person_id, 'deleted' => $this->request->getPost('deleted') != null ]; diff --git a/tests/Controllers/CashupControllerTest.php b/tests/Controllers/CashupControllerTest.php new file mode 100644 index 000000000..6373daabb --- /dev/null +++ b/tests/Controllers/CashupControllerTest.php @@ -0,0 +1,140 @@ +DBGroup)->call('App\Database\Seeds\TestDatabaseBootstrapSeeder'); + Config::connect($this->DBGroup)->close(); + + self::$doneBootstrap = true; + } + + parent::setUp(); + + $ospos = new OSPOS(); + $ospos->settings = [ + 'company' => 'Test Co', + 'dateformat' => 'Y-m-d', + 'timeformat' => 'H:i:s', + 'number_locale' => 'en_US', + 'currency_decimals' => 2, + 'thousands_separator' => ',', + ]; + Factories::injectMock('config', OSPOS::class, $ospos); + } + + protected function tearDown(): void + { + Factories::reset(); + parent::tearDown(); + } + + protected function createCashupEmployee(): int + { + return $this->createEmployee( + first_name: 'Cashier', + last_name: 'Cashup', + email: 'cashup.' . uniqid() . '@test.com', + username: 'cashup_' . uniqid(), + grants: [ + ['permission_id' => 'cashups', 'menu_group' => 'home'], + ], + ); + } + + protected function loginAs(int $personId): void + { + $this->withSession([ + 'person_id' => $personId, + 'menu_group' => 'home', + ]); + } + + public function testTamperedTotalIsRecomputedServerSide(): void + { + $cashierId = $this->createCashupEmployee(); + $this->loginAs($cashierId); + + // Component amounts only sum to (50 - 0 - 0 + 10 + 0 + 0) = 60, but the + // client claims a total of 1000.00. The server must store 60, not 1000. + $response = $this->post('/cashups/save/-1', [ + 'open_date' => '2026-09-05 08:00:00', + 'close_date' => '2026-09-05 16:00:00', + 'open_amount_cash' => '0', + 'transfer_amount_cash' => '0', + 'closed_amount_cash' => '50', + 'closed_amount_due' => '10', + 'closed_amount_card' => '0', + 'closed_amount_check' => '0', + 'closed_amount_total' => '1000.00', + 'open_employee_id' => '999999', + 'close_employee_id' => '999999', + 'description' => 'Test cashup - tampered total', + ]); + + $response->assertStatus(200); + $result = json_decode($response->getJSON(), true); + $this->assertTrue($result['success']); + + $cashup = model(Cashup::class)->get_info($result['id']); + $this->assertEquals(60.0, (float) $cashup->closed_amount_total); + } + + public function testConsistentTotalIsStoredAndOwnerIsForcedToAuthenticatedUser(): void + { + $cashierId = $this->createCashupEmployee(); + $this->loginAs($cashierId); + + // 100 - 20 - 5 + 15 + 40 + 25 = 155. The client also tries to attribute + // the cashup to other employees (777/888); the server must force the + // authenticated user's person_id. + $response = $this->post('/cashups/save/-1', [ + 'open_date' => '2026-09-05 08:00:00', + 'close_date' => '2026-09-05 16:00:00', + 'open_amount_cash' => '20', + 'transfer_amount_cash' => '5', + 'closed_amount_cash' => '100', + 'closed_amount_due' => '15', + 'closed_amount_card' => '40', + 'closed_amount_check' => '25', + 'closed_amount_total' => '155', + 'open_employee_id' => '777', + 'close_employee_id' => '888', + 'description' => 'Test cashup - consistent total', + ]); + + $response->assertStatus(200); + $result = json_decode($response->getJSON(), true); + $this->assertTrue($result['success']); + + $cashup = model(Cashup::class)->get_info($result['id']); + $this->assertEquals(155.0, (float) $cashup->closed_amount_total); + $this->assertEquals($cashierId, (int) $cashup->open_employee_id); + $this->assertEquals($cashierId, (int) $cashup->close_employee_id); + } +}