From c052beeacf7c945e2da651ba37e25f0e1a9a5935 Mon Sep 17 00:00:00 2001 From: Nitesh Yadav Date: Wed, 2 Sep 2026 12:07:24 +0530 Subject: [PATCH] feat(branchday): reject day close if cashiers given cash are not reconciled or settled --- .../api/BranchDayRegisterApiResource.java | 56 +++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java index 249622985..b1bc8ebfd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/branchday/api/BranchDayRegisterApiResource.java @@ -494,6 +494,62 @@ public class BranchDayRegisterApiResource { "Business day " + businessDate + " is already closed."); } + // ── Ensure every cashier holding cash has been reconciled AND settled ── + // Find all cashiers under tellers belonging to this office, check if they have a cash position, + // and if so, verify reconciliation and settlement records exist for the business date. + final List> unsettledCashiers = jdbcTemplate.queryForList(""" + SELECT c.id AS cashier_id, c.staff_id, + COALESCE(s.display_name, CONCAT('Cashier #', c.id)) AS cashier_name, + (SELECT COUNT(*) FROM m_cash_reconciliation cr + WHERE cr.cashier_id = c.id AND cr.recon_date = ?) AS recon_count, + (SELECT COUNT(*) FROM m_cash_settlement cs + WHERE cs.from_cashier_id = c.id AND cs.settlement_date = ? + AND (cs.is_reversed IS NULL OR cs.is_reversed = false)) AS settle_count + FROM m_cashiers c + JOIN m_tellers t ON t.id = c.teller_id + LEFT JOIN m_staff s ON s.id = c.staff_id + WHERE t.office_id = ? + AND (c.start_date IS NULL OR c.start_date <= ?) + AND (c.end_date IS NULL OR c.end_date >= ?) + AND ( + EXISTS ( + SELECT 1 FROM m_cash_transfer ct + WHERE ct.to_cashier_id = c.id + AND ct.transfer_date = ? + AND (ct.is_reversed IS NULL OR ct.is_reversed = false) + AND ct.transfer_amount > 0 + ) + OR EXISTS ( + SELECT 1 FROM m_cash_inter_cashier cic + WHERE cic.to_cashier_id = c.id + AND cic.transfer_date = ? + AND (cic.is_reversed IS NULL OR cic.is_reversed = false) + AND cic.transfer_amount > 0 + ) + ) + """, Date.valueOf(businessDate), Date.valueOf(businessDate), + officeId, Date.valueOf(businessDate), Date.valueOf(businessDate), + Date.valueOf(businessDate), Date.valueOf(businessDate)); + + final List blockingCashiers = new ArrayList<>(); + for (final Map row : unsettledCashiers) { + final int reconCount = ((Number) row.get("recon_count")).intValue(); + final int settleCount = ((Number) row.get("settle_count")).intValue(); + if (reconCount == 0 || settleCount == 0) { + final String name = String.valueOf(row.get("cashier_name")); + final List issues = new ArrayList<>(); + if (reconCount == 0) issues.add("Not Reconciled"); + if (settleCount == 0) issues.add("Not Settled"); + blockingCashiers.add(name + " (" + String.join(", ", issues) + ")"); + } + } + if (!blockingCashiers.isEmpty()) { + throw new PlatformDataIntegrityException("error.msg.branchday.cashiers.not.settled", + "Cannot close the day — the following cashiers have not been reconciled or settled: " + + String.join("; ", blockingCashiers) + + ". Each cashier must reconcile their cash and settle it back to the vault before the branch day can be closed."); + } + // Closing a past date is allowed — otherwise a day left open could never be settled at all — // but it must say why it was left open, and it is stamped as a force close. final LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant());