Compare commits

...
2 Commits
Author SHA1 Message Date
nitesh c052beeacf 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
2026-09-02 12:07:24 +05:30
niteshandClaude Opus 5 942bad6c8c fix(branchday): count account closures in the day-close cash reconciliation
Day Close reported SHORT by exactly the amount a closure had paid out. The
"Cash paid out (approved withdrawals)" line only summed
m_clerk_withdrawal_request, so a closure -- which pays cash over the same
counter but lives in its own register -- was missing from the total. Expected
cash stayed high by what had actually been handed over, and a closure that
balanced perfectly read as a shortage.

Verified against live data: opening 145,304 + deposits 0 - slip withdrawals 0
- closure 12,999 = 132,305, which is exactly the counted branch total. Variance
goes from -12,999 to 0.

Sums cashier_disbursed_amount rather than closing_amount: the member's ledger
gives up the paisa-exact figure, but what physically leaves the drawer is that
figure rounded to whole rupees, and this total has to agree with a physical
count. Restricted to CLOSED rows dated on disbursed_at, since a closure takes
nothing out of the drawer until the cashier hands it over.

Note: dividend cash claims (m_share_dividend_claim_request) are also absent
from this total and will misreport the same way. Left alone deliberately --
that register splits a claim across loan/savings/cash with its own cash_status
column that nothing in the Java sets, so the right predicate cannot be
established from the code as it stands.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-01 21:58:12 +05:30
@@ -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());
@@ -853,12 +909,29 @@ public class BranchDayRegisterApiResource {
// disbursements use APPROVED; withdrawal is the one register that doesn't) -- filtering on
// APPROVED alone silently excluded every completed withdrawal from this total, overstating
// the branch's expected cash by exactly what had actually been paid out that day.
final BigDecimal withdrawals = zero(jdbcTemplate.queryForObject("""
final BigDecimal slipWithdrawals = zero(jdbcTemplate.queryForObject("""
SELECT COALESCE(SUM(amount), 0) FROM m_clerk_withdrawal_request
WHERE status = 'WITHDRAWN'
AND COALESCE(txn_date, CAST(submitted_at AS DATE)) = ?
""", BigDecimal.class, date));
// Account closures pay cash over the same counter but live in their own register, so they
// were missing from this total entirely -- the branch's expected cash stayed high by exactly
// what had been handed over, reporting a shortage for a closure that balanced perfectly.
//
// cashier_disbursed_amount, not closing_amount: the member's ledger gives up the paisa-exact
// figure, but what physically leaves the drawer is that figure rounded to whole rupees, and
// this total has to agree with a physical count. Only CLOSED rows, dated on disbursed_at,
// since a closure takes nothing out of the drawer until the cashier hands it over (a
// nil-balance closure never reaches a cashier at all, hence the closure_date fallback).
final BigDecimal closureWithdrawals = zero(jdbcTemplate.queryForObject("""
SELECT COALESCE(SUM(cashier_disbursed_amount), 0) FROM m_clerk_closure_request
WHERE status = 'CLOSED'
AND COALESCE(CAST(disbursed_at AS DATE), closure_date) = ?
""", BigDecimal.class, date));
final BigDecimal withdrawals = slipWithdrawals.add(closureWithdrawals);
// Cash drawn from the vault to a cashier — more physical cash sitting in the branch.
final BigDecimal vaultIn = zero(jdbcTemplate.queryForObject("""
SELECT COALESCE(SUM(transfer_amount), 0) FROM m_cash_transfer