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 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 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 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)); }