From d002fc0c9b87ddb20ee6d7c7f7fbfcae549ae268 Mon Sep 17 00:00:00 2001 From: Jaanu423 Date: Tue, 16 Jun 2026 15:27:32 +0530 Subject: [PATCH] =?UTF-8?q?feat(clerk):=20add=20collection=20flow=20backen?= =?UTF-8?q?d=20=E2=80=94=20savings=20withdrawal=20+=20Cash@Vault=20GL=20of?= =?UTF-8?q?fset?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ClerkDisbursementRequestApiResource: support collection mode (dbCr='Dr' entries); submit() separates DR/CR totals and validates balance; approve() detects mode, calls executeSavingsWithdrawal/executeCurrentAccountWithdrawal for DR entries, then posts GL offset via findCashVaultGlId(txnRefs, type_enum=1) → target GL - ClerkWithdrawalRequestApiResource: minor updates - DB migration: changelog-tenant.xml + 0333_rename_disbursed_to_withdrawn.xml Co-Authored-By: Claude Sonnet 4.6 --- .../ClerkDisbursementRequestApiResource.java | 284 ++++++++++++++---- .../ClerkWithdrawalRequestApiResource.java | 12 +- .../db/changelog/tenant/changelog-tenant.xml | 1 + .../0333_rename_disbursed_to_withdrawn.xml | 16 + 4 files changed, 245 insertions(+), 68 deletions(-) create mode 100644 fineract-provider/src/main/resources/db/changelog/tenant/parts/0333_rename_disbursed_to_withdrawn.xml 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 b1d9248e9..83ed14900 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 @@ -50,7 +50,10 @@ import lombok.RequiredArgsConstructor; import org.apache.fineract.commands.domain.CommandWrapper; import org.apache.fineract.commands.service.CommandWrapperBuilder; import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.commands.service.SynchronousCommandProcessingService; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder; +import org.apache.fineract.infrastructure.core.exception.IdempotentCommandProcessSucceedException; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.voucher.service.VoucherService; @@ -69,6 +72,7 @@ public class ClerkDisbursementRequestApiResource { private final FromJsonHelper fromJsonHelper; private final PlatformSecurityContext context; private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; + private final FineractRequestContextHolder fineractRequestContextHolder; private final VoucherService voucherService; private final Gson gson = new GsonBuilder().serializeNulls().create(); @@ -93,14 +97,11 @@ public class ClerkDisbursementRequestApiResource { final String description = fromJsonHelper.extractStringNamed("description", root); final String creditEntriesJson = fromJsonHelper.extractStringNamed("creditEntries", root); - if (glAccountNo == null || glAccountNo.isBlank()) { - throw apiError(400, "GL account number (DR side) is required"); - } if (creditEntriesJson == null || creditEntriesJson.isBlank()) { - throw apiError(400, "At least one credit entry (CR side) is required"); + throw apiError(400, "At least one entry is required"); } - // Parse and validate credit entries + // Parse and validate entries final JsonArray creditEntries; try { creditEntries = gson.fromJson(creditEntriesJson, JsonArray.class); @@ -108,25 +109,45 @@ public class ClerkDisbursementRequestApiResource { throw apiError(400, "creditEntries is not valid JSON"); } if (creditEntries == null || creditEntries.size() == 0) { - throw apiError(400, "At least one credit entry (CR side) is required"); + throw apiError(400, "At least one entry is required"); } BigDecimal totalCr = BigDecimal.ZERO; + BigDecimal totalDr = BigDecimal.ZERO; for (final JsonElement el : creditEntries) { final JsonObject entry = el.getAsJsonObject(); + final String dbCr = entry.has("dbCr") ? entry.get("dbCr").getAsString() : "Cr"; final String amt = entry.has("amount") ? entry.get("amount").getAsString() : null; final BigDecimal entryAmt = parseBigDecimal(amt); if (entryAmt == null || entryAmt.compareTo(BigDecimal.ZERO) <= 0) { - throw apiError(400, "Each credit entry must have a positive amount"); + throw apiError(400, "Each entry must have a positive amount"); } if (!entry.has("accountNo") || entry.get("accountNo").getAsString().isBlank()) { - throw apiError(400, "Each credit entry must have an account number"); + throw apiError(400, "Each entry must have an account number"); + } + if ("Dr".equals(dbCr)) { + totalDr = totalDr.add(entryAmt); + } else { + totalCr = totalCr.add(entryAmt); } - totalCr = totalCr.add(entryAmt); } - if (totalCr.compareTo(BigDecimal.ZERO) <= 0) { - throw apiError(400, "Total credit amount must be greater than zero"); + // Validate balance when both DR and CR entries present (collection mode) + if (totalDr.compareTo(BigDecimal.ZERO) > 0 && totalCr.compareTo(BigDecimal.ZERO) > 0 + && totalDr.subtract(totalCr).abs().compareTo(new BigDecimal("0.01")) > 0) { + throw apiError(400, "DR entries total (" + totalDr + ") must equal CR entries total (" + totalCr + ")"); + } + + // Transaction amount = CR side for disbursement; either side for collection (they are equal) + final BigDecimal txnAmount = totalCr.compareTo(BigDecimal.ZERO) > 0 ? totalCr : totalDr; + if (txnAmount.compareTo(BigDecimal.ZERO) <= 0) { + throw apiError(400, "Total transaction amount must be greater than zero"); + } + + // Source GL is required for disbursement; collection uses DR savings entries instead + final boolean hasDrEntries = totalDr.compareTo(BigDecimal.ZERO) > 0; + if ((glAccountNo == null || glAccountNo.isBlank()) && !hasDrEntries) { + throw apiError(400, "Source GL account (disbursement) or DR savings entries (collection) are required"); } final String activityLog = buildActivityLog(null, "SUBMITTED", currentUser.getUsername(), null); @@ -140,7 +161,7 @@ public class ClerkDisbursementRequestApiResource { RETURNING id """, Long.class, - glAccountId, glAccountNo, glAccountName, totalCr, + glAccountId, glAccountNo, glAccountName, txnAmount, officeId != null ? officeId : 1L, currencyCode, creditEntriesJson, paymentTypeId, paymentTypeName, description, "PENDING", @@ -150,12 +171,12 @@ public class ClerkDisbursementRequestApiResource { final String voucherNo = voucherService.createVoucher( VoucherService.TYPE_DISBURSEMENT, VoucherService.REF_CLERK_DISBURSEMENT_REQUEST, - newId, totalCr, currentUser); + newId, txnAmount, currentUser); jdbcTemplate.update( "UPDATE m_clerk_disbursement_request SET voucher_no = ? WHERE id = ?", voucherNo, newId); return gson.toJson(Map.of("resourceId", newId, "status", "PENDING", - "voucherNo", voucherNo, "totalAmount", totalCr)); + "voucherNo", voucherNo, "totalAmount", txnAmount)); } // ── GET /v1/clerk/disbursement-requests ── Manager sees all requests ───────── @@ -295,73 +316,139 @@ public class ClerkDisbursementRequestApiResource { final String description = (String) record.get("description"); final String txnDateStr = LocalDate.now().toString(); - // Validate all entries have account IDs before posting any transaction + final Long sourceGlId = record.get("gl_account_id") != null + ? ((Number) record.get("gl_account_id")).longValue() : null; + final Long officeId = record.get("office_id") != null + ? ((Number) record.get("office_id")).longValue() : 1L; + final String currencyCode = record.get("currency_code") != null + ? (String) record.get("currency_code") : "INR"; + + // Validate all entries have required IDs before posting any transaction for (int i = 0; i < creditEntries.size(); i++) { final JsonObject entry = creditEntries.get(i).getAsJsonObject(); - final boolean hasId = entry.has("savingsAccountId") && !entry.get("savingsAccountId").isJsonNull(); - if (!hasId) { - final String acNo = entry.has("accountNo") ? entry.get("accountNo").getAsString() : "entry " + (i + 1); - throw apiError(400, "Account ID missing for credit entry: " + acNo - + ". Clerk must re-submit with account details loaded."); + final String entryType = entry.has("accountType") ? entry.get("accountType").getAsString() : "SAVINGS"; + final String acNo = entry.has("accountNo") ? entry.get("accountNo").getAsString() : "entry " + (i + 1); + if ("GL".equals(entryType)) { + if (!entry.has("glAccountId") || entry.get("glAccountId").isJsonNull()) { + throw apiError(400, "GL account ID missing for GL credit entry: " + acNo + + ". Clerk must re-submit with account details loaded."); + } + } else { + if (!entry.has("savingsAccountId") || entry.get("savingsAccountId").isJsonNull()) { + throw apiError(400, "Account ID missing for credit entry: " + acNo + + ". Clerk must re-submit with account details loaded."); + } + } + } + + // Detect collection mode: any DR entry in creditEntries = collection (savings debit) + boolean isCollectionMode = false; + for (final JsonElement el : creditEntries) { + final JsonObject e = el.getAsJsonObject(); + if ("Dr".equals(e.has("dbCr") ? e.get("dbCr").getAsString() : "Cr")) { + isCollectionMode = true; + break; } } final List txnRefs = new ArrayList<>(); - final List savingsTxnRefs = new ArrayList<>(); + // Disbursement: savings deposits — for offset DR sourceGL / CR Cash@Vault + final List depositSavingsTxnRefs = new ArrayList<>(); + BigDecimal depositSavingsTotal = BigDecimal.ZERO; + // Collection: savings withdrawals — for offset DR Cash@Vault / CR targetGL + final List withdrawalSavingsTxnRefs = new ArrayList<>(); + BigDecimal withdrawalSavingsTotal = BigDecimal.ZERO; + Long collectionTargetGlId = null; - // Post savings/current deposit for each CR entry — all or nothing (rollback on any failure) + // Process each entry — all or nothing (@Transactional rolls back on any failure) for (final JsonElement el : creditEntries) { final JsonObject entry = el.getAsJsonObject(); - final Long savingsAccountId = entry.get("savingsAccountId").getAsLong(); + final String dbCr = entry.has("dbCr") ? entry.get("dbCr").getAsString() : "Cr"; final BigDecimal entryAmt = new BigDecimal(entry.get("amount").getAsString()); + final String entryType = entry.has("accountType") ? entry.get("accountType").getAsString() : "SAVINGS"; + final String acNo = entry.has("accountNo") ? entry.get("accountNo").getAsString() : "?"; final Long entryPtId = entry.has("paymentTypeId") && !entry.get("paymentTypeId").isJsonNull() ? entry.get("paymentTypeId").getAsLong() : (ptId != null ? ((Number) ptId).longValue() : null); - final String acNo = entry.has("accountNo") ? entry.get("accountNo").getAsString() : String.valueOf(savingsAccountId); - final boolean isCurrentAccount = isCurrentAccountById(savingsAccountId); - final String txnId; - try { - if (isCurrentAccount) { - txnId = executeCurrentAccountDeposit(savingsAccountId, entryAmt, entryPtId, txnDateStr, description); - } else { - txnId = executeSavingsDeposit(savingsAccountId, entryAmt, entryPtId, txnDateStr, description); + if ("GL".equals(entryType)) { + final Long glId = entry.get("glAccountId").getAsLong(); + if ("Cr".equals(dbCr)) { + if (!isCollectionMode && sourceGlId != null) { + // Disbursement: direct GL-to-GL — DR sourceGL / CR thisGL + postGlOffsetEntry(sourceGlId, glId, entryAmt, officeId, currencyCode, txnDateStr, description); + } else { + // Collection: CR GL is the target for the Cash@Vault offset + collectionTargetGlId = glId; + } } - } catch (Exception e) { - // Rethrow so @Transactional rolls back all previously posted deposits - throw apiError(422, "Failed to credit account " + acNo + ": " - + (e.getMessage() != null ? e.getMessage() : e.getClass().getSimpleName())); + txnRefs.add("GL:" + acNo); + continue; } - if (txnId != null) { - txnRefs.add(txnId); - if (!isCurrentAccount) savingsTxnRefs.add(txnId); + + // Savings or Current account + final Long accountId = entry.get("savingsAccountId").getAsLong(); + final boolean isCurrentAccount = isCurrentAccountById(accountId); + String txnId = null; + + if ("Cr".equals(dbCr)) { + // Deposit — credit savings/current account (disbursement) + try { + txnId = isCurrentAccount + ? executeCurrentAccountDeposit(accountId, entryAmt, entryPtId, txnDateStr, description) + : executeSavingsDeposit(accountId, entryAmt, entryPtId, txnDateStr, description); + } catch (IdempotentCommandProcessSucceedException idempotent) { + txnId = extractResourceIdFromResponse(idempotent.getResponse()); + } catch (Exception ex) { + throw apiError(422, "Failed to credit account " + acNo + ": " + + (ex.getMessage() != null ? ex.getMessage() : ex.getClass().getSimpleName())); + } + if (txnId != null) { + txnRefs.add(txnId); + if (!isCurrentAccount) { + depositSavingsTxnRefs.add(txnId); + depositSavingsTotal = depositSavingsTotal.add(entryAmt); + } + } + } else { + // Withdrawal — debit savings/current account (collection) + try { + txnId = isCurrentAccount + ? executeCurrentAccountWithdrawal(accountId, entryAmt, entryPtId, txnDateStr, description) + : executeSavingsWithdrawal(accountId, entryAmt, entryPtId, txnDateStr, description); + } catch (IdempotentCommandProcessSucceedException idempotent) { + txnId = extractResourceIdFromResponse(idempotent.getResponse()); + } catch (Exception ex) { + throw apiError(422, "Failed to debit account " + acNo + ": " + + (ex.getMessage() != null ? ex.getMessage() : ex.getClass().getSimpleName())); + } + if (txnId != null) { + txnRefs.add(txnId); + if (!isCurrentAccount) { + withdrawalSavingsTxnRefs.add(txnId); + withdrawalSavingsTotal = withdrawalSavingsTotal.add(entryAmt); + } + } } } - // Post offsetting GL journal entry: DR source GL / CR Cash at Vault - // Fineract's savings deposit auto-creates "DR Cash at Vault / CR Savings" via product GL mapping. - // This offsetting entry makes the net accounting: DR source GL / CR savings accounts. - final Long sourceGlId = record.get("gl_account_id") != null - ? ((Number) record.get("gl_account_id")).longValue() : null; - if (sourceGlId != null && !savingsTxnRefs.isEmpty()) { - Long cashVaultGlId = null; - for (final String txnRef : savingsTxnRefs) { - final List> jeRows = jdbcTemplate.queryForList( - "SELECT account_id FROM acc_gl_journal_entry" - + " WHERE transaction_id = ? AND type_enum = 1 LIMIT 1", "S" + txnRef); - if (!jeRows.isEmpty()) { - cashVaultGlId = ((Number) jeRows.get(0).get("account_id")).longValue(); - break; - } - } + // Disbursement offset: deposits auto-create DR Cash@Vault / CR Savings. + // Cancel Cash@Vault side: DR sourceGL / CR Cash@Vault. Net: DR sourceGL / CR Savings. + if (sourceGlId != null && !depositSavingsTxnRefs.isEmpty()) { + final Long cashVaultGlId = findCashVaultGlId(depositSavingsTxnRefs, 2); // DEBIT side of deposit entry if (cashVaultGlId != null) { - final BigDecimal totalAmount = (BigDecimal) record.get("dr_amount"); - final Long officeId = record.get("office_id") != null - ? ((Number) record.get("office_id")).longValue() : 1L; - final String currencyCode = record.get("currency_code") != null - ? (String) record.get("currency_code") : "INR"; - postGlOffsetEntry(sourceGlId, cashVaultGlId, totalAmount, officeId, - currencyCode, txnDateStr, description); + postGlOffsetEntry(sourceGlId, cashVaultGlId, depositSavingsTotal, + officeId, currencyCode, txnDateStr, description); + } + } + + // Collection offset: withdrawals auto-create DR Savings / CR Cash@Vault. + // Cancel Cash@Vault side: DR Cash@Vault / CR targetGL. Net: DR Savings / CR targetGL. + if (collectionTargetGlId != null && !withdrawalSavingsTxnRefs.isEmpty()) { + final Long cashVaultGlId = findCashVaultGlId(withdrawalSavingsTxnRefs, 1); // CREDIT side of withdrawal entry + if (cashVaultGlId != null) { + postGlOffsetEntry(cashVaultGlId, collectionTargetGlId, withdrawalSavingsTotal, + officeId, currencyCode, txnDateStr, description); } } @@ -489,6 +576,9 @@ public class ClerkDisbursementRequestApiResource { .savingsAccountDeposit(savingsAccountId) .withJson(payload.toString()) .build(); + // Clear the stored command source ID so each deposit is treated as a fresh command, + // not a retry of the previous deposit within the same HTTP request. + fineractRequestContextHolder.setAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID, null); final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(cmd); return result.getResourceId() != null ? result.getResourceId().toString() : null; } @@ -510,6 +600,7 @@ public class ClerkDisbursementRequestApiResource { .currentAccountDeposit(currentAccountId) .withJson(payload.toString()) .build(); + fineractRequestContextHolder.setAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID, null); final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(cmd); return result.getResourceId() != null ? result.getResourceId().toString() : null; } @@ -554,6 +645,74 @@ public class ClerkDisbursementRequestApiResource { return gson.toJson(log); } + private String extractResourceIdFromResponse(final String responseJson) { + if (responseJson == null || responseJson.isBlank()) return null; + try { + final JsonObject obj = gson.fromJson(responseJson, JsonObject.class); + return obj.has("resourceId") && !obj.get("resourceId").isJsonNull() + ? obj.get("resourceId").getAsString() : null; + } catch (Exception e) { + return null; + } + } + + private String executeSavingsWithdrawal( + final Long savingsAccountId, final BigDecimal amount, + final Long paymentTypeId, final String txnDateStr, final String note) { + + final String date = formatForFineract(txnDateStr); + 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", paymentTypeId); + if (note != null && !note.isBlank()) payload.addProperty("note", note); + + final CommandWrapper cmd = new CommandWrapperBuilder() + .savingsAccountWithdrawal(savingsAccountId) + .withJson(payload.toString()) + .build(); + fineractRequestContextHolder.setAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID, null); + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(cmd); + return result.getResourceId() != null ? result.getResourceId().toString() : null; + } + + private String executeCurrentAccountWithdrawal( + final Long currentAccountId, final BigDecimal amount, + final Long paymentTypeId, final String txnDateStr, final String note) { + + final String date = formatForFineract(txnDateStr); + 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", paymentTypeId); + if (note != null && !note.isBlank()) payload.addProperty("note", note); + + final CommandWrapper cmd = new CommandWrapperBuilder() + .currentAccountWithdrawal(currentAccountId) + .withJson(payload.toString()) + .build(); + fineractRequestContextHolder.setAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID, null); + final CommandProcessingResult result = commandsSourceWritePlatformService.logCommandSource(cmd); + return result.getResourceId() != null ? result.getResourceId().toString() : null; + } + + private Long findCashVaultGlId(final List txnRefs, final int typeEnum) { + for (final String txnRef : txnRefs) { + final List> rows = jdbcTemplate.queryForList( + "SELECT account_id FROM acc_gl_journal_entry" + + " WHERE transaction_id = ? AND type_enum = ? LIMIT 1", + "S" + txnRef, typeEnum); + if (!rows.isEmpty()) { + return ((Number) rows.get(0).get("account_id")).longValue(); + } + } + return null; + } + private void postGlOffsetEntry( final Long drGlAccountId, final Long crGlAccountId, final BigDecimal amount, final Long officeId, @@ -579,7 +738,7 @@ public class ClerkDisbursementRequestApiResource { payload.addProperty("locale", "en"); payload.addProperty("currencyCode", currencyCode != null ? currencyCode : "INR"); if (comments != null && !comments.isBlank()) { - payload.addProperty("comments", "Disbursement GL offset: " + comments); + payload.addProperty("comments", "GL offset: " + comments); } payload.add("debits", debits); payload.add("credits", credits); @@ -588,6 +747,7 @@ public class ClerkDisbursementRequestApiResource { .createJournalEntry() .withJson(payload.toString()) .build(); + fineractRequestContextHolder.setAttribute(SynchronousCommandProcessingService.COMMAND_SOURCE_ID, null); commandsSourceWritePlatformService.logCommandSource(cmd); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkWithdrawalRequestApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkWithdrawalRequestApiResource.java index cae04d393..bd7055cc6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkWithdrawalRequestApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/clerkwithdrawal/api/ClerkWithdrawalRequestApiResource.java @@ -287,8 +287,8 @@ public class ClerkWithdrawalRequestApiResource { case "APPROVED" -> gson.toJson(rows.get(0)); case "PENDING" -> throw tokenError(422, "Token '" + token + "' is still PENDING — manager has not approved this request yet."); - case "DISBURSED" -> throw tokenError(422, - "Token '" + token + "' has already been disbursed. Cash was handed over by another cashier."); + case "WITHDRAWN" -> throw tokenError(422, + "Token '" + token + "' has already been withdrawn. Cash was handed over by another cashier."); case "REJECTED" -> throw tokenError(422, "Token '" + token + "' was REJECTED by the manager. This withdrawal cannot be processed."); default -> throw tokenError(422, @@ -573,13 +573,13 @@ public class ClerkWithdrawalRequestApiResource { } final String updatedLog = buildActivityLog( - (String) record.get("activity_log"), "DISBURSED", currentUser.getUsername(), null + (String) record.get("activity_log"), "WITHDRAWN", currentUser.getUsername(), null ); // WHERE status = 'APPROVED' prevents race condition — only one cashier can win final int updated = jdbcTemplate.update(""" UPDATE m_clerk_withdrawal_request SET - status = 'DISBURSED', cashier_id = ?, disbursed_by = ?, + status = 'WITHDRAWN', cashier_id = ?, disbursed_by = ?, disbursement_denomination_data = ?, activity_log = ?, disbursed_at = ? WHERE id = ? AND status = 'APPROVED' """, @@ -588,7 +588,7 @@ public class ClerkWithdrawalRequestApiResource { ); if (updated == 0) { - throw apiError(409, "This withdrawal has already been disbursed by another cashier. Double disbursement prevented."); + throw apiError(409, "This withdrawal has already been withdrawn by another cashier. Double disbursement prevented."); } // Record denomination breakdown in m_savings_txn_denomination so cashier vault position updates @@ -597,7 +597,7 @@ public class ClerkWithdrawalRequestApiResource { } final Object voucherNoObj = record.get("voucher_no"); - return gson.toJson(Map.of("resourceId", id, "status", "DISBURSED", + return gson.toJson(Map.of("resourceId", id, "status", "WITHDRAWN", "voucherNo", voucherNoObj != null ? voucherNoObj.toString() : "")); } diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml index 0536d80df..a39cc16de 100644 --- a/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml +++ b/fineract-provider/src/main/resources/db/changelog/tenant/changelog-tenant.xml @@ -360,4 +360,5 @@ + diff --git a/fineract-provider/src/main/resources/db/changelog/tenant/parts/0333_rename_disbursed_to_withdrawn.xml b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0333_rename_disbursed_to_withdrawn.xml new file mode 100644 index 000000000..8369fc165 --- /dev/null +++ b/fineract-provider/src/main/resources/db/changelog/tenant/parts/0333_rename_disbursed_to_withdrawn.xml @@ -0,0 +1,16 @@ + + + + + + + + + UPDATE m_clerk_withdrawal_request SET status = 'WITHDRAWN' WHERE status = 'DISBURSED' + + + +