fix(cashier+clerk): route CA deposits and withdrawals to current account commands

CashierDepositRequestApiResource:
- approve(): use isCurrentAccountById() to detect current accounts, route to
  executeCurrentAccountDeposit() instead of executeSavingsDeposit()
- Add executeCurrentAccountDeposit() using CommandWrapperBuilder.currentAccountDeposit()
- Add isCurrentAccountById() helper querying m_current_account table
- Add POST /{id}/reprocess endpoint to fix already-approved CA records that
  were wrongly posted to savings account

ClerkWithdrawalRequestApiResource:
- approve(): use isCurrentAccountById() to detect current accounts, route to
  executeCurrentAccountWithdrawal() instead of executeSavingsWithdrawal()
- Add executeCurrentAccountWithdrawal() using CommandWrapperBuilder.currentAccountWithdrawal()
- Add isCurrentAccountById() helper querying m_current_account table

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
Jaanu423
2026-06-15 12:52:09 +05:30
co-authored by Claude Sonnet 4.6
parent 33ea10689f
commit 40449ef0f7
2 changed files with 135 additions and 2 deletions
@@ -335,7 +335,14 @@ public class CashierDepositRequestApiResource {
final String txnDateStr = txnDateObj != null ? txnDateObj.toString().split(" ")[0] : null;
final String description = (String) record.get("description");
final String txnId = executeSavingsDeposit(savingsAccountId, amount, ptId, txnDateStr, description);
// Determine real account type by checking which table owns this ID.
// Do NOT rely solely on the stored account_type field — old records may have 'SAVINGS'
// even for current accounts (submitted before the frontend fix).
final boolean isCurrentAccount = isCurrentAccountById(savingsAccountId);
final String txnId = isCurrentAccount
? executeCurrentAccountDeposit(savingsAccountId, amount, ptId, txnDateStr, description)
: executeSavingsDeposit(savingsAccountId, amount, ptId, txnDateStr, description);
// Link voucher to the created savings txn and stamp voucher_no on it
if (txnId != null) {
@@ -419,6 +426,52 @@ public class CashierDepositRequestApiResource {
return gson.toJson(Map.of("resourceId", id, "status", "REJECTED"));
}
// ── POST /v1/cashier/deposit-requests/{id}/reprocess ── Fix wrongly-routed approvals ──
// Use this for records that show APPROVED but whose deposit went to the wrong account
// (savings instead of current). Requires ADMIN / ALL_FUNCTIONS permission.
@POST
@Path("/{id}/reprocess")
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
@Transactional
public String reprocess(@PathParam("id") final Long id, final String body) {
final AppUser currentUser = context.authenticatedUser();
if (!currentUser.hasAnyPermission("ALL_FUNCTIONS")) {
throw apiError(403, "Only a super-admin (ALL_FUNCTIONS) can reprocess deposit requests.");
}
final Map<?, ?> record = loadRecord(id);
if (!"APPROVED".equals(record.get("status"))) {
throw apiError(409, "Only APPROVED requests can be reprocessed. Current status: " + record.get("status"));
}
final Long savingsAccountId = ((Number) record.get("savings_account_id")).longValue();
final BigDecimal amount = new BigDecimal(record.get("amount").toString());
final Object ptId = record.get("payment_type_id");
final Object txnDateObj = record.get("txn_date");
final String txnDateStr = txnDateObj != null ? txnDateObj.toString().split(" ")[0] : null;
final String description = (String) record.get("description");
if (!isCurrentAccountById(savingsAccountId)) {
throw apiError(400, "This account is not a current account. Reprocess is only needed for current account deposits.");
}
final String txnId = executeCurrentAccountDeposit(savingsAccountId, amount, ptId, txnDateStr, description);
if (txnId != null) {
voucherService.linkSavingsTransaction(VoucherService.REF_CASHIER_DEPOSIT_REQUEST, id, Long.valueOf(txnId));
jdbcTemplate.update("UPDATE m_cashier_deposit_request SET txn_id = ? WHERE id = ?", txnId, id);
}
final JsonObject res = new JsonObject();
res.addProperty("resourceId", id);
res.addProperty("status", "APPROVED");
res.addProperty("reprocessed", true);
if (txnId != null) res.addProperty("txnId", txnId);
return gson.toJson(res);
}
// ── helpers ──────────────────────────────────────────────────────────────────
private Map<?, ?> loadRecord(final Long id) {
@@ -464,6 +517,45 @@ public class CashierDepositRequestApiResource {
return result.getResourceId() != null ? result.getResourceId().toString() : null;
}
private String executeCurrentAccountDeposit(
final Long currentAccountId, final BigDecimal amount,
final Object paymentTypeId, final String txnDateStr, final String note) {
final String today = LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
final JsonObject payload = new JsonObject();
payload.addProperty("locale", "en");
payload.addProperty("dateFormat", "dd MMMM yyyy");
payload.addProperty("transactionDate", date);
payload.addProperty("transactionAmount", amount);
if (paymentTypeId != null) {
payload.addProperty("paymentTypeId", ((Number) paymentTypeId).longValue());
}
if (note != null && !note.isBlank()) {
payload.addProperty("note", note);
}
final CommandWrapper commandRequest = new CommandWrapperBuilder()
.currentAccountDeposit(currentAccountId)
.withJson(payload.toString())
.build();
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
return result.getResourceId() != null ? result.getResourceId().toString() : null;
}
/** Returns true if this account ID belongs to m_current_account (not m_savings_account). */
private boolean isCurrentAccountById(final Long accountId) {
try {
final Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM m_current_account WHERE id = ?", Integer.class, accountId);
return count != null && count > 0;
} catch (Exception e) {
return false;
}
}
/**
* Saves the denomination breakdown the cashier entered at submit time into
* m_savings_txn_denomination once the deposit is approved, so the cashier's
@@ -442,7 +442,10 @@ public class ClerkWithdrawalRequestApiResource {
String txnId = null;
if (savingsAccountId != null) {
txnId = executeSavingsWithdrawal(savingsAccountId, amount, ptId, txnDateStr, description);
final boolean isCurrentAccount = isCurrentAccountById(savingsAccountId);
txnId = isCurrentAccount
? executeCurrentAccountWithdrawal(savingsAccountId, amount, ptId, txnDateStr, description)
: executeSavingsWithdrawal(savingsAccountId, amount, ptId, txnDateStr, description);
}
// Link voucher to the created savings txn and stamp voucher_no on it
@@ -734,6 +737,44 @@ public class ClerkWithdrawalRequestApiResource {
return result.getResourceId() != null ? result.getResourceId().toString() : null;
}
private String executeCurrentAccountWithdrawal(
final Long currentAccountId, final BigDecimal amount,
final Object paymentTypeId, final String txnDateStr, final String note) {
final String today = LocalDate.now().format(DateTimeFormatter.ofPattern("dd MMMM yyyy"));
final String date = txnDateStr != null && !txnDateStr.isBlank() ? formatForFineract(txnDateStr) : today;
final JsonObject payload = new JsonObject();
payload.addProperty("locale", "en");
payload.addProperty("dateFormat", "dd MMMM yyyy");
payload.addProperty("transactionDate", date);
payload.addProperty("transactionAmount", amount);
if (paymentTypeId != null) {
payload.addProperty("paymentTypeId", ((Number) paymentTypeId).longValue());
}
if (note != null && !note.isBlank()) {
payload.addProperty("note", note);
}
final CommandWrapper commandRequest = new CommandWrapperBuilder()
.currentAccountWithdrawal(currentAccountId)
.withJson(payload.toString())
.build();
final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(commandRequest);
return result.getResourceId() != null ? result.getResourceId().toString() : null;
}
private boolean isCurrentAccountById(final Long accountId) {
try {
final Integer count = jdbcTemplate.queryForObject(
"SELECT COUNT(*) FROM m_current_account WHERE id = ?", Integer.class, accountId);
return count != null && count > 0;
} catch (Exception e) {
return false;
}
}
private String formatForFineract(final String isoDate) {
try {
final LocalDate ld = LocalDate.parse(isoDate, DateTimeFormatter.ISO_LOCAL_DATE);