diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashierdeposit/api/CashierDepositRequestApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashierdeposit/api/CashierDepositRequestApiResource.java index 7378801ec..25b268f90 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashierdeposit/api/CashierDepositRequestApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/cashierdeposit/api/CashierDepositRequestApiResource.java @@ -418,13 +418,20 @@ public class CashierDepositRequestApiResource { txnId = executeGlJournalEntry(glAccountId, contraGlId, glOfficeId, currencyCode, amount, ptId, txnDateStr, description, true); } else { - // Determine real account type by checking which table owns this ID. + // Determine real account type by checking which table/column 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); - txnId = isCurrentAccount - ? executeCurrentAccountDeposit(savingsAccountId, amount, ptId, txnDateStr, description) - : executeSavingsDeposit(savingsAccountId, amount, ptId, txnDateStr, description); + // even for current accounts (submitted before the frontend fix), and FD/RD accounts share + // the very same m_savings_account table as a plain savings account, distinguished only by + // deposit_type_enum -- so this is looked up fresh rather than trusted from the request. + final String depositCategory = resolveDepositAccountCategory(savingsAccountId); + final boolean isCurrentAccount = "CURRENT".equals(depositCategory); + txnId = switch (depositCategory) { + case "CURRENT" -> executeCurrentAccountDeposit(savingsAccountId, amount, ptId, txnDateStr, description); + case "FIXED_DEPOSIT" -> executeFixedDepositDeposit(savingsAccountId, amount, ptId, txnDateStr, description); + case "RECURRING_DEPOSIT" -> + executeRecurringDepositDeposit(savingsAccountId, amount, ptId, txnDateStr, description); + default -> executeSavingsDeposit(savingsAccountId, amount, ptId, txnDateStr, description); + }; if (!isCurrentAccount && txnId != null) { // approve() is one @Transactional block, so an uncaught exception here would roll back // the deposit that was just posted along with it -- every Cheque/DD approval that ever @@ -718,6 +725,64 @@ public class CashierDepositRequestApiResource { return result.getResourceId() != null ? result.getResourceId().toString() : null; } + private String executeFixedDepositDeposit( + final Long accountId, final BigDecimal amount, + final Object paymentTypeId, final String txnDateStr, final String note) { + + final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).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() + .fixedDepositAccountDeposit(accountId) + .withJson(payload.toString()) + .build(); + + final CommandProcessingResult result = voucherService + .withOwnVoucher(() -> commandsSourceWritePlatformService.logCommandSource(commandRequest)); + return result.getResourceId() != null ? result.getResourceId().toString() : null; + } + + private String executeRecurringDepositDeposit( + final Long accountId, final BigDecimal amount, + final Object paymentTypeId, final String txnDateStr, final String note) { + + final String today = LocalDate.now(DateUtils.getDateTimeZoneOfTenant()).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() + .recurringAccountDeposit(accountId) + .withJson(payload.toString()) + .build(); + + final CommandProcessingResult result = voucherService + .withOwnVoucher(() -> commandsSourceWritePlatformService.logCommandSource(commandRequest)); + return result.getResourceId() != null ? result.getResourceId().toString() : null; + } + /** * A Cheque/DD deposit is credited to the ledger immediately (same as cash) but is not really the * branch's own money yet -- it still has to clear. Real-world CBS practice is to hold that amount @@ -809,6 +874,32 @@ public class CashierDepositRequestApiResource { } } + /** + * "CURRENT", "FIXED_DEPOSIT", "RECURRING_DEPOSIT" or "SAVINGS" — checked against the database + * rather than the request's own account_type string. FD and RD accounts live in the same + * m_savings_account table as a plain savings account (deposit_type_enum 200/300 vs 100), so a + * plain "not current → savings" check would post an FD/RD deposit through the generic savings + * deposit command, which does not accept a term-deposit account id. + */ + private String resolveDepositAccountCategory(final Long accountId) { + if (accountId == null) { + return "SAVINGS"; + } + if (isCurrentAccountById(accountId)) { + return "CURRENT"; + } + final List depositTypes = jdbcTemplate.queryForList( + "SELECT deposit_type_enum FROM m_savings_account WHERE id = ?", Integer.class, accountId); + final Integer depositType = depositTypes.isEmpty() ? null : depositTypes.get(0); + if (Integer.valueOf(200).equals(depositType)) { + return "FIXED_DEPOSIT"; + } + if (Integer.valueOf(300).equals(depositType)) { + return "RECURRING_DEPOSIT"; + } + return "SAVINGS"; + } + // Parses a Fineract transaction id string to Long. // Savings/current txn ids are decimal; GL journal entry ids are hex strings. private Long parseTxnIdToLong(final String txnId) {