diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkdisbursement/api/ClerkDisbursementRequestApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkdisbursement/api/ClerkDisbursementRequestApiResource.java index 08d596d26..e75955127 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkdisbursement/api/ClerkDisbursementRequestApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkdisbursement/api/ClerkDisbursementRequestApiResource.java @@ -177,6 +177,15 @@ public class ClerkDisbursementRequestApiResource { kycRestrictionGuard.check(entry.get("savingsAccountId").getAsLong(), "Dr".equals(dbCr) ? KycRestrictionGuard.Operation.TRANSFER : KycRestrictionGuard.Operation.CREDIT); } + // A Dr leg against a savings account is a withdrawal, and the clerk who raises it should + // not learn the account can't pay out until a manager tries to approve it and the whole + // request bounces back generic and reversed. Every reason SavingsAccount.withdraw() would + // refuse this later -- inactive, closed, or still inside its lock-in period -- is checked + // here too, against the same columns, so the clerk sees the real reason at entry time. + if ("Dr".equals(dbCr) && "SAVINGS".equals(entryAccountType) + && entry.has("savingsAccountId") && !entry.get("savingsAccountId").isJsonNull()) { + validateSavingsWithdrawalEligible(entry.get("savingsAccountId").getAsLong(), entryAmt); + } if ("Dr".equals(dbCr)) { totalDr = totalDr.add(entryAmt); } else { @@ -1179,6 +1188,51 @@ public class ClerkDisbursementRequestApiResource { .findFirst().orElse(null); } + /** Savings status_enum for an account still open for business (Fineract's SavingsAccountStatusType.ACTIVE). */ + private static final int SAVINGS_STATUS_ACTIVE = 300; + + /** + * The same refusals {@code SavingsAccount.withdraw()} raises at approval time -- account not + * active, or still inside its lock-in period -- checked here against the same columns, so the + * clerk sees the real reason when the request is raised rather than a generic failure once a + * manager tries to approve it. Balance is checked too, against the account's own minimum, since + * an entry that can never post is no more usable sitting in the approval queue than one refused + * outright. + */ + private void validateSavingsWithdrawalEligible(final Long savingsAccountId, final BigDecimal amount) { + final List> rows = jdbcTemplate.queryForList(""" + SELECT account_no, status_enum, lockedin_until_date_derived, account_balance_derived, + min_required_balance, enforce_min_required_balance + FROM m_savings_account WHERE id = ? + """, savingsAccountId); + if (rows.isEmpty()) { + return; // not found here is not this check's job -- the id/type mismatch surfaces elsewhere + } + final Map row = rows.get(0); + final String accountNo = (String) row.get("account_no"); + final Number statusEnum = (Number) row.get("status_enum"); + if (statusEnum == null || statusEnum.intValue() != SAVINGS_STATUS_ACTIVE) { + throw apiError(422, "Withdrawal not allowed for savings account " + accountNo + ": account is not active."); + } + final java.sql.Date lockedUntil = (java.sql.Date) row.get("lockedin_until_date_derived"); + if (lockedUntil != null) { + final LocalDate today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()); + final LocalDate lockedUntilDate = lockedUntil.toLocalDate(); + if (today.isBefore(lockedUntilDate)) { + throw apiError(422, "Withdrawal not allowed for savings account " + accountNo + + ": account is locked-in until " + lockedUntilDate + "."); + } + } + final BigDecimal balance = (BigDecimal) row.get("account_balance_derived"); + final boolean enforceMin = Boolean.TRUE.equals(row.get("enforce_min_required_balance")); + final BigDecimal minRequired = enforceMin && row.get("min_required_balance") != null + ? (BigDecimal) row.get("min_required_balance") : BigDecimal.ZERO; + if (balance != null && balance.subtract(amount).compareTo(minRequired) < 0) { + throw apiError(422, "Withdrawal not allowed for savings account " + accountNo + ": available balance (" + + balance.subtract(minRequired) + ") is less than the requested amount (" + amount + ")."); + } + } + private void addDisbursementVoucherLines(final Map record, final JsonArray creditEntries, final Long sourceGlId, final String requestType) {