feat(branchday): reject day close if cashiers given cash are not reconciled or settled
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-5) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, build-core) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (main, cucumber) (push) Canceled after 0s
Fineract Build & Cucumber tests (without E2E tests) / verify (progressive-loan, build-progressive-loan) (push) Canceled after 0s
Fineract Docker Builds / build (false, docker-compose-postgresql.yml, postgresql) (push) Canceled after 0s
Fineract Docker Builds / build (true, docker-compose.yml, mariadb) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-1) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 10 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 2 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 3 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 4 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 5 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 6 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-5) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MySQL / test (test-core-2) (push) Canceled after 0s
Fineract Documentation build / build (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 1 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 7 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 8 of 10) (push) Canceled after 0s
Fineract E2E Tests / E2E Tests (Shard 9 of 10) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - MariaDB / test (test-core-1) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-2) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-3) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-4) (push) Canceled after 0s
Fineract Cargo & Unit- & Integration tests - PostgreSQL / test (test-core-5) (push) Canceled after 0s
Fineract Liquibase Only mode - PostgreSQL / test (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with ActiveMQ (push) Canceled after 0s
Fineract Messaging Smoke Tests / Smoke Test with Kafka (push) Canceled after 0s

This commit is contained in:
2026-09-02 12:07:24 +05:30
parent 942bad6c8c
commit c052beeacf
@@ -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<Map<String, Object>> 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<String> blockingCashiers = new ArrayList<>();
for (final Map<String, Object> 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<String> 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());