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:
co-authored by
Claude Sonnet 4.6
parent
33ea10689f
commit
40449ef0f7
+93
-1
@@ -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
|
||||
|
||||
+42
-1
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user