This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 6d3357e74 FINERACT-1958 - reverse handling of disbursement with down
payment
6d3357e74 is described below
commit 6d3357e7400b4b56f89c89e38404029b0e415111
Author: Peter Bagrij <[email protected]>
AuthorDate: Sun Sep 10 15:42:13 2023 +0200
FINERACT-1958
- reverse handling of disbursement with down payment
---
.../portfolio/loanaccount/domain/Loan.java | 41 +-
.../loanaccount/domain/LoanTransaction.java | 2 +-
.../LoanWritePlatformServiceJpaRepositoryImpl.java | 10 -
.../integrationtests/BaseLoanIntegrationTest.java | 338 ++++
...oanDisbursalWithDownPaymentIntegrationTest.java | 1701 ++++++++++++++++++++
.../common/accounting/JournalEntryHelper.java | 9 +
.../common/loans/LoanProductHelper.java | 4 +
7 files changed, 2092 insertions(+), 13 deletions(-)
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
index 80b3286c7..f81970b8f 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java
@@ -3044,7 +3044,12 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom {
? Boolean.FALSE
: Boolean.TRUE;
this.loanRepaymentScheduleDetail.setPrincipal(this.approvedPrincipal);
- if (this.loanProduct.isMultiDisburseLoan()) {
+ // Remove All the Disbursement Details If the Loan Product is
disabled and exists one
+ if (this.loanProduct().isDisallowExpectedDisbursements() &&
!getDisbursementDetails().isEmpty()) {
+ for (LoanDisbursementDetails disbursementDetail :
getAllDisbursementDetails()) {
+ disbursementDetail.reverse();
+ }
+ } else {
for (final LoanDisbursementDetails details :
getDisbursementDetails()) {
details.updateActualDisbursementDate(null);
}
@@ -5535,6 +5540,16 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom {
}
+ public ChangedTransactionDetail
recalculateScheduleFromLastTransaction(final ScheduleGeneratorDTO generatorDTO)
{
+ if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
+ regenerateRepaymentScheduleWithInterestRecalculation(generatorDTO);
+ } else {
+ regenerateRepaymentSchedule(generatorDTO);
+ }
+ return processTransactions();
+
+ }
+
public ChangedTransactionDetail
handleRegenerateRepaymentScheduleWithInterestRecalculation(final
ScheduleGeneratorDTO generatorDTO) {
regenerateRepaymentScheduleWithInterestRecalculation(generatorDTO);
return processTransactions();
@@ -6505,7 +6520,7 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom {
}
}
reverseExistingTransactionsTillLastDisbursal(lastDisbursalTransaction);
- loan.recalculateScheduleFromLastTransaction(scheduleGeneratorDTO,
existingTransactionIds, existingReversedTransactionIds);
+ loan.recalculateScheduleFromLastTransaction(scheduleGeneratorDTO);
actualChanges.put("undolastdisbursal", "true");
actualChanges.put("disbursedAmount", this.getDisbursedAmount());
updateLoanSummaryDerivedFields();
@@ -6529,6 +6544,28 @@ public class Loan extends
AbstractAuditableWithUTCDateTimeCustom {
transaction.reverse();
}
}
+ if (isAutoRepaymentForDownPaymentEnabled()) {
+ // identify down-payment amount for the transaction
+ BigDecimal disbursedAmountPercentageForDownPayment =
this.loanRepaymentScheduleDetail
+ .getDisbursedAmountPercentageForDownPayment();
+ Money downPaymentMoney = Money.of(getCurrency(),
+
MathUtil.percentageOf(lastDisbursalTransaction.getAmount(),
disbursedAmountPercentageForDownPayment, 19));
+
+ // find the matching down-payment transaction based on date,
amount and it also must have downpayment
+ // installment linked
+ Optional<LoanTransaction> downPaymentTransaction =
this.loanTransactions.stream()
+ .filter(tr ->
tr.getTransactionDate().equals(lastDisbursalTransaction.getTransactionDate())
+ && hasAnInstallmentWithDownPayment(tr,
downPaymentMoney.getAmount()))
+ .max(Comparator.comparing(LoanTransaction::getId));
+
+ // reverse the down-payment transaction
+ downPaymentTransaction.ifPresent(LoanTransaction::reverse);
+ }
+ }
+
+ private boolean hasAnInstallmentWithDownPayment(LoanTransaction tr,
BigDecimal amount) {
+ return tr.getAmount().compareTo(amount) == 0 &&
tr.getLoanTransactionToRepaymentScheduleMappings().stream()
+ .anyMatch(mapping ->
mapping.getLoanRepaymentScheduleInstallment().isDownPayment());
}
private void updateLoanToLastDisbursalState(LoanDisbursementDetails
disbursementDetail) {
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
index c7b1afd7f..185ed2d30 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
@@ -910,7 +910,7 @@ public class LoanTransaction extends
AbstractAuditableWithUTCDateTimeCustom {
}
public Boolean isAllowTypeTransactionAtTheTimeOfLastUndo() {
- return isDisbursement() || isAccrual() || isRepaymentAtDisbursement();
+ return isDisbursement() || isAccrual() || isRepaymentAtDisbursement()
|| isRepayment();
}
public boolean isAccrualTransaction() {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
index 7c3c739f8..e72be1ab3 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java
@@ -869,16 +869,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl
implements LoanWritePlatf
final Map<String, Object> accountingBridgeData =
loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds,
existingReversedTransactionIds, isAccountTransfer);
journalEntryWritePlatformService.createJournalEntriesForLoan(accountingBridgeData);
-
- // Remove All the Disbursement Details If the Loan Product is
disabled and exists one
- if (loan.loanProduct().isDisallowExpectedDisbursements() &&
!loan.getDisbursementDetails().isEmpty()) {
- List<LoanDisbursementDetails> reversedDisbursementDetails =
new ArrayList<>();
- for (LoanDisbursementDetails disbursementDetail :
loan.getAllDisbursementDetails()) {
- disbursementDetail.reverse();
- reversedDisbursementDetails.add(disbursementDetail);
- }
-
this.loanDisbursementDetailsRepository.saveAllAndFlush(reversedDisbursementDetails);
- }
loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan,
existingTransactionIds);
businessEventNotifierService.notifyPostBusinessEvent(new
LoanUndoDisbursalBusinessEvent(loan));
}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
new file mode 100644
index 000000000..bc1b9ebca
--- /dev/null
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -0,0 +1,338 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static java.lang.Boolean.FALSE;
+import static java.lang.Boolean.TRUE;
+import static
org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
+import static
org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY;
+import static org.junit.jupiter.api.Assertions.assertNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Objects;
+import java.util.UUID;
+import lombok.AllArgsConstructor;
+import lombok.ToString;
+import org.apache.fineract.client.models.AllowAttributeOverrides;
+import org.apache.fineract.client.models.BusinessDateRequest;
+import
org.apache.fineract.client.models.GetJournalEntriesTransactionIdResponse;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.client.models.PostLoansLoanIdRequest;
+import org.apache.fineract.client.models.PostLoansLoanIdResponse;
+import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
+import org.apache.fineract.client.models.PostLoansRequest;
+import org.apache.fineract.client.models.PostLoansResponse;
+import org.apache.fineract.client.util.CallFailedRuntimeException;
+import org.apache.fineract.integrationtests.common.BusinessDateHelper;
+import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.accounting.Account;
+import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
+import
org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper;
+import org.apache.fineract.integrationtests.common.loans.LoanProductHelper;
+import
org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
+import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
+import org.junit.jupiter.api.Assertions;
+
+public abstract class BaseLoanIntegrationTest {
+
+ static {
+ Utils.initializeRESTAssured();
+ }
+
+ protected static final String DATETIME_PATTERN = "dd MMMM yyyy";
+
+ protected final ResponseSpecification requestSpec =
createResponseSpecification(200);
+ protected final RequestSpecification responseSpec =
createRequestSpecification();
+
+ protected final AccountHelper accountHelper = new
AccountHelper(responseSpec, requestSpec);
+ protected final LoanTransactionHelper loanTransactionHelper = new
LoanTransactionHelper(responseSpec, requestSpec);
+ protected final LoanProductHelper loanProductHelper = new
LoanProductHelper();
+ protected JournalEntryHelper journalEntryHelper = new
JournalEntryHelper(responseSpec, requestSpec);
+ protected BusinessDateHelper businessDateHelper = new BusinessDateHelper();
+
+ // asset
+ protected final Account loansReceivableAccount =
accountHelper.createAssetAccount();
+ protected final Account interestFeeReceivableAccount =
accountHelper.createAssetAccount();
+ protected final Account suspenseAccount =
accountHelper.createAssetAccount();
+ // liability
+ protected final Account suspenseClearingAccount =
accountHelper.createLiabilityAccount();
+ protected final Account overpaymentAccount =
accountHelper.createLiabilityAccount();
+ // income
+ protected final Account interestIncomeAccount =
accountHelper.createIncomeAccount();
+ protected final Account feeIncomeAccount =
accountHelper.createIncomeAccount();
+ protected final Account feeChargeOffAccount =
accountHelper.createIncomeAccount();
+ protected final Account recoveriesAccount =
accountHelper.createIncomeAccount();
+ protected final Account interestIncomeChargeOffAccount =
accountHelper.createIncomeAccount();
+ // expense
+ protected final Account creditLossBadDebtAccount =
accountHelper.createExpenseAccount();
+ protected final Account creditLossBadDebtFraudAccount =
accountHelper.createExpenseAccount();
+ protected final Account writtenOffAccount =
accountHelper.createExpenseAccount();
+ protected final Account goodwillExpenseAccount =
accountHelper.createExpenseAccount();
+
+ // Loan product with proper accounting setup
+ protected PostLoanProductsRequest
createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() {
+ return new
PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_",
6))//
+ .shortName(Utils.uniqueRandomStringGenerator("", 4))//
+ .description("Loan Product Description")//
+ .includeInBorrowerCycle(false)//
+ .currencyCode("USD")//
+ .digitsAfterDecimal(2)//
+ .inMultiplesOf(0)//
+ .installmentAmountInMultiplesOf(1)//
+ .useBorrowerCycle(false)//
+ .minPrincipal(100.0)//
+ .principal(1000.0)//
+ .maxPrincipal(10000.0)//
+ .minNumberOfRepayments(1)//
+ .numberOfRepayments(1)//
+ .maxNumberOfRepayments(30)//
+ .isLinkedToFloatingInterestRates(false)//
+ .minInterestRatePerPeriod((double) 0)//
+ .interestRatePerPeriod((double) 0)//
+ .maxInterestRatePerPeriod((double) 0)//
+ .interestRateFrequencyType(2)//
+ .repaymentEvery(30)//
+ .repaymentFrequencyType(0)//
+ .amortizationType(1)//
+ .interestType(0)//
+ .isEqualAmortization(false)//
+ .interestCalculationPeriodType(1)//
+ .transactionProcessingStrategyCode(
+
LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY)//
+ .daysInYearType(1)//
+ .daysInMonthType(1)//
+ .canDefineInstallmentAmount(true)//
+ .graceOnArrearsAgeing(3)//
+ .overdueDaysForNPA(179)//
+ .accountMovesOutOfNPAOnlyOnArrearsCompletion(false)//
+ .principalThresholdForLastInstallment(50)//
+ .allowVariableInstallments(false)//
+ .canUseForTopup(false)//
+ .isInterestRecalculationEnabled(false)//
+ .holdGuaranteeFunds(false)//
+ .multiDisburseLoan(true)//
+ .allowAttributeOverrides(new AllowAttributeOverrides()//
+ .amortizationType(true)//
+ .interestType(true)//
+ .transactionProcessingStrategyCode(true)//
+ .interestCalculationPeriodType(true)//
+ .inArrearsTolerance(true)//
+ .repaymentEvery(true)//
+ .graceOnPrincipalAndInterestPayment(true)//
+ .graceOnArrearsAgeing(true))//
+ .allowPartialPeriodInterestCalcualtion(true)//
+ .maxTrancheCount(10)//
+ .outstandingLoanBalance(10000.0)//
+ .charges(Collections.emptyList())//
+ .accountingRule(3)//
+
.fundSourceAccountId(suspenseClearingAccount.getAccountID().longValue())//
+
.loanPortfolioAccountId(loansReceivableAccount.getAccountID().longValue())//
+
.transfersInSuspenseAccountId(suspenseAccount.getAccountID().longValue())//
+
.interestOnLoanAccountId(interestIncomeAccount.getAccountID().longValue())//
+
.incomeFromFeeAccountId(feeIncomeAccount.getAccountID().longValue())//
+
.incomeFromPenaltyAccountId(feeIncomeAccount.getAccountID().longValue())//
+
.incomeFromRecoveryAccountId(recoveriesAccount.getAccountID().longValue())//
+
.writeOffAccountId(writtenOffAccount.getAccountID().longValue())//
+
.overpaymentLiabilityAccountId(overpaymentAccount.getAccountID().longValue())//
+
.receivableInterestAccountId(interestFeeReceivableAccount.getAccountID().longValue())//
+
.receivableFeeAccountId(interestFeeReceivableAccount.getAccountID().longValue())//
+
.receivablePenaltyAccountId(interestFeeReceivableAccount.getAccountID().longValue())//
+ .dateFormat(DATETIME_PATTERN)//
+ .locale("en_GB")//
+ .disallowExpectedDisbursements(true)//
+ .allowApprovedDisbursedAmountsOverApplied(true)//
+ .overAppliedCalculationType("percentage")//
+ .overAppliedNumber(50)//
+
.goodwillCreditAccountId(goodwillExpenseAccount.getAccountID().longValue())//
+
.incomeFromGoodwillCreditInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
+
.incomeFromGoodwillCreditFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
+
.incomeFromGoodwillCreditPenaltyAccountId(feeChargeOffAccount.getAccountID().longValue())//
+
.incomeFromChargeOffInterestAccountId(interestIncomeChargeOffAccount.getAccountID().longValue())//
+
.incomeFromChargeOffFeesAccountId(feeChargeOffAccount.getAccountID().longValue())//
+
.chargeOffExpenseAccountId(creditLossBadDebtAccount.getAccountID().longValue())//
+
.chargeOffFraudExpenseAccountId(creditLossBadDebtFraudAccount.getAccountID().longValue())//
+
.incomeFromChargeOffPenaltyAccountId(feeChargeOffAccount.getAccountID().longValue());
+ }
+
+ private static RequestSpecification createRequestSpecification() {
+ RequestSpecification request = new
RequestSpecBuilder().setContentType(ContentType.JSON).build();
+ request.header("Authorization", "Basic " +
Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+ request.header("Fineract-Platform-TenantId", "default");
+ return request;
+ }
+
+ private static ResponseSpecification createResponseSpecification(int
statusCode) {
+ return new ResponseSpecBuilder().expectStatusCode(statusCode).build();
+ }
+
+ protected void verifyUndoLastDisbursalShallFail(Long loanId, String
expectedError) {
+ ResponseSpecification errorResponse = new
ResponseSpecBuilder().expectStatusCode(403).build();
+ LoanTransactionHelper validationErrorHelper = new
LoanTransactionHelper(this.responseSpec, errorResponse);
+ CallFailedRuntimeException exception =
assertThrows(CallFailedRuntimeException.class, () -> {
+ validationErrorHelper.undoLastDisbursalLoan(loanId, new
PostLoansLoanIdRequest());
+ });
+ assertTrue(exception.getMessage().contains(expectedError));
+ }
+
+ protected void verifyNoTransactions(Long loanId) {
+ verifyTransactions(loanId);
+ }
+
+ protected void verifyTransactions(Long loanId, Transaction...
transactions) {
+ GetLoansLoanIdResponse loanDetails =
loanTransactionHelper.getLoan(responseSpec, requestSpec, loanId.intValue());
+ if (transactions == null || transactions.length == 0) {
+ assertNull(loanDetails.getTransactions(), "No transaction is
expected");
+ } else {
+ Assertions.assertEquals(transactions.length,
loanDetails.getTransactions().size());
+ DateTimeFormatter dateTimeFormatter =
DateTimeFormatter.ofPattern(DATETIME_PATTERN);
+ Arrays.stream(transactions).forEach(tr -> {
+ boolean found = loanDetails.getTransactions().stream()
+ .anyMatch(item -> Objects.equals(item.getAmount(),
tr.amount) && Objects.equals(item.getType().getValue(), tr.type)
+ && Objects.equals(item.getDate(),
LocalDate.parse(tr.date, dateTimeFormatter)));
+ Assertions.assertTrue(found, "Required transaction not found:
" + tr);
+ });
+ }
+ }
+
+ protected void disburseLoan(Long loanId, BigDecimal amount, String date) {
+ loanTransactionHelper.disburseLoan(loanId, new
PostLoansLoanIdRequest().actualDisbursementDate(date).dateFormat(DATETIME_PATTERN)
+ .transactionAmount(amount).locale("en"));
+ }
+
+ protected void verifyJournalEntries(Long loanId, JournalEntry... entries) {
+ GetJournalEntriesTransactionIdResponse journalEntriesForLoan =
journalEntryHelper.getJournalEntriesForLoan(loanId);
+ Assertions.assertEquals(entries.length,
journalEntriesForLoan.getPageItems().size());
+ Arrays.stream(entries).forEach(journalEntry -> {
+ boolean found = journalEntriesForLoan.getPageItems().stream()
+ .anyMatch(item -> Objects.equals(item.getAmount(),
journalEntry.amount)
+ && Objects.equals(item.getGlAccountId(),
journalEntry.account.getAccountID().longValue())
+ &&
Objects.requireNonNull(item.getEntryType()).getValue().equals(journalEntry.type));
+ Assertions.assertTrue(found, "Required journal entry not found: "
+ journalEntry);
+ });
+ }
+
+ protected void verifyRepaymentSchedule(Long loanId, Installment...
installments) {
+ GetLoansLoanIdResponse loanResponse =
loanTransactionHelper.getLoan(responseSpec, requestSpec, loanId.intValue());
+ DateTimeFormatter dateTimeFormatter =
DateTimeFormatter.ofPattern(DATETIME_PATTERN);
+
+ Assertions.assertNotNull(loanResponse.getRepaymentSchedule());
+
Assertions.assertNotNull(loanResponse.getRepaymentSchedule().getPeriods());
+ Assertions.assertEquals(installments.length,
loanResponse.getRepaymentSchedule().getPeriods().size(),
+ "Expected installments are not matching with the installments
configured on the loan");
+
+ for (int i = 1; i < installments.length; i++) {
+ if (installments[i].completed == null) { // this is for the
disbursement
+ Assertions.assertEquals(installments[i].amount,
+
loanResponse.getRepaymentSchedule().getPeriods().get(i).getPrincipalLoanBalanceOutstanding());
+ } else {
+ Assertions.assertEquals(installments[i].amount,
loanResponse.getRepaymentSchedule().getPeriods().get(i).getPrincipalDue());
+ }
+ Assertions.assertEquals(installments[i].completed,
loanResponse.getRepaymentSchedule().getPeriods().get(i).getComplete());
+ Assertions.assertEquals(LocalDate.parse(installments[i].dueDate,
dateTimeFormatter),
+
loanResponse.getRepaymentSchedule().getPeriods().get(i).getDueDate());
+ }
+ }
+
+ protected void runAt(String date, Runnable runnable) {
+ try {
+
GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(responseSpec,
requestSpec, 42, true);
+
GlobalConfigurationHelper.updateIsBusinessDateEnabled(responseSpec,
requestSpec, TRUE);
+ businessDateHelper.updateBusinessDate(
+ new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date(date).dateFormat(DATETIME_PATTERN).locale("en"));
+ runnable.run();
+ } finally {
+
GlobalConfigurationHelper.updateIsBusinessDateEnabled(responseSpec,
requestSpec, FALSE);
+
GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(responseSpec,
requestSpec, 42, false);
+ }
+ }
+
+ protected Long applyAndApproveLoan(Long clientId, Long loanProductId,
String loanDisbursementDate, Double amount) {
+ PostLoansResponse postLoansResponse =
loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(clientId)
+
.productId(loanProductId).expectedDisbursementDate(loanDisbursementDate).dateFormat(DATETIME_PATTERN)
+
.transactionProcessingStrategyCode(DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY)
+
.locale("en").submittedOnDate(loanDisbursementDate).amortizationType(1).interestRatePerPeriod(0)
+
.interestCalculationPeriodType(1).interestType(0).repaymentFrequencyType(0).repaymentEvery(30).repaymentFrequencyType(0)
+
.numberOfRepayments(1).loanTermFrequency(30).loanTermFrequencyType(0).maxOutstandingLoanBalance(BigDecimal.valueOf(amount))
+ .principal(BigDecimal.valueOf(amount)).loanType("individual"));
+
+ PostLoansLoanIdResponse approvedLoanResult =
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+ new
PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(amount)).dateFormat(DATETIME_PATTERN)
+ .approvedOnDate("01 January 2023").locale("en"));
+
+ return approvedLoanResult.getLoanId();
+ }
+
+ protected void addRepaymentForLoan(Long loanId, Double amount, String
date) {
+ String firstRepaymentUUID = UUID.randomUUID().toString();
+ loanTransactionHelper.makeLoanRepayment(loanId, new
PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN)
+
.transactionDate(date).locale("en").transactionAmount(amount).externalId(firstRepaymentUUID));
+ }
+
+ protected JournalEntry journalEntry(double amount, Account account, String
type) {
+ return new JournalEntry(amount, account, type);
+ }
+
+ protected Transaction transaction(double amount, String type, String date)
{
+ return new Transaction(amount, type, date);
+ }
+
+ protected Installment installment(double amount, Boolean completed, String
dueDate) {
+ return new Installment(amount, completed, dueDate);
+ }
+
+ @ToString
+ @AllArgsConstructor
+ public static class Transaction {
+
+ Double amount;
+ String type;
+ String date;
+ }
+
+ @ToString
+ @AllArgsConstructor
+ public static class JournalEntry {
+
+ Double amount;
+ Account account;
+ String type;
+ }
+
+ @ToString
+ @AllArgsConstructor
+ public static class Installment {
+
+ Double amount;
+ Boolean completed;
+ String dueDate;
+ }
+}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/UndoLoanDisbursalWithDownPaymentIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/UndoLoanDisbursalWithDownPaymentIntegrationTest.java
new file mode 100644
index 000000000..f19ee7526
--- /dev/null
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/UndoLoanDisbursalWithDownPaymentIntegrationTest.java
@@ -0,0 +1,1701 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.integrationtests;
+
+import static java.lang.Boolean.TRUE;
+import static
org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+
+import java.math.BigDecimal;
+import org.apache.fineract.client.models.BusinessDateRequest;
+import org.apache.fineract.client.models.GetLoanProductsProductIdResponse;
+import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import
org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(LoanTestLifecycleExtension.class)
+public class UndoLoanDisbursalWithDownPaymentIntegrationTest extends
BaseLoanIntegrationTest {
+
+ public static final BigDecimal DOWN_PAYMENT_PERCENTAGE = new
BigDecimal(25);
+ private final ClientHelper clientHelper = new
ClientHelper(this.responseSpec, this.requestSpec);
+
+ @Test
+ public void
testUndoDisbursalForLoanWithSingleDisbursalAutoDowpaymentEnabledAndNoManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(true,
false);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1000.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, false, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ // undoDisbursal
+ loanTransactionHelper.undoDisbursal(loanId.intValue());
+
+ // Verify that all transactions are reverted
+ verifyNoTransactions(loanId);
+
+ // verify journal entries are compensated after undo disbursal
+ verifyJournalEntries(loanId,
+ // original entries
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+ // original entries reverted
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "DEBIT")); //
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, false, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+ });
+ }
+
+ @Test
+ public void
testUndoDisbursalForLoanWithSingleDisbursalAutoDowPaymentEnabledAndHasManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(true,
false);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1000.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, false, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // Verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // make a repayment
+ addRepaymentForLoan(loanId, 100.0, "01 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023"), //
+ transaction(100.0, "Repayment", "01 January 2023") //
+ );
+
+ // undoDisbursal
+ loanTransactionHelper.undoDisbursal(loanId.intValue());
+
+ // Verify that all transactions are reverted
+ verifyNoTransactions(loanId);
+
+ // verify journal entries are compensated after undo disbursal
+ verifyJournalEntries(loanId, //
+ // original entries down-payment
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+
+ // repayment entries
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+
+ // original entries compensated
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "DEBIT"), //
+
+ // repayment entries compensated
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, false, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+ });
+ }
+
+ @Test
+ public void
testUndoDisbursalForLoanWithSingleDisbursalAutoDowPaymentDisabledAndNoManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(false,
false);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1000.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, false, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // Manual down-payment
+ addRepaymentForLoan(loanId, 250.0, "01 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Repayment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // undoDisbursal
+ loanTransactionHelper.undoDisbursal(loanId.intValue());
+
+ // Verify that all transactions are reverted
+ verifyNoTransactions(loanId);
+
+ // verify journal entries are compensated after undo disbursal
+ verifyJournalEntries(loanId, //
+ // original entries
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+
+ // original entries are compensated
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "DEBIT") //
+ );
+
+ // verify repayment entries are reverted
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, false, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+ });
+ }
+
+ @Test
+ public void
testUndoDisbursalForLoanWithSingleDisbursalAutoDowPaymentDisabledAndHasManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(false,
false);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1000.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, false, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // Manual down-payment
+ addRepaymentForLoan(loanId, 250.0, "01 January 2023");
+
+ // An extra Manual Repayment after the down-payment
+ addRepaymentForLoan(loanId, 100.0, "01 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(1000.0, "Disbursement", "01 January 2023"), //
+ transaction(250.0, "Repayment", "01 January 2023"), //
+ transaction(100.0, "Repayment", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ // undoDisbursal
+ loanTransactionHelper.undoDisbursal(loanId.intValue());
+
+ // Verify that all transactions are reverted
+ verifyNoTransactions(loanId);
+
+ // verify journal entries are compensated after undo disbursal
+ verifyJournalEntries(loanId, //
+ // original entries
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+
+ // original entries compensated
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "DEBIT"), //
+
+ // manual partial repayment of the first installment
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+
+ // manual partial repayment of the first installment
compensation after undoDisburse
+ journalEntry(100.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, false, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+ });
+ }
+
+ @Test
+ public void
testUndoLastDisbursalForLoanWithSingleDisbursalAutoDowPaymentEnabledAndNoManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(true,
false);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1000.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, false, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ verifyUndoLastDisbursalShallFail(loanId,
"error.msg.loan.product.does.not.support.multiple.disbursals.cannot.undo.last");
+
+ });
+ }
+
+ @Test
+ public void
testUndoLastDisbursalForLoanWithMultiDisbursalAutoDowPaymentEnabledAndNoManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(true,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1000.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(1000.0, false, "31 January 2023") //
+ );
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ verifyUndoLastDisbursalShallFail(loanId,
"error.msg.tranches.should.be.disbursed.more.than.one.to.undo.last.disbursal");
+ });
+ }
+
+ @Test
+ public void
testUndoDisbursalForLoanWithMultiDisbursalAutoDowPaymentEnabledAndNoManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(true,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1000.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(1000.0, false, "31 January 2023") //
+ );
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ // undoDisbursal
+ loanTransactionHelper.undoDisbursal(loanId.intValue());
+
+ // Verify that all transactions are reverted
+ verifyNoTransactions(loanId);
+
+ // verify journal entries are compensated after undo disbursal
+ verifyJournalEntries(loanId, //
+ // original entries
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+ // original entries reverted
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "DEBIT") //
+ );
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(1000.0, false, "31 January 2023") //
+ );
+ });
+ }
+
+ @Test
+ public void
testUndoDisbursalForLoanWithMultiDisbursalAutoDowPaymentEnabledAndHasManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(true,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1000.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(1000.0, false, "31 January 2023") //
+ );
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // Verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // make a repayment
+ addRepaymentForLoan(loanId, 100.0, "01 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023"), //
+ transaction(100.0, "Repayment", "01 January 2023") //
+ );
+
+ // undoDisbursal
+ loanTransactionHelper.undoDisbursal(loanId.intValue());
+
+ // Verify that all transactions are reverted
+ verifyNoTransactions(loanId);
+
+ // verify journal entries are compensated after undo disbursal
+ verifyJournalEntries(loanId,
+ // original entries down-payment
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+
+ // repayment entries
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+
+ // original entries compensated
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "DEBIT"), //
+
+ // repayment entries compensated
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(1000.0, false, "31 January 2023") //
+ );
+ });
+ }
+
+ @Test
+ public void
testUndoDisbursalForLoanWithMultiDisbursalAutoDowPaymentDisabledAndNoManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(false,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1000.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(1000.0, false, "31 January 2023") //
+ );
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // Manual down-payment
+ addRepaymentForLoan(loanId, 250.0, "01 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Repayment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023")//
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // undoDisbursal
+ loanTransactionHelper.undoDisbursal(loanId.intValue());
+
+ // Verify that all transactions are reverted
+ verifyNoTransactions(loanId);
+
+ // verify journal entries are compensated after undo disbursal
+ verifyJournalEntries(loanId,
+ // original entries
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+
+ // original entries are compensated
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "DEBIT") //
+ );
+
+ // verify repayment entries are reverted
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(1000.0, false, "31 January 2023") //
+ );
+ });
+ }
+
+ @Test
+ public void
testUndoDisbursalForLoanWithMultiDisbursalAutoDowPaymentDisabledAndHasManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(false,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1000.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, installment(1000.0, null, "01
January 2023"), installment(1000.0, false, "31 January 2023"));
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // Manual down-payment
+ addRepaymentForLoan(loanId, 250.0, "01 January 2023");
+
+ // An extra Manual Repayment after the down-payment
+ addRepaymentForLoan(loanId, 100.0, "01 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(1000.0, "Disbursement", "01 January 2023"), //
+ transaction(250.0, "Repayment", "01 January 2023"), //
+ transaction(100.0, "Repayment", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ // undoDisbursal
+ loanTransactionHelper.undoDisbursal(loanId.intValue());
+
+ // Verify that all transactions are reverted
+ verifyNoTransactions(loanId);
+
+ // verify journal entries are compensated after undo disbursal
+ verifyJournalEntries(loanId,
+ // original entries
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+
+ // original entries compensated
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "DEBIT"), //
+
+ // manual partial repayment of the first installment
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+
+ // manual partial repayment of the first installment
compensation after undoDisburse
+ journalEntry(100.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(1000.0, false, "31 January 2023") //
+ );
+ });
+ }
+
+ @Test
+ public void
testUndoLastDisbursalForLoanWithMultiDisbursalWith2DisburseAutoDowPaymentEnabledAndNoManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(true,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1500.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1500.0, null, "01 January 2023"), //
+ installment(1500.0, false, "31 January 2023") //
+ );
+
+ // 1st Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // verify transactions
+ verifyTransactions(loanId, transaction(250.0, "Down Payment", "01
January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, journalEntry(250.0,
loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, installment(1000.0, null, "01
January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("15 January 2023")
+ .dateFormat(DATETIME_PATTERN).locale("en"));
+
+ // 2nd Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(400.0), "15 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023"), //
+ transaction(100.0, "Down Payment", "15 January 2023"), //
+ transaction(400.0, "Disbursement", "15 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(400.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(400.0, null, "15 January 2023"), //
+ installment(100.0, true, "15 January 2023"), //
+ installment(1050.0, false, "31 January 2023") //
+ );
+
+ // undoLastDisbursal
+ loanTransactionHelper.undoLastDisbursal(loanId.intValue());
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId,
+ // first disbursement + down-payment
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+
+ // second disbursement + down-payment
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(400.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "CREDIT"), //
+
+ // compensation of second disbursement + down-payment
+ journalEntry(100.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(400.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "DEBIT") //
+ );
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+ });
+ }
+
+ @Test
+ public void
testUndoLastDisbursalForLoanWithMultiDisbursalWith2DisburseAutoDowPaymentDisabledAndNoManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(false,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1500.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1500.0, null, "01 January 2023"), //
+ installment(1500.0, false, "31 January 2023") //
+ );
+
+ // 1st Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // Manual down-payment
+ addRepaymentForLoan(loanId, 250.0, "01 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Repayment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("15 January 2023")
+ .dateFormat(DATETIME_PATTERN).locale("en"));
+
+ // 2nd Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(400.0), "15 January 2023");
+
+ // Manual down-payment
+ addRepaymentForLoan(loanId, 100.0, "15 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Repayment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023"), //
+ transaction(100.0, "Repayment", "15 January 2023"), //
+ transaction(400.0, "Disbursement", "15 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(400.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(400.0, null, "15 January 2023"), //
+ installment(100.0, true, "15 January 2023"), //
+ installment(1050.0, false, "31 January 2023") //
+ );
+
+ // undoLastDisbursal
+ loanTransactionHelper.undoLastDisbursal(loanId.intValue());
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Repayment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId,
+ // first disbursement + down-payment
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+
+ // second disbursement + down-payment
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(400.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "CREDIT"), //
+
+ // compensation of second disbursement + down-payment
+ journalEntry(100.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(400.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "DEBIT") //
+ );
+
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+ });
+ }
+
+ @Test
+ public void
testUndoLastDisbursalForLoanWithMultiDisbursalWith2DisburseAutoDowPaymentEnabledAndHasManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(true,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1500.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1500.0, null, "01 January 2023"), //
+ installment(1500.0, false, "31 January 2023") //
+ );
+
+ // 1st Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("15 January 2023")
+ .dateFormat(DATETIME_PATTERN).locale("en"));
+
+ // 2nd Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(400.0), "15 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023"), //
+ transaction(100.0, "Down Payment", "15 January 2023"), //
+ transaction(400.0, "Disbursement", "15 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(400.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(400.0, null, "15 January 2023"), //
+ installment(100.0, true, "15 January 2023"), //
+ installment(1050.0, false, "31 January 2023") //
+ );
+
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("20 January 2023")
+ .dateFormat(DATETIME_PATTERN).locale("en"));
+
+ // make an additional repayment after the 2nd disbursal
+ addRepaymentForLoan(loanId, 50.0, "20 January 2023");
+
+ // undo last disbursal shall fail
+ verifyUndoLastDisbursalShallFail(loanId,
"error.msg.cannot.undo.last.disbursal.after.repayments or waivers");
+ });
+ }
+
+ @Test
+ public void
testUndoLastDisbursalForLoanWithMultiDisbursalWith2DisburseAutoDowPaymentDisabledAndHasManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(false,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1500.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1500.0, null, "01 January 2023"), //
+ installment(1500.0, false, "31 January 2023") //
+ );
+
+ // 1st Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // Manual down-payment
+ addRepaymentForLoan(loanId, 250.0, "01 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Repayment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("15 January 2023")
+ .dateFormat(DATETIME_PATTERN).locale("en"));
+
+ // 2nd Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(400.0), "15 January 2023");
+
+ // Manual down-payment
+ addRepaymentForLoan(loanId, 100.0, "15 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Repayment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023"), //
+ transaction(100.0, "Repayment", "15 January 2023"), //
+ transaction(400.0, "Disbursement", "15 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(400.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(400.0, null, "15 January 2023"), //
+ installment(100.0, true, "15 January 2023"), //
+ installment(1050.0, false, "31 January 2023") //
+ );
+
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("20 January 2023")
+ .dateFormat(DATETIME_PATTERN).locale("en"));
+
+ // make an additional repayment after the 2nd disbursal
+ addRepaymentForLoan(loanId, 50.0, "20 January 2023");
+
+ // undo last disbursal shall fail
+ verifyUndoLastDisbursalShallFail(loanId,
"error.msg.cannot.undo.last.disbursal.after.repayments or waivers");
+ });
+ }
+
+ @Test
+ public void
testUndoDisbursalForLoanWithMultiDisbursalWith2DisburseAutoDowPaymentEnabledAndNoManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(true,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1500.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1500.0, null, "01 January 2023"), //
+ installment(1500.0, false, "31 January 2023") //
+ );
+
+ // 1st Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("15 January 2023")
+ .dateFormat(DATETIME_PATTERN).locale("en"));
+
+ // 2nd Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(400.0), "15 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023"), //
+ transaction(100.0, "Down Payment", "15 January 2023"), //
+ transaction(400.0, "Disbursement", "15 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(400.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(400.0, null, "15 January 2023"), //
+ installment(100.0, true, "15 January 2023"), //
+ installment(1050.0, false, "31 January 2023") //
+ );
+
+ // undoDisbursal
+ loanTransactionHelper.undoDisbursal(loanId.intValue());
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1500.0, null, "01 January 2023"), //
+ installment(1500.0, false, "31 January 2023") //
+ );
+
+ verifyNoTransactions(loanId);
+
+ // verify journal entries
+ verifyJournalEntries(loanId,
+ // 1st disbursal + down-payment
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+
+ // 2nd disbursal + down-payment
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(400.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "CREDIT"), //
+
+ // compensation of the 1st disbursal + down-payment
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "DEBIT"), //
+
+ // compensation of the 2nd disbursal + down-payment
+ journalEntry(100.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(400.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "DEBIT") //
+ );
+ });
+ }
+
+ @Test
+ public void
testUndoDisbursalForLoanWithMultiDisbursalWith2DisburseAutoDowPaymentDisabledAndNoManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(false,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1500.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1500.0, null, "01 January 2023"), //
+ installment(1500.0, false, "31 January 2023") //
+ );
+
+ // 1st Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // Manual down-payment
+ addRepaymentForLoan(loanId, 250.0, "01 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Repayment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("15 January 2023")
+ .dateFormat(DATETIME_PATTERN).locale("en"));
+
+ // 2nd Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(400.0), "15 January 2023");
+
+ // Manual down-payment
+ addRepaymentForLoan(loanId, 100.0, "15 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Repayment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023"), //
+ transaction(100.0, "Repayment", "15 January 2023"), //
+ transaction(400.0, "Disbursement", "15 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(400.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(400.0, null, "15 January 2023"), //
+ installment(100.0, true, "15 January 2023"), //
+ installment(1050.0, false, "31 January 2023") //
+ );
+
+ // undoDisbursal
+ loanTransactionHelper.undoDisbursal(loanId.intValue());
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1500.0, null, "01 January 2023"), //
+ installment(1500.0, false, "31 January 2023") //
+ );
+
+ verifyNoTransactions(loanId);
+
+ // verify journal entries
+ verifyJournalEntries(loanId,
+ // 1st disbursal + down-payment
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+
+ // 2nd disbursal + down-payment
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(400.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "CREDIT"), //
+
+ // compensation of the 1st disbursal + down-payment
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "DEBIT"), //
+
+ // compensation of the 2nd disbursal + down-payment
+ journalEntry(100.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(400.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "DEBIT") //
+ );
+ });
+ }
+
+ @Test
+ public void
testUndoDisbursalForLoanWithMultiDisbursalWith2DisburseAutoDowPaymentEnabledAndHasManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(true,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1500.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1500.0, null, "01 January 2023"), //
+ installment(1500.0, false, "31 January 2023") //
+ );
+
+ // 1st Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("15 January 2023")
+ .dateFormat(DATETIME_PATTERN).locale("en"));
+
+ // 2nd Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(400.0), "15 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Down Payment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023"), //
+ transaction(100.0, "Down Payment", "15 January 2023"), //
+ transaction(400.0, "Disbursement", "15 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(400.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(400.0, null, "15 January 2023"), //
+ installment(100.0, true, "15 January 2023"), //
+ installment(1050.0, false, "31 January 2023") //
+ );
+
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("20 January 2023")
+ .dateFormat(DATETIME_PATTERN).locale("en"));
+
+ // make an additional repayment after the 2nd disbursal
+ addRepaymentForLoan(loanId, 50.0, "20 January 2023");
+
+ // undoDisbursal
+ loanTransactionHelper.undoDisbursal(loanId.intValue());
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1500.0, null, "01 January 2023"), //
+ installment(1500.0, false, "31 January 2023") //
+ );
+
+ verifyNoTransactions(loanId);
+
+ // verify journal entries
+ verifyJournalEntries(loanId,
+ // 1st disbursal + down-payment
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+
+ // 2nd disbursal + down-payment
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(400.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "CREDIT"), //
+
+ // manual repayment
+ journalEntry(50.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(50.0, suspenseClearingAccount, "DEBIT"), //
+
+ // compensation of the 1st disbursal + down-payment
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "DEBIT"), //
+
+ // compensation of the 2nd disbursal + down-payment
+ journalEntry(100.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(400.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "DEBIT"), //
+
+ // compensation of repayment
+ journalEntry(50.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(50.0, suspenseClearingAccount, "CREDIT") //
+ );
+ });
+ }
+
+ @Test
+ public void
testUndoDisbursalForLoanWithMultiDisbursalWith2DisburseAutoDowPaymentDisabledAndHasManualTransactions()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+ // Create Loan Product
+ Long loanProductId = createLoanProductWith25PctDownPayment(false,
true);
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId, "01
January 2023", 1500.0);
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1500.0, null, "01 January 2023"), //
+ installment(1500.0, false, "31 January 2023") //
+ );
+
+ // 1st Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1000.00), "01 January
2023");
+
+ // Manual down-payment
+ addRepaymentForLoan(loanId, 250.0, "01 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Repayment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, //
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(750.0, false, "31 January 2023") //
+ );
+
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("15 January 2023")
+ .dateFormat(DATETIME_PATTERN).locale("en"));
+
+ // 2nd Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(400.0), "15 January 2023");
+
+ // Manual down-payment
+ addRepaymentForLoan(loanId, 100.0, "15 January 2023");
+
+ // verify transactions
+ verifyTransactions(loanId, //
+ transaction(250.0, "Repayment", "01 January 2023"), //
+ transaction(1000.0, "Disbursement", "01 January 2023"), //
+ transaction(100.0, "Repayment", "15 January 2023"), //
+ transaction(400.0, "Disbursement", "15 January 2023") //
+ );
+
+ // verify journal entries
+ verifyJournalEntries(loanId, journalEntry(250.0,
loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(400.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "CREDIT") //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1000.0, null, "01 January 2023"), //
+ installment(250.0, true, "01 January 2023"), //
+ installment(400.0, null, "15 January 2023"), //
+ installment(100.0, true, "15 January 2023"), //
+ installment(1050.0, false, "31 January 2023") //
+ );
+
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BUSINESS_DATE.getName()).date("20 January 2023")
+ .dateFormat(DATETIME_PATTERN).locale("en"));
+
+ // make an additional repayment after the 2nd disbursal
+ addRepaymentForLoan(loanId, 50.0, "20 January 2023");
+
+ // undoDisbursal
+ loanTransactionHelper.undoDisbursal(loanId.intValue());
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(1500.0, null, "01 January 2023"), //
+ installment(1500.0, false, "31 January 2023") //
+ );
+
+ verifyNoTransactions(loanId);
+
+ // verify journal entries
+ verifyJournalEntries(loanId,
+ // 1st disbursal + down-payment
+ journalEntry(250.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), //
+
+ // 2nd disbursal + down-payment
+ journalEntry(100.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "DEBIT"), //
+ journalEntry(400.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "CREDIT"), //
+
+ // manual repayment
+ journalEntry(50.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(50.0, suspenseClearingAccount, "DEBIT"), //
+
+ // compensation of the 1st disbursal + down-payment
+ journalEntry(250.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(250.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(1000.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(1000.0, suspenseClearingAccount, "DEBIT"), //
+
+ // compensation of the 2nd disbursal + down-payment
+ journalEntry(100.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(100.0, suspenseClearingAccount, "CREDIT"), //
+ journalEntry(400.0, loansReceivableAccount, "CREDIT"), //
+ journalEntry(400.0, suspenseClearingAccount, "DEBIT"), //
+
+ // compensation of repayment
+ journalEntry(50.0, loansReceivableAccount, "DEBIT"), //
+ journalEntry(50.0, suspenseClearingAccount, "CREDIT") //
+ );
+ });
+ }
+
+ private Long createLoanProductWith25PctDownPayment(boolean
autoDownPaymentEnabled, boolean multiDisburseEnabled) {
+ PostLoanProductsRequest product =
createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct();
+ product.setMultiDisburseLoan(multiDisburseEnabled);
+
+ if (!multiDisburseEnabled) {
+ product.disallowExpectedDisbursements(null);
+ product.setAllowApprovedDisbursedAmountsOverApplied(null);
+ product.overAppliedCalculationType(null);
+ product.overAppliedNumber(null);
+ }
+
+ product.setEnableDownPayment(true);
+
product.setDisbursedAmountPercentageForDownPayment(DOWN_PAYMENT_PERCENTAGE);
+ product.setEnableAutoRepaymentForDownPayment(autoDownPaymentEnabled);
+
+ PostLoanProductsResponse loanProductResponse =
loanProductHelper.createLoanProduct(product);
+ GetLoanProductsProductIdResponse getLoanProductsProductIdResponse =
loanProductHelper
+ .retrieveLoanProductById(loanProductResponse.getResourceId());
+
+ Long loanProductId = loanProductResponse.getResourceId();
+
+ assertEquals(TRUE,
getLoanProductsProductIdResponse.getEnableDownPayment());
+
assertNotNull(getLoanProductsProductIdResponse.getDisbursedAmountPercentageForDownPayment());
+ assertEquals(0,
getLoanProductsProductIdResponse.getDisbursedAmountPercentageForDownPayment().compareTo(DOWN_PAYMENT_PERCENTAGE));
+ assertEquals(autoDownPaymentEnabled,
getLoanProductsProductIdResponse.getEnableAutoRepaymentForDownPayment());
+ assertEquals(multiDisburseEnabled,
getLoanProductsProductIdResponse.getMultiDisburseLoan());
+ return loanProductId;
+ }
+
+}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/JournalEntryHelper.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/JournalEntryHelper.java
index 155586294..d1b771abd 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/JournalEntryHelper.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/accounting/JournalEntryHelper.java
@@ -118,4 +118,13 @@ public class JournalEntryHelper {
return GSON.fromJson(response,
GetJournalEntriesTransactionIdResponse.class);
}
+ public GetJournalEntriesTransactionIdResponse
getJournalEntriesForLoan(final Long loanId) {
+ log.info("Getting GL Journal entries for loan id {}", loanId);
+ final String url = "/fineract-provider/api/v1/journalentries?loanId="
+ loanId + "&tenantIdentifier=default"
+ + "&orderBy=id&sortOrder=desc&locale=en&dateFormat=dd MMMM
yyyy";
+ final String response = Utils.performServerGet(this.requestSpec,
this.responseSpec, url, null);
+ log.info("response {}", response);
+ return GSON.fromJson(response,
GetJournalEntriesTransactionIdResponse.class);
+ }
+
}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductHelper.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductHelper.java
index d5873c82e..b899f20d1 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductHelper.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanProductHelper.java
@@ -38,6 +38,10 @@ public class LoanProductHelper extends IntegrationTest {
return
ok(fineract().loanProducts.retrieveLoanProductDetails1(externalId));
}
+ public GetLoanProductsProductIdResponse retrieveLoanProductById(Long
loanProductId) {
+ return
ok(fineract().loanProducts.retrieveLoanProductDetails(loanProductId));
+ }
+
public PutLoanProductsProductIdResponse
updateLoanProductByExternalId(String externalId,
PutLoanProductsProductIdRequest request) {
return ok(fineract().loanProducts.updateLoanProduct1(externalId,
request));
}