Skip to content

Commit 689126b

Browse files
FINERACT-2463: Allow undo of Fixed Deposit transactions
1 parent 9eb94dd commit 689126b

6 files changed

Lines changed: 223 additions & 4 deletions

File tree

fineract-client/src/main/java/org/apache/fineract/client/util/FineractClient.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,7 @@
7272
import org.apache.fineract.client.services.ExternalServicesApi;
7373
import org.apache.fineract.client.services.FetchAuthenticatedUserDetailsApi;
7474
import org.apache.fineract.client.services.FixedDepositAccountApi;
75+
import org.apache.fineract.client.services.FixedDepositAccountTransactionsApi;
7576
import org.apache.fineract.client.services.FixedDepositProductApi;
7677
import org.apache.fineract.client.services.FloatingRatesApi;
7778
import org.apache.fineract.client.services.GeneralLedgerAccountApi;
@@ -262,6 +263,7 @@ public final class FineractClient {
262263
public final ProvisioningCriteriaApi provisioningCriterias;
263264
public final ProvisioningEntriesApi provisioningEntries;
264265
public final RecurringDepositAccountApi recurringDepositAccounts;
266+
public final FixedDepositAccountTransactionsApi fixedDepositAccountTransactions;
265267
public final RecurringDepositAccountTransactionsApi recurringDepositAccountTransactions;
266268
public final RecurringDepositProductApi recurringDepositProducts;
267269
public final ReportMailingJobsApi reportMailingJobs;
@@ -397,6 +399,7 @@ private FineractClient(OkHttpClient okHttpClient, Retrofit retrofit) {
397399
provisioningCriterias = retrofit.create(ProvisioningCriteriaApi.class);
398400
provisioningEntries = retrofit.create(ProvisioningEntriesApi.class);
399401
recurringDepositAccounts = retrofit.create(RecurringDepositAccountApi.class);
402+
fixedDepositAccountTransactions = retrofit.create(FixedDepositAccountTransactionsApi.class);
400403
recurringDepositAccountTransactions = retrofit.create(RecurringDepositAccountTransactionsApi.class);
401404
recurringDepositProducts = retrofit.create(RecurringDepositProductApi.class);
402405
reportMailingJobs = retrofit.create(ReportMailingJobsApi.class);

fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2419,6 +2419,17 @@ public CommandWrapperBuilder undoFixedDepositAccountApplication(final Long accou
24192419
return this;
24202420
}
24212421

2422+
public CommandWrapperBuilder undoFixedDepositAccountTransaction(final Long accountId, final Long transactionId) {
2423+
this.actionName = "UNDOTRANSACTION";
2424+
this.entityName = "FIXEDDEPOSITACCOUNT";
2425+
this.savingsId = accountId;
2426+
this.entityId = accountId;
2427+
this.subentityId = transactionId;
2428+
this.transactionId = transactionId.toString();
2429+
this.href = "/fixeddepositaccounts/" + accountId + "/transactions/" + transactionId + "?command=undo";
2430+
return this;
2431+
}
2432+
24222433
public CommandWrapperBuilder fixedDepositAccountActivation(final Long accountId) {
24232434
this.actionName = "ACTIVATE";
24242435
this.entityName = "FIXEDDEPOSITACCOUNT";

fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/api/FixedDepositAccountTransactionsApiResource.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,7 @@ public String adjustTransaction(@PathParam("fixedDepositAccountId") final Long f
157157

158158
CommandProcessingResult result = null;
159159
if (is(commandParam, DepositsApiConstants.COMMAND_UNDO_TRANSACTION)) {
160-
final CommandWrapper commandRequest = builder.undoSavingsAccountTransaction(fixedDepositAccountId, transactionId).build();
160+
final CommandWrapper commandRequest = builder.undoFixedDepositAccountTransaction(fixedDepositAccountId, transactionId).build();
161161
result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
162162
} else if (is(commandParam, DepositsApiConstants.COMMAND_ADJUST_TRANSACTION)) {
163163
final CommandWrapper commandRequest = builder.adjustSavingsAccountTransaction(fixedDepositAccountId, transactionId).build();

fineract-provider/src/main/java/org/apache/fineract/portfolio/savings/service/DepositAccountWritePlatformServiceJpaRepositoryImpl.java

Lines changed: 62 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -542,10 +542,69 @@ private void postInterest(final SavingsAccount account) {
542542
}
543543

544544
@Override
545-
public CommandProcessingResult undoFDTransaction(final Long savingsId, @SuppressWarnings("unused") final Long transactionId,
546-
@SuppressWarnings("unused") final boolean allowAccountTransferModification) {
545+
public CommandProcessingResult undoFDTransaction(final Long savingsId, final Long transactionId,
546+
final boolean allowAccountTransferModification) {
547+
548+
final boolean isSavingsInterestPostingAtCurrentPeriodEnd = this.configurationDomainService
549+
.isSavingsInterestPostingAtCurrentPeriodEnd();
550+
final Integer financialYearBeginningMonth = this.configurationDomainService.retrieveFinancialYearBeginningMonth();
551+
552+
final FixedDepositAccount account = (FixedDepositAccount) this.depositAccountAssembler.assembleFrom(savingsId,
553+
DepositAccountType.FIXED_DEPOSIT);
554+
final Set<Long> existingTransactionIds = new HashSet<>();
555+
final Set<Long> existingReversedTransactionIds = new HashSet<>();
556+
updateExistingTransactionsDetails(account, existingTransactionIds, existingReversedTransactionIds);
557+
558+
final SavingsAccountTransaction savingsAccountTransaction = this.savingsAccountTransactionRepository
559+
.findOneByIdAndSavingsAccountId(transactionId, savingsId);
560+
if (savingsAccountTransaction == null) {
561+
throw new SavingsAccountTransactionNotFoundException(savingsId, transactionId);
562+
}
563+
564+
if (!allowAccountTransferModification
565+
&& this.accountTransfersReadPlatformService.isAccountTransfer(transactionId, PortfolioAccountType.SAVINGS)) {
566+
throw new PlatformServiceUnavailableException("error.msg.fixed.deposit.account.transfer.transaction.update.not.allowed",
567+
"Fixed deposit account transaction:" + transactionId + " update not allowed as it involves in account transfer",
568+
transactionId);
569+
}
570+
571+
final LocalDate today = DateUtils.getBusinessLocalDate();
572+
final MathContext mc = MathContext.DECIMAL64;
573+
574+
if (!account.isTransactionsAllowed()) {
575+
throwValidationForActiveStatus(SavingsApiConstants.undoTransactionAction);
576+
}
577+
account.undoTransaction(transactionId);
578+
boolean isInterestTransfer = false;
579+
LocalDate postInterestOnDate = null;
580+
checkClientOrGroupActive(account);
581+
final boolean backdatedTxnsAllowedTill = false;
582+
final boolean postReversals = false;
583+
if (savingsAccountTransaction.isPostInterestCalculationRequired()
584+
&& account.isBeforeLastPostingPeriod(savingsAccountTransaction.getTransactionDate(), false)) {
585+
account.postInterest(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd, financialYearBeginningMonth,
586+
postInterestOnDate, backdatedTxnsAllowedTill);
587+
} else {
588+
account.calculateInterestUsing(mc, today, isInterestTransfer, isSavingsInterestPostingAtCurrentPeriodEnd,
589+
financialYearBeginningMonth, postInterestOnDate, backdatedTxnsAllowedTill, postReversals);
590+
}
591+
List<DepositAccountOnHoldTransaction> depositAccountOnHoldTransactions = null;
592+
if (account.getOnHoldFunds().compareTo(BigDecimal.ZERO) > 0) {
593+
depositAccountOnHoldTransactions = this.depositAccountOnHoldTransactionRepository
594+
.findBySavingsAccountAndReversedFalseOrderByCreatedDateAsc(account);
595+
}
596+
597+
account.validateAccountBalanceDoesNotBecomeNegative(SavingsApiConstants.undoTransactionAction, depositAccountOnHoldTransactions,
598+
false);
599+
final boolean isPreMatureClosure = false;
600+
account.updateMaturityDateAndAmount(mc, isPreMatureClosure, isSavingsInterestPostingAtCurrentPeriodEnd,
601+
financialYearBeginningMonth);
602+
603+
this.savingAccountRepositoryWrapper.saveAndFlush(account);
604+
postJournalEntries(account, existingTransactionIds, existingReversedTransactionIds);
547605

548-
throw new DepositAccountTransactionNotAllowedException(savingsId, "undo", DepositAccountType.FIXED_DEPOSIT);
606+
return new CommandProcessingResultBuilder().withEntityId(savingsId).withOfficeId(account.officeId())
607+
.withClientId(account.clientId()).withGroupId(account.groupId()).withSavingsId(savingsId).build();
549608
}
550609

551610
@Override

integration-tests/src/test/java/org/apache/fineract/integrationtests/FixedDepositTest.java

Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2952,6 +2952,125 @@ private Integer createTaxGroup(final String percentage, final Account liabilityA
29522952
/**
29532953
* Delete the Liability transfer account
29542954
*/
2955+
@Test
2956+
public void testFixedDepositAccountUndoTransaction() {
2957+
this.fixedDepositProductHelper = new FixedDepositProductHelper(this.requestSpec, this.responseSpec);
2958+
this.fixedDepositAccountHelper = new FixedDepositAccountHelper(this.requestSpec, this.responseSpec);
2959+
this.accountHelper = new AccountHelper(this.requestSpec, this.responseSpec);
2960+
this.journalEntryHelper = new JournalEntryHelper(this.requestSpec, this.responseSpec);
2961+
2962+
final Account assetAccount = this.accountHelper.createAssetAccount();
2963+
final Account liabilityAccount = this.accountHelper.createLiabilityAccount();
2964+
final Account incomeAccount = this.accountHelper.createIncomeAccount();
2965+
final Account expenseAccount = this.accountHelper.createExpenseAccount();
2966+
2967+
DateFormat dateFormat = new SimpleDateFormat("dd MMMM yyyy", Locale.US);
2968+
2969+
Calendar todaysDate = Calendar.getInstance();
2970+
todaysDate.add(Calendar.MONTH, -3);
2971+
final String VALID_FROM = dateFormat.format(todaysDate.getTime());
2972+
todaysDate.add(Calendar.YEAR, 10);
2973+
final String VALID_TO = dateFormat.format(todaysDate.getTime());
2974+
2975+
todaysDate = Calendar.getInstance();
2976+
todaysDate.add(Calendar.MONTH, -1);
2977+
final String SUBMITTED_ON_DATE = dateFormat.format(todaysDate.getTime());
2978+
final String APPROVED_ON_DATE = dateFormat.format(todaysDate.getTime());
2979+
final String ACTIVATION_DATE = dateFormat.format(todaysDate.getTime());
2980+
2981+
Integer clientId = ClientHelper.createClient(this.requestSpec, this.responseSpec);
2982+
Assertions.assertNotNull(clientId);
2983+
2984+
Integer fixedDepositProductId = createFixedDepositProduct(VALID_FROM, VALID_TO, CASH_BASED, assetAccount, liabilityAccount,
2985+
incomeAccount, expenseAccount);
2986+
Assertions.assertNotNull(fixedDepositProductId);
2987+
2988+
Integer fixedDepositAccountId = applyForFixedDepositApplication(clientId.toString(), fixedDepositProductId.toString(),
2989+
SUBMITTED_ON_DATE, WHOLE_TERM);
2990+
Assertions.assertNotNull(fixedDepositAccountId);
2991+
2992+
HashMap fixedDepositAccountStatusHashMap = FixedDepositAccountStatusChecker.getStatusOfFixedDepositAccount(this.requestSpec,
2993+
this.responseSpec, fixedDepositAccountId.toString());
2994+
FixedDepositAccountStatusChecker.verifyFixedDepositIsPending(fixedDepositAccountStatusHashMap);
2995+
2996+
fixedDepositAccountStatusHashMap = this.fixedDepositAccountHelper.approveFixedDeposit(fixedDepositAccountId, APPROVED_ON_DATE);
2997+
FixedDepositAccountStatusChecker.verifyFixedDepositIsApproved(fixedDepositAccountStatusHashMap);
2998+
2999+
fixedDepositAccountStatusHashMap = this.fixedDepositAccountHelper.activateFixedDeposit(fixedDepositAccountId, ACTIVATION_DATE);
3000+
FixedDepositAccountStatusChecker.verifyFixedDepositIsActive(fixedDepositAccountStatusHashMap);
3001+
3002+
this.fixedDepositAccountHelper.calculateInterestForFixedDeposit(fixedDepositAccountId);
3003+
3004+
Integer postInterestResult = this.fixedDepositAccountHelper.postInterestForFixedDeposit(fixedDepositAccountId);
3005+
Assertions.assertNotNull(postInterestResult);
3006+
3007+
// Compute INTEREST_POSTED_DATE as end of the activation month - Fineract posts interest
3008+
// at the last day of the posting period (MONTHLY) = end of the month containing ACTIVATION_DATE.
3009+
// All other FD tests use this same pattern (e.g. line 314).
3010+
todaysDate = Calendar.getInstance();
3011+
todaysDate.add(Calendar.MONTH, -1);
3012+
Integer currentDay = Integer.valueOf(new SimpleDateFormat("dd", Locale.US).format(todaysDate.getTime()));
3013+
Integer daysInMonth = todaysDate.getActualMaximum(Calendar.DATE);
3014+
Integer numberOfDaysLeft = daysInMonth - currentDay + 1;
3015+
todaysDate.add(Calendar.DATE, numberOfDaysLeft);
3016+
final String INTEREST_POSTED_DATE = dateFormat.format(todaysDate.getTime());
3017+
3018+
// Capture interest amount before undo for journal entry assertions
3019+
HashMap accountSummaryBeforeUndo = this.fixedDepositAccountHelper.getFixedDepositSummary(fixedDepositAccountId);
3020+
Float totalInterestPostedBeforeUndo = (Float) accountSummaryBeforeUndo.get("totalInterestPosted");
3021+
Assertions.assertNotNull(totalInterestPostedBeforeUndo);
3022+
Assertions.assertTrue(totalInterestPostedBeforeUndo > 0f, "Expected interest > 0 before undo");
3023+
3024+
// Verify journal entries exist after interest posting
3025+
this.journalEntryHelper.checkJournalEntryForAssetAccount(expenseAccount, INTEREST_POSTED_DATE,
3026+
new JournalEntry[] { new JournalEntry(totalInterestPostedBeforeUndo, JournalEntry.TransactionType.DEBIT) });
3027+
this.journalEntryHelper.checkJournalEntryForLiabilityAccount(liabilityAccount, INTEREST_POSTED_DATE,
3028+
new JournalEntry[] { new JournalEntry(totalInterestPostedBeforeUndo, JournalEntry.TransactionType.CREDIT) });
3029+
3030+
ArrayList transactions = this.fixedDepositAccountHelper.getFixedDepositTransactions(fixedDepositAccountId);
3031+
Assertions.assertNotNull(transactions);
3032+
3033+
Integer interestTransactionId = null;
3034+
for (Object txnObj : transactions) {
3035+
HashMap txn = (HashMap) txnObj;
3036+
HashMap txnType = (HashMap) txn.get("transactionType");
3037+
if (Boolean.TRUE.equals(txnType.get("interestPosting"))) {
3038+
interestTransactionId = (Integer) txn.get("id");
3039+
break;
3040+
}
3041+
}
3042+
Assertions.assertNotNull(interestTransactionId);
3043+
3044+
Integer undoResult = this.fixedDepositAccountHelper.undoFixedDepositTransaction(fixedDepositAccountId, interestTransactionId);
3045+
Assertions.assertNotNull(undoResult);
3046+
3047+
// 1. Verify transaction is marked reversed
3048+
ArrayList transactionsAfterUndo = this.fixedDepositAccountHelper.getFixedDepositTransactions(fixedDepositAccountId);
3049+
boolean foundReversed = false;
3050+
for (Object txnObj : transactionsAfterUndo) {
3051+
HashMap txn = (HashMap) txnObj;
3052+
if (interestTransactionId.equals(txn.get("id"))) {
3053+
Assertions.assertTrue(Boolean.TRUE.equals(txn.get("reversed")),
3054+
"Interest posting transaction must be marked reversed after undo");
3055+
foundReversed = true;
3056+
break;
3057+
}
3058+
}
3059+
Assertions.assertTrue(foundReversed, "Interest transaction must still be present after undo");
3060+
3061+
// 2. Verify balance returns to zero
3062+
HashMap accountSummaryAfterUndo = this.fixedDepositAccountHelper.getFixedDepositSummary(fixedDepositAccountId);
3063+
Float totalInterestPostedAfterUndo = (Float) accountSummaryAfterUndo.get("totalInterestPosted");
3064+
Assertions.assertEquals(0f, totalInterestPostedAfterUndo == null ? 0f : totalInterestPostedAfterUndo, 0.01f,
3065+
"totalInterestPosted must be zero after undo");
3066+
3067+
// 3. Verify reversal journal entries
3068+
this.journalEntryHelper.checkJournalEntryForAssetAccount(expenseAccount, INTEREST_POSTED_DATE,
3069+
new JournalEntry[] { new JournalEntry(totalInterestPostedBeforeUndo, JournalEntry.TransactionType.CREDIT) });
3070+
this.journalEntryHelper.checkJournalEntryForLiabilityAccount(liabilityAccount, INTEREST_POSTED_DATE,
3071+
new JournalEntry[] { new JournalEntry(totalInterestPostedBeforeUndo, JournalEntry.TransactionType.DEBIT) });
3072+
}
3073+
29553074
@AfterEach
29563075
public void tearDown() {
29573076
this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();

integration-tests/src/test/java/org/apache/fineract/integrationtests/common/fixeddeposit/FixedDepositAccountHelper.java

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@
2525
import java.util.Calendar;
2626
import java.util.HashMap;
2727
import java.util.List;
28+
import org.apache.fineract.client.util.Calls;
2829
import org.apache.fineract.integrationtests.common.CommonConstants;
30+
import org.apache.fineract.integrationtests.common.FineractClientHelper;
2931
import org.apache.fineract.integrationtests.common.Utils;
3032
import org.slf4j.Logger;
3133
import org.slf4j.LoggerFactory;
@@ -636,4 +638,29 @@ public FixedDepositAccountHelper withCharges(List<HashMap<String, String>> charg
636638
this.charges = charges;
637639
return this;
638640
}
641+
642+
// TODO: Rewrite to use fineract-client instead!
643+
// Example: org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper.disburseLoan(java.lang.Long,
644+
// org.apache.fineract.client.models.PostLoansLoanIdRequest)
645+
@Deprecated(forRemoval = true)
646+
public ArrayList getFixedDepositTransactions(final Integer fixedDepositAccountId) {
647+
LOG.info("-------------------- RETRIEVING FIXED DEPOSIT TRANSACTIONS ---------------------");
648+
final String url = FIXED_DEPOSIT_ACCOUNT_URL + "/" + fixedDepositAccountId + "?associations=transactions&"
649+
+ Utils.TENANT_IDENTIFIER;
650+
HashMap accountDetails = Utils.performServerGet(this.requestSpec, this.responseSpec, url, "");
651+
return (ArrayList) accountDetails.get("transactions");
652+
}
653+
654+
// Rewritten to use fineract-client (FixedDepositAccountTransactionsApi.adjustTransaction)
655+
// which maps to POST /v1/fixeddepositaccounts/{accountId}/transactions/{transactionId}?command=undo
656+
// Note: getFixedDepositTransactions stays as raw HTTP because GetFixedDepositAccountsAccountIdResponse
657+
// has no transactions field in the generated model - associations=transactions enrichment is not reflected there.
658+
@Deprecated(forRemoval = true)
659+
public Integer undoFixedDepositTransaction(final Integer fixedDepositAccountId, final Integer transactionId) {
660+
LOG.info("--------------------------------- UNDO FIXED DEPOSIT TRANSACTION --------------------------------");
661+
String response = Calls.ok(FineractClientHelper.getFineractClient().fixedDepositAccountTransactions
662+
.adjustTransaction(fixedDepositAccountId.longValue(), transactionId.longValue(), "undo", "{}"));
663+
return new com.google.gson.JsonParser().parse(response).getAsJsonObject().get("resourceId").getAsInt();
664+
}
665+
639666
}

0 commit comments

Comments
 (0)