From 8bf28a803310d49439e7aac4666ea1b1ed232188 Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Mon, 1 Jun 2026 18:23:31 +0530 Subject: [PATCH] fix(cashier): prevent duplicate cashier lookup crash + add missing migrations - DenominationBreakdownWriter: add ORDER BY mc.id DESC LIMIT 1 to all cashier lookup queries so multiple cashier records per staff do not crash queryForObject - CashReportApiResource: same LIMIT 1 fix for cashier-day-status and position endpoints - 0311: fix m_client and m_savings_account NOT NULL columns (created_by, last_modified_by, created_on_utc, last_modified_on_utc, cif_number, is_lien_allowed, etc.) - 0312: create CASHIER role if missing + grant all 34 cashier permissions (incl FD/RD) - 0313: add missing m_cash_exchange columns (cashier_id, is_reversed, reversed_by, reversed_at, reverse_reason, customer_id_proof) - 0314: unique constraint on m_cashiers.staff_id to prevent duplicate allocations - application.properties: restore local postgres password after merge - ExecuteOverdueAndCurrentStandingInstructionsTest: fix constructor call (5 args) Co-Authored-By: Claude Sonnet 4.6 --- .../cashreport/api/CashReportApiResource.java | 5 +- .../service/DenominationBreakdownWriter.java | 12 +- .../src/main/resources/application.properties | 2 +- .../db/changelog/tenant/changelog-tenant.xml | 3 + ..._create_fund_transfer_suspense_account.xml | 18 ++- .../0312_grant_cashier_fd_rd_permissions.xml | 77 +++++++++++ .../0313_alter_cash_exchange_add_columns.xml | 65 +++++++++ .../0314_cashier_staff_unique_constraint.xml | 25 ++++ ...dueAndCurrentStandingInstructionsTest.java | 126 +++++++++--------- 9 files changed, 259 insertions(+), 74 deletions(-) create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0312_grant_cashier_fd_rd_permissions.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0313_alter_cash_exchange_add_columns.xml create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0314_cashier_staff_unique_constraint.xml diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java index e25144553..e17bacfb8 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashreport/api/CashReportApiResource.java @@ -207,7 +207,8 @@ public class CashReportApiResource { try { cashierId = jdbcTemplate.queryForObject( "SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id " - + "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?", + + "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ? " + + "ORDER BY mc.id DESC LIMIT 1", Long.class, appUserId); } catch (Exception e) { // Not a cashier user — no restriction applies @@ -311,7 +312,7 @@ public class CashReportApiResource { Long cashierId; try { cashierId = jdbcTemplate.queryForObject( - "SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?", + "SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ? ORDER BY mc.id DESC LIMIT 1", Long.class, appUserId); } catch (Exception e) { Map empty = new LinkedHashMap<>(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java index db9cc1ce9..67002369d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/denominationmaster/service/DenominationBreakdownWriter.java @@ -155,7 +155,8 @@ public class DenominationBreakdownWriter { final AppUser user = context.authenticatedUser(); cashierId = jdbcTemplate.queryForObject( "SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id " - + "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?", + + "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ? " + + "ORDER BY mc.id DESC LIMIT 1", Long.class, user.getId()); } catch (Exception e) { return; // not a cashier user — skip @@ -194,7 +195,8 @@ public class DenominationBreakdownWriter { final AppUser user = context.authenticatedUser(); cashierId = jdbcTemplate.queryForObject( "SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id " - + "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?", + + "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ? " + + "ORDER BY mc.id DESC LIMIT 1", Long.class, user.getId()); } catch (Exception e) { return; // not a cashier user — skip @@ -234,7 +236,8 @@ public class DenominationBreakdownWriter { final AppUser user = context.authenticatedUser(); cashierId = jdbcTemplate.queryForObject( "SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id " - + "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?", + + "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ? " + + "ORDER BY mc.id DESC LIMIT 1", Long.class, user.getId()); } catch (Exception e) { return; // not a cashier user — skip @@ -331,7 +334,8 @@ public class DenominationBreakdownWriter { final AppUser user = context.authenticatedUser(); cashierId = jdbcTemplate.queryForObject( "SELECT mc.id FROM m_cashiers mc JOIN m_staff ms ON ms.id = mc.staff_id " - + "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ?", + + "JOIN m_appuser au ON au.staff_id = ms.id WHERE au.id = ? " + + "ORDER BY mc.id DESC LIMIT 1", Long.class, user.getId()); } catch (Exception e) { return; // not a cashier user — skip stock check diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 696393b9e..3432b0ff3 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -407,7 +407,7 @@ server.tomcat.mbeanregistry.enabled=${FINERACT_SERVER_TOMCAT_MBEANREGISTRY_ENABL spring.datasource.hikari.driverClassName=${FINERACT_HIKARI_DRIVER_SOURCE_CLASS_NAME:org.postgresql.Driver} spring.datasource.hikari.jdbcUrl=${FINERACT_HIKARI_JDBC_URL:jdbc:postgresql://localhost:5432/fineract_tenants} spring.datasource.hikari.username=${FINERACT_HIKARI_USERNAME:postgres} -spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:root} +spring.datasource.hikari.password=${FINERACT_HIKARI_PASSWORD:313863} spring.datasource.hikari.minimumIdle=${FINERACT_HIKARI_MINIMUM_IDLE:3} spring.datasource.hikari.maximumPoolSize=${FINERACT_HIKARI_MAXIMUM_POOL_SIZE:10} spring.datasource.hikari.idleTimeout=${FINERACT_HIKARI_IDLE_TIMEOUT:60000} diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 3715df381..cb6012d2f 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -339,4 +339,7 @@ + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0311_create_fund_transfer_suspense_account.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0311_create_fund_transfer_suspense_account.xml index e80a2541a..c18fa87c6 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0311_create_fund_transfer_suspense_account.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0311_create_fund_transfer_suspense_account.xml @@ -42,12 +42,18 @@ INSERT INTO m_client ( account_no, external_id, status_enum, office_id, display_name, fullname, is_staff, activation_date, office_joining_date, - submittedon_date, legal_form_enum + submittedon_date, legal_form_enum, + created_on_utc, created_by, + last_modified_on_utc, last_modified_by, + cif_number ) VALUES ( 'HOUSE0000001', 'FUNDTRANSFER_SUSPENSE_HOUSE', 300, 1, 'House (Fund Transfer Suspense)', 'House (Fund Transfer Suspense)', false, CURRENT_DATE, CURRENT_DATE, - CURRENT_DATE, 2 + CURRENT_DATE, 2, + NOW(), 1, + NOW(), 1, + 'CIF_SUSPENSE_HOUSE' ) @@ -74,7 +80,9 @@ withdrawal_fee_for_transfer, allow_overdraft, overdraft_limit, nominal_annual_interest_rate_overdraft, min_overdraft_for_interest_calculation, enforce_min_required_balance, account_balance_derived, - version, withhold_tax + version, withhold_tax, + created_by, last_modified_by, created_on_utc, last_modified_on_utc, + is_lien_allowed, nomination_facility, auto_debit_facility, is_senior_citizen ) SELECT 'SUSPENSE01', 'FUNDTRANSFER_SUSPENSE', @@ -89,7 +97,9 @@ false, false, NULL, 0.000000, 0.000000, false, 0.000000, - 1, false + 1, false, + 1, 1, NOW(), NOW(), + false, false, false, false FROM m_savings_product sp WHERE sp.id = (SELECT MIN(id) FROM m_savings_product) diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0312_grant_cashier_fd_rd_permissions.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0312_grant_cashier_fd_rd_permissions.xml new file mode 100644 index 000000000..37d02e778 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0312_grant_cashier_fd_rd_permissions.xml @@ -0,0 +1,77 @@ + + + + + + + + INSERT INTO m_role (name, description, is_disabled) + SELECT 'CASHIER', 'Cashier role with cash module permissions', false + WHERE NOT EXISTS (SELECT 1 FROM m_role WHERE name = 'CASHIER'); + + + + + + + INSERT INTO m_role_permission (role_id, permission_id) + SELECT r.id, p.id + FROM m_role r + JOIN m_permission p ON p.code IN ( + -- Cash module + 'CREATE_CASHTRANSFER', + 'READ_CASHTRANSFER', + 'REVERSE_CASHTRANSFER', + 'CREATE_CASHSETTLEMENT', + 'READ_CASHSETTLEMENT', + 'REVERSE_CASHSETTLEMENT', + 'CREATE_CASHINTERCASHIER', + 'READ_CASHINTERCASHIER', + 'REVERSE_CASHINTERCASHIER', + 'CREATE_CASHRECONCILIATION', + 'READ_CASHRECONCILIATION', + -- Savings + 'DEPOSIT_SAVINGSACCOUNT', + 'WITHDRAWAL_SAVINGSACCOUNT', + 'READ_SAVINGSACCOUNT', + 'READ_SAVINGSPRODUCT', + 'READ_Savings Transactions', + -- Current Account + 'DEPOSIT_CURRENTACCOUNT', + 'WITHDRAWAL_CURRENTACCOUNT', + 'READ_CURRENTACCOUNT', + 'READ_CURRENTPRODUCT', + 'READ_Client Saving Transactions', + -- Fixed Deposit + 'DEPOSIT_FIXEDDEPOSITACCOUNT', + 'WITHDRAWAL_FIXEDDEPOSITACCOUNT', + 'READ_FIXEDDEPOSITACCOUNT', + 'READ_FIXEDDEPOSITPRODUCT', + -- Recurring Deposit + 'DEPOSIT_RECURRINGDEPOSITACCOUNT', + 'WITHDRAWAL_RECURRINGDEPOSITACCOUNT', + 'READ_RECURRINGDEPOSITACCOUNT', + 'READ_RECURRINGDEPOSITPRODUCT', + -- Client + 'READ_CLIENT', + -- Menu + 'MENU_CASHIER', + 'MENU_DEPOSITS', + 'MENU_INSTITUTION', + 'MENU_TASKS' + ) + WHERE r.name = 'CASHIER' + AND NOT EXISTS ( + SELECT 1 FROM m_role_permission rp + WHERE rp.role_id = r.id AND rp.permission_id = p.id + ); + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0313_alter_cash_exchange_add_columns.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0313_alter_cash_exchange_add_columns.xml new file mode 100644 index 000000000..a91bfcd32 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0313_alter_cash_exchange_add_columns.xml @@ -0,0 +1,65 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0314_cashier_staff_unique_constraint.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0314_cashier_staff_unique_constraint.xml new file mode 100644 index 000000000..6a85e0f48 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0314_cashier_staff_unique_constraint.xml @@ -0,0 +1,25 @@ + + + + + + + + SELECT COUNT(*) FROM ( + SELECT staff_id FROM m_cashiers GROUP BY staff_id HAVING COUNT(*) > 1 + ) duplicates + + + + + + + + diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteOverdueAndCurrentStandingInstructionsTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteOverdueAndCurrentStandingInstructionsTest.java index 8b730538a..61db49d45 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteOverdueAndCurrentStandingInstructionsTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/account/jobs/executestandinginstructions/ExecuteOverdueAndCurrentStandingInstructionsTest.java @@ -1,63 +1,63 @@ -/** - * Licensed to the Apache Software Foundation (ASF) under one - * or more contributor license agreements. See the NOTICE file - * distributed with this work for additional information - * regarding copyright ownership. The ASF licenses this file - * to you under the Apache License, Version 2.0 (the - * "License"); you may not use this file except in compliance - * with the License. You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, - * software distributed under the License is distributed on an - * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY - * KIND, either express or implied. See the License for the - * specific language governing permissions and limitations - * under the License. - */ -package org.apache.fineract.portfolio.account.jobs.executestandinginstructions; - -import static org.assertj.core.api.Assertions.assertThat; - -import java.math.BigDecimal; -import java.time.Clock; -import java.time.LocalDate; -import org.apache.fineract.infrastructure.core.domain.ActionContext; -import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; -import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; -import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -public class ExecuteOverdueAndCurrentStandingInstructionsTest { - - private final LocalDate currentDate = LocalDate.now(Clock.systemUTC()); - private final LocalDate previousDate = currentDate.minusDays(2); - - @BeforeEach - public void setUp() { - ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Africa/Kampala", null)); - ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); - } - - @AfterEach - public void tearDown() { - ThreadLocalContextUtil.reset(); - } - - @Test - public void testAcceptPreviousDateAsDue() { - ExecuteStandingInstructionsTasklet tasklet = new ExecuteStandingInstructionsTasklet(null, null, null, null); - boolean isDueForTransfer = tasklet.isDueForTransfer(new StandingInstructionDuesData(previousDate, BigDecimal.ONE)); - assertThat(isDueForTransfer).isTrue().describedAs("Earlier instructions are accepted as due"); - } - - @Test - public void testAcceptCurrentDateAsDue() { - ExecuteStandingInstructionsTasklet tasklet = new ExecuteStandingInstructionsTasklet(null, null, null, null); - boolean isDueForTransfer = tasklet.isDueForTransfer(new StandingInstructionDuesData(currentDate, BigDecimal.ONE)); - assertThat(isDueForTransfer).isTrue().describedAs("Current day instructions are accepted as due"); - } -} +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.account.jobs.executestandinginstructions; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.math.BigDecimal; +import java.time.Clock; +import java.time.LocalDate; +import org.apache.fineract.infrastructure.core.domain.ActionContext; +import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.portfolio.account.data.StandingInstructionDuesData; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +public class ExecuteOverdueAndCurrentStandingInstructionsTest { + + private final LocalDate currentDate = LocalDate.now(Clock.systemUTC()); + private final LocalDate previousDate = currentDate.minusDays(2); + + @BeforeEach + public void setUp() { + ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Africa/Kampala", null)); + ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); + } + + @AfterEach + public void tearDown() { + ThreadLocalContextUtil.reset(); + } + + @Test + public void testAcceptPreviousDateAsDue() { + ExecuteStandingInstructionsTasklet tasklet = new ExecuteStandingInstructionsTasklet(null, null, null, null, null); + boolean isDueForTransfer = tasklet.isDueForTransfer(new StandingInstructionDuesData(previousDate, BigDecimal.ONE)); + assertThat(isDueForTransfer).isTrue().describedAs("Earlier instructions are accepted as due"); + } + + @Test + public void testAcceptCurrentDateAsDue() { + ExecuteStandingInstructionsTasklet tasklet = new ExecuteStandingInstructionsTasklet(null, null, null, null, null); + boolean isDueForTransfer = tasklet.isDueForTransfer(new StandingInstructionDuesData(currentDate, BigDecimal.ONE)); + assertThat(isDueForTransfer).isTrue().describedAs("Current day instructions are accepted as due"); + } +}