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 f069466f6..deeb280ce 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 @@ -2967,8 +2967,24 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { Money downPaymentMoney = Money.of(getCurrency(), MathUtil.percentageOf(disbursementTransaction.getAmount(), disbursedAmountPercentageForDownPayment, 19)); - Money adjustedDownPaymentMoney = MathUtil - .negativeToZero(downPaymentMoney.minus(disbursementTransaction.getOverPaymentPortion(getCurrency()))); + Money adjustedDownPaymentMoney = switch (getLoanProductRelatedDetail().getLoanScheduleType()) { + // For Cumulative loan: To check whether the loan was overpaid when the disbursement happened and to get the + // proper amount after the disbursement we are using two balances: + // 1. Whether the loan is still overpaid after the disbursement, + // 2. if the loan is not overpaid anymore after the disbursement, but was it more overpaid than the + // calculated down-payment amount? + case CUMULATIVE -> { + if (getTotalOverpaidAsMoney().isGreaterThanZero()) { + yield Money.zero(getCurrency()); + } + yield MathUtil.negativeToZero(downPaymentMoney.minus(MathUtil.negativeToZero(disbursementTransaction + .getAmount(getCurrency()).minus(disbursementTransaction.getOutstandingLoanBalanceMoney(getCurrency()))))); + } + // For Progressive loan: Disbursement transaction portion balances are enough to see whether the overpayment + // amount was more than the calculated down-payment amount + case PROGRESSIVE -> + MathUtil.negativeToZero(downPaymentMoney.minus(disbursementTransaction.getOverPaymentPortion(getCurrency()))); + }; if (adjustedDownPaymentMoney.isGreaterThanZero()) { LoanTransaction downPaymentTransaction = LoanTransaction.downPayment(getOffice(), adjustedDownPaymentMoney, null, disbursedOn, externalId); @@ -3602,7 +3618,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final LocalDate currentTransactionDate = loanTransaction.getTransactionDate(); for (final LoanTransaction previousTransaction : loanTransactions) { if (!previousTransaction.isDisbursement() && previousTransaction.isNotReversed() - && !DateUtils.isAfter(currentTransactionDate, previousTransaction.getTransactionDate())) { + && (DateUtils.isBefore(currentTransactionDate, previousTransaction.getTransactionDate()) + || (DateUtils.isEqual(currentTransactionDate, previousTransaction.getTransactionDate()) + && ((loanTransaction.getId() == null && previousTransaction.getId() == null) + || (loanTransaction.getId() != null && (previousTransaction.getId() == null + || loanTransaction.getId().compareTo(previousTransaction.getId()) < 0)))))) { isChronologicallyLatestRepaymentOrWaiver = false; break; } @@ -5958,7 +5978,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { if (loanTransaction.isDisbursement() || loanTransaction.isIncomePosting()) { outstanding = outstanding.plus(loanTransaction.getAmount(getCurrency())) .minus(loanTransaction.getOverPaymentPortion(getCurrency())); - loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount()); + loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount())); } else if (loanTransaction.isChargeback() || loanTransaction.isCreditBalanceRefund()) { Money transactionOutstanding = loanTransaction.getPrincipalPortion(getCurrency()); if (!loanTransaction.getOverPaymentPortion(getCurrency()).isZero()) { @@ -5977,8 +5997,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } } outstanding = outstanding.plus(transactionOutstanding); - loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount()); - + loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount())); } else { if (this.loanInterestRecalculationDetails != null && this.loanInterestRecalculationDetails.isCompoundingToBePostedAsTransaction() @@ -5987,7 +6006,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { } else { outstanding = outstanding.minus(loanTransaction.getPrincipalPortion(getCurrency())); } - loanTransaction.updateOutstandingLoanBalance(outstanding.getAmount()); + loanTransaction.updateOutstandingLoanBalance(MathUtil.negativeToZero(outstanding.getAmount())); } } } 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 f14835273..960ef6138 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 @@ -46,6 +46,7 @@ import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.portfolio.account.data.AccountTransferData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionEnumData; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; import org.apache.fineract.portfolio.paymentdetail.data.PaymentDetailData; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; @@ -147,14 +148,16 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { penaltyChargesPortion, overPaymentPortion, reversed, paymentDetail, externalId); } - public static LoanTransaction disbursement(final Office office, final Money amount, final PaymentDetail paymentDetail, + public static LoanTransaction disbursement(final Loan loan, final Money amount, final PaymentDetail paymentDetail, final LocalDate disbursementDate, final ExternalId externalId, final Money loanTotalOverpaid) { // We need to set the overpayment amount because it could happen the transaction got saved before the proper // portion calculation and side effect would be reverse-replay - Money overPaymentPortion = amount.isGreaterThan(loanTotalOverpaid) ? loanTotalOverpaid : amount; - LoanTransaction disbursement = new LoanTransaction(null, office, LoanTransactionType.DISBURSEMENT, paymentDetail, + LoanTransaction disbursement = new LoanTransaction(null, loan.getOffice(), LoanTransactionType.DISBURSEMENT, paymentDetail, amount.getAmount(), disbursementDate, externalId); - disbursement.setOverPayments(overPaymentPortion); + if (LoanScheduleType.PROGRESSIVE.equals(loan.getLoanProductRelatedDetail().getLoanScheduleType())) { + Money overPaymentPortion = amount.isGreaterThan(loanTotalOverpaid) ? loanTotalOverpaid : amount; + disbursement.setOverPayments(overPaymentPortion); + } return disbursement; } @@ -911,6 +914,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { return outstandingLoanBalance; } + public Money getOutstandingLoanBalanceMoney(final MonetaryCurrency currency) { + return Money.of(currency, this.outstandingLoanBalance); + } + public PaymentDetail getPaymentDetail() { return this.paymentDetail; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index 8039a6479..d6c542e28 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -534,8 +534,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { final List existingTransactionIds = new ArrayList<>(); final List existingReversedTransactionIds = new ArrayList<>(); final Money amount = Money.of(loan.getCurrency(), transactionAmount); - LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), amount, paymentDetail, transactionDate, - txnExternalId, loan.getTotalOverpaidAsMoney()); + LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan, amount, paymentDetail, transactionDate, txnExternalId, + loan.getTotalOverpaidAsMoney()); // Subtract Previous loan outstanding balance from netDisbursalAmount loan.deductFromNetDisbursalAmount(transactionAmount); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 6d6643f9d..9b0be1385 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -1715,7 +1715,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa public Collection retrieveLoanDisbursementDetails(final Long loanId) { final LoanDisbursementDetailMapper rm = new LoanDisbursementDetailMapper(sqlGenerator); final String sql = "select " + rm.schema() - + " where dd.loan_id=? and dd.is_reversed=false group by dd.id, lc.amount_waived_derived order by dd.expected_disburse_date,dd.disbursedon_date"; + + " where dd.loan_id=? and dd.is_reversed=false group by dd.id, lc.amount_waived_derived order by dd.expected_disburse_date,dd.disbursedon_date,dd.id"; return this.jdbcTemplate.query(sql, rm, loanId); // NOSONAR } 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 479370353..42d4ff675 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 @@ -435,8 +435,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } else { existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); - disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), amountToDisburse, paymentDetail, - actualDisbursementDate, txnExternalId, loan.getTotalOverpaidAsMoney()); + disbursementTransaction = LoanTransaction.disbursement(loan, amountToDisburse, paymentDetail, actualDisbursementDate, + txnExternalId, loan.getTotalOverpaidAsMoney()); disbursementTransaction.updateLoan(loan); loan.addLoanTransaction(disbursementTransaction); } @@ -461,6 +461,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (disbursementTransaction != null) { loanTransactionRepository.saveAndFlush(disbursementTransaction); } + if (changedTransactionDetail != null) { + for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); + accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); + } + // Trigger transaction replayed event + replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); + } if (loan.isAutoRepaymentForDownPaymentEnabled()) { // updating linked savings account for auto down payment transaction for disbursement to savings account if (isAccountTransfer && loan.shouldCreateStandingInstructionAtDisbursement()) { @@ -490,14 +498,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } } if (!changes.isEmpty()) { - if (changedTransactionDetail != null) { - for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { - loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); - accountTransfersWritePlatformService.updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); - } - // Trigger transaction replayed event - replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); - } + loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); final String noteText = command.stringValueOfParameterNamed("note"); @@ -563,8 +564,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf businessEventNotifierService.notifyPostBusinessEvent(new LoanDisbursalTransactionBusinessEvent(disbursalTransaction)); } - loan.updateLoanSummaryAndStatus(); - return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // .withEntityId(loan.getId()) // @@ -751,7 +750,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } else { existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); - LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan.getOffice(), disburseAmount, paymentDetail, + LoanTransaction disbursementTransaction = LoanTransaction.disbursement(loan, disburseAmount, paymentDetail, actualDisbursementDate, txnExternalId, loan.getTotalOverpaidAsMoney()); disbursementTransaction.updateLoan(loan); loan.addLoanTransaction(disbursementTransaction); 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 index 0d74fc161..fc4aaf202 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -159,7 +159,6 @@ public abstract class BaseLoanIntegrationTest { .includeInBorrowerCycle(false)// .currencyCode("USD")// .digitsAfterDecimal(2)// - .inMultiplesOf(0)// .installmentAmountInMultiplesOf(1)// .useBorrowerCycle(false)// .minPrincipal(100.0)// @@ -643,7 +642,14 @@ public abstract class BaseLoanIntegrationTest { protected TransactionExt transaction(double amount, String type, String date, double outstandingAmount, double principalPortion, double interestPortion, double feePortion, double penaltyPortion, double unrecognizedIncomePortion, double overpaymentPortion) { return new TransactionExt(amount, type, date, outstandingAmount, principalPortion, interestPortion, feePortion, penaltyPortion, - unrecognizedIncomePortion, overpaymentPortion); + unrecognizedIncomePortion, overpaymentPortion, false); + } + + protected TransactionExt transaction(double amount, String type, String date, double outstandingAmount, double principalPortion, + double interestPortion, double feePortion, double penaltyPortion, double unrecognizedIncomePortion, double overpaymentPortion, + boolean reversed) { + return new TransactionExt(amount, type, date, outstandingAmount, principalPortion, interestPortion, feePortion, penaltyPortion, + unrecognizedIncomePortion, overpaymentPortion, reversed); } protected Installment installment(double principalAmount, Boolean completed, String dueDate) { @@ -766,6 +772,7 @@ public abstract class BaseLoanIntegrationTest { Double penaltyPortion; Double unrecognizedPortion; Double overpaymentPortion; + Boolean reversed; } @ToString diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java index 2db85269b..d9cf82d3c 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRepaymentScheduleWithDownPaymentTest.java @@ -18,37 +18,33 @@ */ package org.apache.fineract.integrationtests; -import static org.apache.fineract.integrationtests.BaseLoanIntegrationTest.DATETIME_PATTERN; import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; 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.util.ArrayList; -import java.util.Arrays; import java.util.HashMap; import java.util.List; import java.util.UUID; -import java.util.concurrent.atomic.AtomicInteger; -import org.apache.fineract.client.models.AdvancedPaymentData; import org.apache.fineract.client.models.GetDelinquencyBucketsResponse; import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdSummary; -import org.apache.fineract.client.models.PaymentAllocationOrder; import org.apache.fineract.client.models.PostChargesResponse; +import org.apache.fineract.client.models.PostClientsResponse; +import org.apache.fineract.client.models.PostLoanProductsRequest; +import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansLoanIdChargesResponse; +import org.apache.fineract.client.models.PostLoansLoanIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; +import org.apache.fineract.client.models.PostLoansRequest; +import org.apache.fineract.client.models.PostLoansResponse; import org.apache.fineract.client.models.PutLoanProductsProductIdRequest; import org.apache.fineract.client.models.PutLoanProductsProductIdResponse; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; @@ -59,44 +55,19 @@ import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper; import org.apache.fineract.integrationtests.common.SchedulerJobHelper; 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.JournalEntry; -import org.apache.fineract.integrationtests.common.accounting.JournalEntryHelper; import org.apache.fineract.integrationtests.common.charges.ChargesHelper; import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; -import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; import org.apache.fineract.integrationtests.common.products.DelinquencyBucketsHelper; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; -import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; -import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.ExtendWith; -@ExtendWith(LoanTestLifecycleExtension.class) -public class LoanRepaymentScheduleWithDownPaymentTest { - - private ResponseSpecification responseSpec; - private RequestSpecification requestSpec; - private LoanTransactionHelper loanTransactionHelper; - private ClientHelper clientHelper; - private AccountHelper accountHelper; - private JournalEntryHelper journalEntryHelper; - - @BeforeEach - public void setup() { - Utils.initializeRESTAssured(); - requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); - requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); - responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); - loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec); - clientHelper = new ClientHelper(requestSpec, responseSpec); - accountHelper = new AccountHelper(requestSpec, responseSpec); - journalEntryHelper = new JournalEntryHelper(requestSpec, responseSpec); - } +public class LoanRepaymentScheduleWithDownPaymentTest extends BaseLoanIntegrationTest { @Test public void loanRepaymentScheduleWithSimpleDisbursementAndDownPayment() { @@ -1235,140 +1206,176 @@ public class LoanRepaymentScheduleWithDownPaymentTest { } @Test - public void downPaymentOnOverpaidLoan() { - try { - - // Set business date + public void downPaymentOnOverpaidProgressiveLoan() { + runAt("03 March 2023", () -> { LocalDate disbursementDate = LocalDate.of(2023, 3, 3); - GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); - BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, disbursementDate); + PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); - // Accounts oof periodic accrual - final Account assetAccount = accountHelper.createAssetAccount(); - final Account incomeAccount = accountHelper.createIncomeAccount(); - final Account expenseAccount = accountHelper.createExpenseAccount(); - final Account overpaymentAccount = accountHelper.createLiabilityAccount(); + final PostLoanProductsRequest loanProductsRequest = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .installmentAmountInMultiplesOf(null).enableDownPayment(true).enableAutoRepaymentForDownPayment(true) + .disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)); - // Loan ExternalId - String loanExternalIdStr = UUID.randomUUID().toString(); + PostLoanProductsResponse loanProductsResponse = loanTransactionHelper.createLoanProduct(loanProductsRequest); - // down-payment configuration - Boolean enableDownPayment = true; - BigDecimal disbursedAmountPercentageForDownPayment = BigDecimal.valueOf(25); - Boolean enableAutoRepaymentForDownPayment = true; + String disbursementDateStr = DateUtils.format(disbursementDate, DATETIME_PATTERN); + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(client.getResourceId()) + .productId(loanProductsResponse.getResourceId()).loanType("individual").locale("en").dateFormat(DATETIME_PATTERN) + .amortizationType(1).interestRatePerPeriod(BigDecimal.ZERO).interestCalculationPeriodType(1).interestType(0) + .maxOutstandingLoanBalance(BigDecimal.valueOf(35000)) + .transactionProcessingStrategyCode(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) + .loanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL.name()).expectedDisbursementDate(disbursementDateStr) + .dateFormat(DATETIME_PATTERN).submittedOnDate(disbursementDateStr).repaymentFrequencyType(0).repaymentEvery(30) + .numberOfRepayments(1).loanTermFrequency(30).loanTermFrequencyType(0).principal(BigDecimal.valueOf(1000)) + .loanType("individual").maxOutstandingLoanBalance(BigDecimal.valueOf(35000))); - final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + loanTransactionHelper.approveLoan(loanResponse.getResourceId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN) + .approvedOnDate(disbursementDateStr).locale("en")); - // Loan Product creation with down-payment configuration - final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createProgressiveLoanProductWithDownPaymentConfigurationAndAccrualAccounting( - loanTransactionHelper, enableDownPayment, "25", enableAutoRepaymentForDownPayment, assetAccount, incomeAccount, - expenseAccount, overpaymentAccount); + loanTransactionHelper.disburseLoan(loanResponse.getResourceId(), + new PostLoansLoanIdRequest().actualDisbursementDate(disbursementDateStr).dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(1000)).locale("en")); - assertNotNull(getLoanProductsProductResponse); - assertEquals(enableDownPayment, getLoanProductsProductResponse.getEnableDownPayment()); - assertEquals(0, getLoanProductsProductResponse.getDisbursedAmountPercentageForDownPayment() - .compareTo(disbursedAmountPercentageForDownPayment)); - assertEquals(enableAutoRepaymentForDownPayment, getLoanProductsProductResponse.getEnableAutoRepaymentForDownPayment()); + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getResourceId()); - final Integer loanId = createLoanAccountWithAdvancedPaymentAllocation(clientId, getLoanProductsProductResponse.getId(), - loanExternalIdStr); + assertTrue(loanDetails.getStatus().getActive()); - // Retrieve Loan with loanId + // Verify Repayment Schedule + verifyRepaymentSchedule(loanResponse.getResourceId(), // + installment(1000.0, null, "03 March 2023"), // + installment(250.0, 0.0, 0.0, true, "03 March 2023"), // + installment(750.0, 0.0, 750.0, false, "02 April 2023") // + ); + // verify transactions + verifyTransactions(loanResponse.getResourceId(), // + transaction(1000.0, "Disbursement", "03 March 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Down Payment", "03 March 2023", 750.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); + verifyJournalEntries(loanResponse.getResourceId(), journalEntry(1000.0, loansReceivableAccount, "DEBIT"), // + journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), // + journalEntry(250.0, loansReceivableAccount, "CREDIT"), // + journalEntry(250.0, suspenseClearingAccount, "DEBIT") // + ); - GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + loanTransactionHelper.makeLoanRepayment(loanResponse.getResourceId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat("dd MMMM yyyy").transactionDate("03 March 2023").locale("en").transactionAmount(800.0)); - // verify down-payment details for Loan - assertNotNull(loanDetails); - assertEquals(enableDownPayment, loanDetails.getEnableDownPayment()); - assertEquals(0, loanDetails.getDisbursedAmountPercentageForDownPayment().compareTo(disbursedAmountPercentageForDownPayment)); - assertEquals(enableAutoRepaymentForDownPayment, loanDetails.getEnableAutoRepaymentForDownPayment()); - - // first disbursement - loanTransactionHelper.disburseLoanWithTransactionAmount("03 March 2023", loanId, "1000"); - - loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); - // verify down-payment transaction created - checkDownPaymentTransaction(disbursementDate, 250.0f, 0.0f, 0.0f, 0.0f, loanId); - - // verify journal entries for down-payment - journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "03 March 2023", - new JournalEntry(250, JournalEntry.TransactionType.CREDIT)); - journalEntryHelper.checkJournalEntryForAssetAccount(assetAccount, "03 March 2023", - new JournalEntry(250, JournalEntry.TransactionType.DEBIT)); - - // verify installment details - assertEquals(LocalDate.of(2023, 3, 3), loanDetails.getRepaymentSchedule().getPeriods().get(0).getDueDate()); - assertEquals(1000.0, loanDetails.getRepaymentSchedule().getPeriods().get(0).getPrincipalLoanBalanceOutstanding()); - assertEquals(1, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPeriod()); - assertEquals(LocalDate.of(2023, 3, 3), loanDetails.getRepaymentSchedule().getPeriods().get(1).getDueDate()); - assertEquals(250.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getTotalInstallmentAmountForPeriod()); - assertEquals(true, loanDetails.getRepaymentSchedule().getPeriods().get(1).getDownPaymentPeriod()); - assertEquals(2, loanDetails.getRepaymentSchedule().getPeriods().get(2).getPeriod()); - assertEquals(LocalDate.of(2023, 4, 2), loanDetails.getRepaymentSchedule().getPeriods().get(2).getDueDate()); - assertEquals(750.0, loanDetails.getRepaymentSchedule().getPeriods().get(2).getTotalInstallmentAmountForPeriod()); - assertEquals(false, loanDetails.getRepaymentSchedule().getPeriods().get(2).getDownPaymentPeriod()); - - loanTransactionHelper.makeLoanRepayment((long) loanId, new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy") - .transactionDate("03 March 2023").locale("en").transactionAmount(800.0)); - - loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); - // verify down-payment details for Loan + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getResourceId()); + // Verify Repayment Schedule + verifyRepaymentSchedule(loanResponse.getResourceId(), // + installment(1000.0, null, "03 March 2023"), // + installment(250.0, 0.0, 0.0, true, "03 March 2023"), // + installment(750.0, 0.0, 0.0, true, "02 April 2023") // + ); + // verify transactions + verifyTransactions(loanResponse.getResourceId(), // + transaction(1000.0, "Disbursement", "03 March 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Down Payment", "03 March 2023", 750.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(800.0, "Repayment", "03 March 2023", 0.0, 750.0, 0.0, 0.0, 0.0, 0.0, 50.0) // + ); assertTrue(loanDetails.getStatus().getOverpaid()); assertEquals(50.0, loanDetails.getTotalOverpaid()); // second disbursement - disbursementDate = LocalDate.of(2023, 3, 5); BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, disbursementDate); - loanTransactionHelper.disburseLoanWithTransactionAmount("05 March 2023", loanId, "20"); - loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); - - assertTrue(loanDetails.getTransactions().get(0).getType().getDisbursement()); - assertEquals(1000.0, loanDetails.getTransactions().get(0).getAmount()); - assertEquals("loanTransactionType.downPayment", loanDetails.getTransactions().get(1).getType().getCode()); - assertEquals(250.0, loanDetails.getTransactions().get(1).getAmount()); - assertTrue(loanDetails.getTransactions().get(2).getType().getRepayment()); - assertEquals(800.0, loanDetails.getTransactions().get(2).getAmount()); - assertTrue(loanDetails.getTransactions().get(3).getType().getDisbursement()); - assertEquals(20.0, loanDetails.getTransactions().get(3).getAmount()); - assertEquals(0.0, loanDetails.getTransactions().get(3).getOutstandingLoanBalance()); - assertEquals(4, loanDetails.getTransactions().size()); + loanTransactionHelper.disburseLoan(loanResponse.getResourceId(), + new PostLoansLoanIdRequest().actualDisbursementDate("05 March 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(20.00)).locale("en")); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getResourceId()); + // Verify Repayment Schedule + verifyRepaymentSchedule(loanResponse.getResourceId(), // + installment(1000.0, null, "03 March 2023"), // + installment(250.0, 0.0, 0.0, true, "03 March 2023"), // + installment(20.0, null, "05 March 2023"), // + installment(5.0, 0.0, 0.0, true, "05 March 2023"), // + installment(765.0, 0.0, 0.0, true, "02 April 2023") // + ); + // verify transactions + verifyTransactions(loanResponse.getResourceId(), // + transaction(1000.0, "Disbursement", "03 March 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Down Payment", "03 March 2023", 750.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(800.0, "Repayment", "03 March 2023", 0.0, 750.0, 0.0, 0.0, 0.0, 0.0, 50.0), // + transaction(20.0, "Disbursement", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 30.0) // + ); assertTrue(loanDetails.getStatus().getOverpaid()); assertEquals(30.0, loanDetails.getTotalOverpaid()); - loanTransactionHelper.disburseLoanWithTransactionAmount("05 March 2023", loanId, "30"); - loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); - assertTrue(loanDetails.getTransactions().get(4).getType().getDisbursement()); - assertEquals(30.0, loanDetails.getTransactions().get(4).getAmount()); - assertEquals(0.0, loanDetails.getTransactions().get(4).getOutstandingLoanBalance()); - assertEquals(5, loanDetails.getTransactions().size()); + loanTransactionHelper.disburseLoan(loanResponse.getResourceId(), + new PostLoansLoanIdRequest().actualDisbursementDate("05 March 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(30.00)).locale("en")); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getResourceId()); + + // Verify Repayment Schedule + verifyRepaymentSchedule(loanResponse.getResourceId(), // + installment(1000.0, null, "03 March 2023"), // + installment(250.0, 0.0, 0.0, true, "03 March 2023"), // + installment(20.0, null, "05 March 2023"), // + installment(30.0, null, "05 March 2023"), // + installment(5.0, 0.0, 0.0, true, "05 March 2023"), // + installment(7.5, 0.0, 0.0, true, "05 March 2023"), // + installment(787.5, 0.0, 0.0, true, "02 April 2023") // + ); + // verify transactions + verifyTransactions(loanResponse.getResourceId(), // + transaction(1000.0, "Disbursement", "03 March 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Down Payment", "03 March 2023", 750.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(800.0, "Repayment", "03 March 2023", 0.0, 750.0, 0.0, 0.0, 0.0, 0.0, 50.0), // + transaction(20.0, "Disbursement", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 30.0), // + transaction(30.0, "Disbursement", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); assertTrue(loanDetails.getStatus().getClosedObligationsMet()); assertEquals(0.0, loanDetails.getSummary().getTotalOutstanding()); assertEquals(null, loanDetails.getTotalOverpaid()); - PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment((long) loanId, + PostLoansLoanIdTransactionsResponse repayment = loanTransactionHelper.makeLoanRepayment(loanResponse.getResourceId(), new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("05 March 2023").locale("en") .transactionAmount(1.0)); - loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getResourceId()); + // verify transactions + verifyTransactions(loanResponse.getResourceId(), // + transaction(1000.0, "Disbursement", "03 March 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Down Payment", "03 March 2023", 750.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(800.0, "Repayment", "03 March 2023", 0.0, 750.0, 0.0, 0.0, 0.0, 0.0, 50.0), // + transaction(20.0, "Disbursement", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 30.0), // + transaction(30.0, "Disbursement", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(1.0, "Repayment", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0) // + ); assertTrue(loanDetails.getStatus().getOverpaid()); assertEquals(1.0, loanDetails.getTotalOverpaid()); - loanTransactionHelper.disburseLoanWithTransactionAmount("05 March 2023", loanId, "40"); - loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); - assertTrue(loanDetails.getTransactions().get(5).getType().getRepayment()); - assertEquals(1.0, loanDetails.getTransactions().get(5).getAmount()); - assertTrue(loanDetails.getTransactions().get(6).getType().getDisbursement()); - assertEquals(40.0, loanDetails.getTransactions().get(6).getAmount()); - assertEquals(39.0, loanDetails.getTransactions().get(6).getOutstandingLoanBalance()); - assertEquals("loanTransactionType.downPayment", loanDetails.getTransactions().get(7).getType().getCode()); - assertEquals(9.0, loanDetails.getTransactions().get(7).getAmount()); - assertEquals(30.0, loanDetails.getTransactions().get(7).getOutstandingLoanBalance()); - assertEquals(8, loanDetails.getTransactions().size()); + loanTransactionHelper.disburseLoan(loanResponse.getResourceId(), + new PostLoansLoanIdRequest().actualDisbursementDate("05 March 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(40.00)).locale("en")); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getResourceId()); + // Verify Repayment Schedule + verifyRepaymentSchedule(loanResponse.getResourceId(), // + installment(1000.0, null, "03 March 2023"), // + installment(250.0, 0.0, 0.0, true, "03 March 2023"), // + installment(20.0, null, "05 March 2023"), // + installment(30.0, null, "05 March 2023"), // + installment(40.0, null, "05 March 2023"), // + installment(5.0, 0.0, 0.0, true, "05 March 2023"), // + installment(7.5, 0.0, 0.0, true, "05 March 2023"), // + installment(10.0, 0.0, 0.0, true, "05 March 2023"), // + installment(817.5, 0.0, 30.0, false, "02 April 2023") // + ); + // verify transactions + verifyTransactions(loanResponse.getResourceId(), // + transaction(1000.0, "Disbursement", "03 March 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Down Payment", "03 March 2023", 750.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(800.0, "Repayment", "03 March 2023", 0.0, 750.0, 0.0, 0.0, 0.0, 0.0, 50.0), // + transaction(20.0, "Disbursement", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 30.0), // + transaction(30.0, "Disbursement", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(1.0, "Repayment", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0), // + transaction(40.0, "Disbursement", "05 March 2023", 39.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0), // + transaction(9.0, "Down Payment", "05 March 2023", 30.0, 9.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); assertTrue(loanDetails.getStatus().getActive()); assertEquals(30.0, loanDetails.getSummary().getTotalOutstanding()); @@ -1377,23 +1384,229 @@ public class LoanRepaymentScheduleWithDownPaymentTest { new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("05 March 2023") .transactionAmount(0.0).locale("en")); - loanDetails = loanTransactionHelper.getLoanDetails(loanId.longValue()); - assertTrue(loanDetails.getTransactions().get(5).getType().getRepayment()); - assertEquals(1.0, loanDetails.getTransactions().get(5).getAmount()); - assertTrue(loanDetails.getTransactions().get(5).getManuallyReversed()); - assertTrue(loanDetails.getTransactions().get(6).getType().getDisbursement()); - assertEquals(40.0, loanDetails.getTransactions().get(6).getAmount()); - assertEquals(40.0, loanDetails.getTransactions().get(6).getOutstandingLoanBalance()); - assertEquals("loanTransactionType.downPayment", loanDetails.getTransactions().get(7).getType().getCode()); - assertEquals(9.0, loanDetails.getTransactions().get(7).getAmount()); - assertEquals(31.0, loanDetails.getTransactions().get(7).getOutstandingLoanBalance()); - assertEquals(8, loanDetails.getTransactions().size()); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getResourceId()); + // Verify Repayment Schedule + verifyRepaymentSchedule(loanResponse.getResourceId(), // + installment(1000.0, null, "03 March 2023"), // + installment(250.0, 0.0, 0.0, true, "03 March 2023"), // + installment(20.0, null, "05 March 2023"), // + installment(30.0, null, "05 March 2023"), // + installment(40.0, null, "05 March 2023"), // + installment(5.0, 0.0, 0.0, true, "05 March 2023"), // + installment(7.5, 0.0, 0.0, true, "05 March 2023"), // + installment(10.0, 0.0, 1.0, false, "05 March 2023"), // + installment(817.5, 0.0, 30.0, false, "02 April 2023") // + ); + // verify transactions + verifyTransactions(loanResponse.getResourceId(), // + transaction(1000.0, "Disbursement", "03 March 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Down Payment", "03 March 2023", 750.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(800.0, "Repayment", "03 March 2023", 0.0, 750.0, 0.0, 0.0, 0.0, 0.0, 50.0), // + transaction(20.0, "Disbursement", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 30.0), // + transaction(30.0, "Disbursement", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(1.0, "Repayment", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 1.0, true), // + transaction(40.0, "Disbursement", "05 March 2023", 40.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(9.0, "Down Payment", "05 March 2023", 31.0, 9.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); assertTrue(loanDetails.getStatus().getActive()); assertEquals(31.0, loanDetails.getSummary().getTotalOutstanding()); - } finally { - GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); - } + }); + } + + @Test + public void downPaymentOnOverpaidCumulativeLoan() { + runAt("03 March 2023", () -> { + LocalDate disbursementDate = LocalDate.of(2023, 3, 3); + + PostClientsResponse client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + + final PostLoanProductsRequest loanProductsRequest = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .installmentAmountInMultiplesOf(null).enableDownPayment(true).enableAutoRepaymentForDownPayment(true) + .loanScheduleType(LoanScheduleType.CUMULATIVE.name()).paymentAllocation(null) + .transactionProcessingStrategyCode( + DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.STRATEGY_CODE) + .disbursedAmountPercentageForDownPayment(BigDecimal.valueOf(25)); + + PostLoanProductsResponse loanProductsResponse = loanTransactionHelper.createLoanProduct(loanProductsRequest); + + String disbursementDateStr = DateUtils.format(disbursementDate, DATETIME_PATTERN); + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(client.getResourceId()) + .productId(loanProductsResponse.getResourceId()).loanType("individual").locale("en").dateFormat(DATETIME_PATTERN) + .amortizationType(1).interestRatePerPeriod(BigDecimal.ZERO).interestCalculationPeriodType(1).interestType(0) + .maxOutstandingLoanBalance(BigDecimal.valueOf(35000)) + .transactionProcessingStrategyCode( + DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.STRATEGY_CODE) + .expectedDisbursementDate(disbursementDateStr).dateFormat(DATETIME_PATTERN).submittedOnDate(disbursementDateStr) + .repaymentFrequencyType(0).repaymentEvery(30).numberOfRepayments(1).loanTermFrequency(30).loanTermFrequencyType(0) + .principal(BigDecimal.valueOf(1000)).loanType("individual").maxOutstandingLoanBalance(BigDecimal.valueOf(35000))); + + loanTransactionHelper.approveLoan(loanResponse.getResourceId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN) + .approvedOnDate(disbursementDateStr).locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getResourceId(), + new PostLoansLoanIdRequest().actualDisbursementDate(disbursementDateStr).dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(1000)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getResourceId()); + + assertTrue(loanDetails.getStatus().getActive()); + + // Verify Repayment Schedule + verifyRepaymentSchedule(loanResponse.getResourceId(), // + installment(1000.0, null, "03 March 2023"), // + installment(250.0, 0.0, 0.0, true, "03 March 2023"), // + installment(750.0, 0.0, 750.0, false, "02 April 2023") // + ); + // verify transactions + verifyTransactions(loanResponse.getResourceId(), // + transaction(1000.0, "Disbursement", "03 March 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Down Payment", "03 March 2023", 750.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); + verifyJournalEntries(loanResponse.getResourceId(), journalEntry(1000.0, loansReceivableAccount, "DEBIT"), // + journalEntry(1000.0, suspenseClearingAccount, "CREDIT"), // + journalEntry(250.0, loansReceivableAccount, "CREDIT"), // + journalEntry(250.0, suspenseClearingAccount, "DEBIT") // + ); + + String externalId = UUID.randomUUID().toString(); + loanTransactionHelper.makeLoanRepayment(loanResponse.getResourceId(), + new PostLoansLoanIdTransactionsRequest().dateFormat("dd MMMM yyyy").transactionDate("03 March 2023").locale("en") + .transactionAmount(800.0).externalId(externalId)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getResourceId()); + // Verify Repayment Schedule + verifyRepaymentSchedule(loanResponse.getResourceId(), // + installment(1000.0, null, "03 March 2023"), // + installment(250.0, 0.0, 0.0, true, "03 March 2023"), // + installment(750.0, 0.0, 0.0, true, "02 April 2023") // + ); + // verify transactions + verifyTransactions(loanResponse.getResourceId(), // + transaction(1000.0, "Disbursement", "03 March 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Down Payment", "03 March 2023", 750.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(800.0, "Repayment", "03 March 2023", 0.0, 750.0, 0.0, 0.0, 0.0, 0.0, 50.0) // + ); + assertTrue(loanDetails.getStatus().getOverpaid()); + assertEquals(50.0, loanDetails.getTotalOverpaid()); + + // second disbursement + disbursementDate = LocalDate.of(2023, 3, 5); + BusinessDateHelper.updateBusinessDate(requestSpec, responseSpec, BusinessDateType.BUSINESS_DATE, disbursementDate); + + loanTransactionHelper.disburseLoan(loanResponse.getResourceId(), + new PostLoansLoanIdRequest().actualDisbursementDate("05 March 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(20.00)).locale("en")); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getResourceId()); + // Verify Repayment Schedule + verifyRepaymentSchedule(loanResponse.getResourceId(), // + installment(1000.0, null, "03 March 2023"), // + installment(250.0, 0.0, 0.0, true, "03 March 2023"), // + installment(20.0, null, "05 March 2023"), // + installment(5.0, 0.0, 0.0, true, "05 March 2023"), // + installment(765.0, 0.0, 0.0, true, "02 April 2023") // + ); + // verify transactions + verifyTransactions(loanResponse.getResourceId(), // + transaction(1000.0, "Disbursement", "03 March 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Down Payment", "03 March 2023", 750.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(800.0, "Repayment", "03 March 2023", 0.0, 770.0, 0.0, 0.0, 0.0, 0.0, 30.0), // + transaction(20.0, "Disbursement", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); + assertTrue(loanDetails.getStatus().getOverpaid()); + assertEquals(30.0, loanDetails.getTotalOverpaid()); + + loanTransactionHelper.disburseLoan(loanResponse.getResourceId(), + new PostLoansLoanIdRequest().actualDisbursementDate("05 March 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(30.00)).locale("en")); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getResourceId()); + + // Verify Repayment Schedule + verifyRepaymentSchedule(loanResponse.getResourceId(), // + installment(1000.0, null, "03 March 2023"), // + installment(250.0, 0.0, 0.0, true, "03 March 2023"), // + installment(20.0, null, "05 March 2023"), // + installment(30.0, null, "05 March 2023"), // + installment(5.0, 0.0, 0.0, true, "05 March 2023"), // + installment(7.5, 0.0, 0.0, true, "05 March 2023"), // + installment(787.5, 0.0, 0.0, true, "02 April 2023") // + ); + // verify transactions + verifyTransactions(loanResponse.getResourceId(), // + transaction(1000.0, "Disbursement", "03 March 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Down Payment", "03 March 2023", 750.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(800.0, "Repayment", "03 March 2023", 0.0, 800.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(20.0, "Disbursement", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(30.0, "Disbursement", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); + + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + assertEquals(0.0, loanDetails.getSummary().getTotalOutstanding()); + assertEquals(null, loanDetails.getTotalOverpaid()); + + loanTransactionHelper.disburseLoan(loanResponse.getResourceId(), + new PostLoansLoanIdRequest().actualDisbursementDate("05 March 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(40.00)).locale("en")); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getResourceId()); + // Verify Repayment Schedule + verifyRepaymentSchedule(loanResponse.getResourceId(), // + installment(1000.0, null, "03 March 2023"), // + installment(250.0, 0.0, 0.0, true, "03 March 2023"), // + installment(20.0, null, "05 March 2023"), // + installment(30.0, null, "05 March 2023"), // + installment(40.0, null, "05 March 2023"), // + installment(5.0, 0.0, 0.0, true, "05 March 2023"), // + installment(7.5, 0.0, 0.0, true, "05 March 2023"), // + installment(10.0, 0.0, 0.0, true, "05 March 2023"), // + installment(817.5, 0.0, 30.0, false, "02 April 2023") // + ); + // verify transactions + verifyTransactions(loanResponse.getResourceId(), // + transaction(1000.0, "Disbursement", "03 March 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Down Payment", "03 March 2023", 750.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(800.0, "Repayment", "03 March 2023", 0.0, 800.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(20.0, "Disbursement", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(30.0, "Disbursement", "05 March 2023", 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(40.0, "Disbursement", "05 March 2023", 40.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(10.0, "Down Payment", "05 March 2023", 30.0, 10.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); + + assertTrue(loanDetails.getStatus().getActive()); + assertEquals(30.0, loanDetails.getSummary().getTotalOutstanding()); + + loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), externalId, + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("05 March 2023") + .transactionAmount(0.0).locale("en")); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getResourceId()); + // Verify Repayment Schedule + verifyRepaymentSchedule(loanResponse.getResourceId(), // + installment(1000.0, null, "03 March 2023"), // + installment(250.0, 0.0, 0.0, true, "03 March 2023"), // + installment(20.0, null, "05 March 2023"), // + installment(30.0, null, "05 March 2023"), // + installment(40.0, null, "05 March 2023"), // + installment(5.0, 0.0, 0.0, true, "05 March 2023"), // + installment(7.5, 0.0, 2.5, false, "05 March 2023"), // + installment(10.0, 0.0, 10.0, false, "05 March 2023"), // + installment(817.5, 0.0, 817.5, false, "02 April 2023") // + ); + // verify transactions + verifyTransactions(loanResponse.getResourceId(), // + transaction(1000.0, "Disbursement", "03 March 2023", 1000.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(250.0, "Down Payment", "03 March 2023", 750.0, 250.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(800.0, "Repayment", "03 March 2023", 0.0, 800.0, 0.0, 0.0, 0.0, 0.0, 0.0, true), // + transaction(20.0, "Disbursement", "05 March 2023", 770.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(30.0, "Disbursement", "05 March 2023", 800.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(40.0, "Disbursement", "05 March 2023", 840.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), // + transaction(10.0, "Down Payment", "05 March 2023", 830.0, 10.0, 0.0, 0.0, 0.0, 0.0, 0.0) // + ); + + assertTrue(loanDetails.getStatus().getActive()); + assertEquals(830.0, loanDetails.getSummary().getTotalOutstanding()); + }); } private void checkNoDownPaymentTransaction(final Integer loanID) { @@ -1470,63 +1683,6 @@ public class LoanRepaymentScheduleWithDownPaymentTest { return loanTransactionHelper.getLoanProduct(loanProductId); } - private Integer createLoanAccountWithAdvancedPaymentAllocation(final Integer clientID, final Long loanProductID, - final String externalId) { - - String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("30") - .withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) - .withLoanTermFrequencyAsDays().withNumberOfRepayments("1").withRepaymentEveryAfter("30").withRepaymentFrequencyTypeAsDays() - .withInterestRatePerPeriod("0").withInterestTypeAsFlatBalance().withAmortizationTypeAsEqualPrincipalPayments() - .withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withExpectedDisbursementDate("03 March 2023") - .withSubmittedOnDate("03 March 2023").withLoanType("individual").withExternalId(externalId) - .build(clientID.toString(), loanProductID.toString(), null); - - final Integer loanId = loanTransactionHelper.getLoanId(loanApplicationJSON); - loanTransactionHelper.approveLoan("03 March 2023", "1000", loanId, null); - return loanId; - } - - private GetLoanProductsProductIdResponse createProgressiveLoanProductWithDownPaymentConfigurationAndAccrualAccounting( - LoanTransactionHelper loanTransactionHelper, Boolean enableDownPayment, String disbursedAmountPercentageForDownPayment, - boolean enableAutoRepaymentForDownPayment, final Account... accounts) { - final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("1000").withRepaymentTypeAsMonth() - .withLoanScheduleProcessingType(LoanScheduleProcessingType.HORIZONTAL).withLoanScheduleType(LoanScheduleType.PROGRESSIVE) - .addAdvancedPaymentAllocation(createDefaultPaymentAllocation()).withRepaymentAfterEvery("1").withNumberOfRepayments("1") - .withRepaymentTypeAsMonth().withinterestRatePerPeriod("0").withInterestRateFrequencyTypeAsMonths() - .withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsDecliningBalance() - .withAccountingRulePeriodicAccrual(accounts).withInterestCalculationPeriodTypeAsRepaymentPeriod(true).withDaysInMonth("30") - .withDaysInYear("365").withMoratorium("0", "0").withMultiDisburse().withDisallowExpectedDisbursements(true) - .withEnableDownPayment(enableDownPayment, disbursedAmountPercentageForDownPayment, enableAutoRepaymentForDownPayment) - .build(null); - final Integer loanProductId = loanTransactionHelper.getLoanProductId(loanProductJSON); - return loanTransactionHelper.getLoanProduct(loanProductId); - } - - private List getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) { - AtomicInteger integer = new AtomicInteger(1); - return Arrays.stream(paymentAllocationTypes).map(pat -> { - PaymentAllocationOrder paymentAllocationOrder = new PaymentAllocationOrder(); - paymentAllocationOrder.setPaymentAllocationRule(pat.name()); - paymentAllocationOrder.setOrder(integer.getAndIncrement()); - return paymentAllocationOrder; - }).toList(); - } - - private AdvancedPaymentData createDefaultPaymentAllocation() { - AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData(); - advancedPaymentData.setTransactionType("DEFAULT"); - advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT"); - - List paymentAllocationOrders = getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY, - PaymentAllocationType.PAST_DUE_FEE, PaymentAllocationType.PAST_DUE_PRINCIPAL, PaymentAllocationType.PAST_DUE_INTEREST, - PaymentAllocationType.DUE_PENALTY, PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL, - PaymentAllocationType.DUE_INTEREST, PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE, - PaymentAllocationType.IN_ADVANCE_PRINCIPAL, PaymentAllocationType.IN_ADVANCE_INTEREST); - - advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders); - return advancedPaymentData; - } - private Integer createApproveAndDisburseLoanAccount(final Integer clientID, final Long loanProductID, final String externalId) { String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal("1000").withLoanTermFrequency("1")