From 6d938995a06a26e4d0ab0a43ce3612a7e3d08170 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Mon, 4 Sep 2023 22:28:54 +0200 Subject: [PATCH] FINERACT-1968: Advanced payment allocation - Default allocation --- .../DefaultExceptionMapper.java | 4 +- .../infrastructure/core/service/MathUtil.java | 8 + .../portfolio/loanaccount/domain/Loan.java | 24 +- ...RepaymentScheduleTransactionProcessor.java | 2 +- .../PaymentAllocationTransactionType.java | 10 +- ...RepaymentScheduleTransactionProcessor.java | 24 +- ...edPaymentScheduleTransactionProcessor.java | 293 ++- ...RepaymentScheduleTransactionProcessor.java | 2 +- ...RepaymentScheduleTransactionProcessor.java | 2 +- .../domain/AbstractLoanScheduleGenerator.java | 2 +- ...ymentScheduleTransactionProcessorTest.java | 12 +- ...ymentScheduleTransactionProcessorTest.java | 12 +- ...ntAllocationLoanRepaymentScheduleTest.java | 1809 +++++++++++++++++ .../common/loans/LoanTransactionHelper.java | 6 + 14 files changed, 2163 insertions(+), 47 deletions(-) create mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapper.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapper.java index 5d3e48b24f9..76008cfe8f7 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapper.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/exceptionmapper/DefaultExceptionMapper.java @@ -25,6 +25,7 @@ import jakarta.ws.rs.ext.ExceptionMapper; import java.util.Map; import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.ObjectUtils; import org.springframework.context.annotation.Scope; import org.springframework.stereotype.Component; @@ -40,7 +41,8 @@ public int errorCode() { @Override public Response toResponse(RuntimeException runtimeException) { - return Response.status(SC_INTERNAL_SERVER_ERROR).entity(Map.of("Exception", runtimeException.getMessage())) + return Response.status(SC_INTERNAL_SERVER_ERROR) + .entity(Map.of("Exception", ObjectUtils.defaultIfNull(runtimeException.getMessage(), "No error message available"))) .type(MediaType.APPLICATION_JSON).build(); } } diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java index 285183bb3d0..443ea7ca4ca 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/MathUtil.java @@ -433,6 +433,14 @@ public static Money min(Money first, Money second, Money third, boolean notNull) return min(min(first, second, notNull), third, notNull); } + /** + * Calculate percentage of a value + * + * @param value + * @param percentage + * @param precision + * @return + */ public static BigDecimal percentageOf(final BigDecimal value, final BigDecimal percentage, final int precision) { BigDecimal percentageOf = BigDecimal.ZERO; if (isGreaterThanZero(value)) { 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 d6756550ef0..80b3286c74f 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 @@ -702,7 +702,7 @@ public ChangedTransactionDetail reprocessTransactions() { final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory .determineProcessor(this.transactionProcessingStrategyCode); final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement(); - changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(), + changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { @@ -863,7 +863,7 @@ public void removeLoanCharge(final LoanCharge loanCharge) { * affected Transactions ***/ final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement(); - loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(), + loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); } this.charges.remove(loanCharge); @@ -931,7 +931,7 @@ public Map updateLoanCharge(final LoanCharge loanCharge, final J * affected Transactions ***/ final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement(); - loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(), + loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); } else { // reprocess loan schedule based on charge been waived. @@ -1124,7 +1124,7 @@ public LoanTransaction waiveLoanCharge(final LoanCharge loanCharge, final LoanLi * affected Transactions ***/ final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement(); - loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(), + loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); } else { // reprocess loan schedule based on charge been waived. @@ -2706,7 +2706,7 @@ private ChangedTransactionDetail reprocessTransactionForDisbursement() { if (!allNonContraTransactionsPostDisbursement.isEmpty()) { final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory .determineProcessor(this.transactionProcessingStrategyCode); - changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(), + changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { mapEntry.getValue().updateLoan(this); @@ -3351,7 +3351,7 @@ private ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(fi regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); } final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement(); - changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(), + changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { mapEntry.getValue().updateLoan(this); @@ -3779,7 +3779,7 @@ public ChangedTransactionDetail undoWrittenOff(LoanLifecycleStateMachine loanLif if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); } - ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions( + ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); updateLoanSummaryDerivedFields(); @@ -3928,7 +3928,7 @@ private ChangedTransactionDetail closeDisbursements(final ScheduleGeneratorDTO s regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); } final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement(); - changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(), + changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { mapEntry.getValue().updateLoan(this); @@ -5402,7 +5402,7 @@ public ChangedTransactionDetail updateDisbursementDateAndAmountForTranche(final final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory .determineProcessor(this.transactionProcessingStrategyCode); final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement(); - ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions( + ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { @@ -5545,7 +5545,7 @@ public ChangedTransactionDetail processTransactions() { final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory .determineProcessor(this.transactionProcessingStrategyCode); final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement(); - ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions( + ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { @@ -5788,7 +5788,7 @@ public void processPostDisbursementTransactions() { for (LoanTransaction loanTransaction : allNonContraTransactionsPostDisbursement) { copyTransactions.add(LoanTransaction.copyTransactionProperties(loanTransaction)); } - loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(), copyTransactions, getCurrency(), + loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), copyTransactions, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); updateLoanSummaryDerivedFields(); @@ -6392,7 +6392,7 @@ private ChangedTransactionDetail handleRefundTransaction(final LoanTransaction l getRepaymentScheduleInstallments(), getActiveCharges(), getTotalOverpaidAsMoney()); } else { final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsPostDisbursement(); - changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(getDisbursementDate(), + changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { mapEntry.getValue().updateLoan(this); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java index 9131646eade..efcfba19ba4 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/LoanRepaymentScheduleTransactionProcessor.java @@ -49,7 +49,7 @@ void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency * before existing transactions or and adjustment is made to an existing in which case the entire loan schedule * needs to be re-processed. */ - ChangedTransactionDetail reprocessLoanTranactions(LocalDate disbursementDate, List repaymentsOrWaivers, + ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List repaymentsOrWaivers, MonetaryCurrency currency, List repaymentScheduleInstallments, Set charges); Money handleRepaymentSchedule(List transactionsPostDisbursement, MonetaryCurrency currency, diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java index ba0493a5346..3e95b1bd26a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java @@ -27,7 +27,15 @@ public enum PaymentAllocationTransactionType { DEFAULT(null), // - REPAYMENT(LoanTransactionType.REPAYMENT); // + REPAYMENT(LoanTransactionType.REPAYMENT), // + DOWN_PAYMENT(LoanTransactionType.DOWN_PAYMENT), // + MERCHANT_ISSUED_REFUND(LoanTransactionType.MERCHANT_ISSUED_REFUND), // + PAYOUT_REFUND(LoanTransactionType.PAYOUT_REFUND), // + GOODWILL_CREDIT(LoanTransactionType.GOODWILL_CREDIT), // + CHARGE_REFUND(LoanTransactionType.CHARGE_REFUND), // + CHARGE_ADJUSTMENT(LoanTransactionType.CHARGE_ADJUSTMENT), // + WAIVE_INTEREST(LoanTransactionType.WAIVE_INTEREST),// + ; // private final LoanTransactionType loanTransactionType; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 7ac6dff24c7..fcbb867cfe0 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -64,7 +64,7 @@ public boolean accept(String s) { } @Override - public ChangedTransactionDetail reprocessLoanTranactions(final LocalDate disbursementDate, + public ChangedTransactionDetail reprocessLoanTransactions(final LocalDate disbursementDate, final List transactionsPostDisbursement, final MonetaryCurrency currency, final List installments, final Set charges) { @@ -382,7 +382,7 @@ private void reprocessChargebackTransactionRelation(ChangedTransactionDetail cha } } - private void reprocessInstallments(List installments, MonetaryCurrency currency) { + protected void reprocessInstallments(List installments, MonetaryCurrency currency) { LoanRepaymentScheduleInstallment lastInstallment = installments.get(installments.size() - 1); if (lastInstallment.isAdditional() && lastInstallment.getDue(currency).isZero()) { installments.remove(lastInstallment); @@ -413,7 +413,7 @@ private List getMergedTransactionList(List tra return mergedList; } - private void createNewTransaction(LoanTransaction loanTransaction, LoanTransaction newLoanTransaction, + protected void createNewTransaction(LoanTransaction loanTransaction, LoanTransaction newLoanTransaction, ChangedTransactionDetail changedTransactionDetail) { loanTransaction.reverse(); loanTransaction.updateExternalId(null); @@ -538,7 +538,7 @@ private void processCreditTransaction(LoanTransaction loanTransaction, Money ove } } - private Money handleTransactionAndCharges(final LoanTransaction loanTransaction, final MonetaryCurrency currency, + protected Money handleTransactionAndCharges(final LoanTransaction loanTransaction, final MonetaryCurrency currency, final List installments, final Set charges, final Money chargeAmountToProcess, final boolean isFeeCharge) { if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { @@ -574,7 +574,7 @@ private Money handleTransactionAndCharges(final LoanTransaction loanTransaction, return transactionAmountUnprocessed; } - private Money processTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency, + protected Money processTransaction(final LoanTransaction loanTransaction, final MonetaryCurrency currency, final List installments, final Set charges, Money amountToProcess) { int installmentIndex = 0; @@ -607,7 +607,7 @@ private Money processTransaction(final LoanTransaction loanTransaction, final Mo return transactionAmountUnprocessed; } - private Set extractFeeCharges(final Set loanCharges) { + protected Set extractFeeCharges(final Set loanCharges) { final Set feeCharges = new HashSet<>(); for (final LoanCharge loanCharge : loanCharges) { if (loanCharge.isFeeCharge()) { @@ -617,7 +617,7 @@ private Set extractFeeCharges(final Set loanCharges) { return feeCharges; } - private Set extractPenaltyCharges(final Set loanCharges) { + protected Set extractPenaltyCharges(final Set loanCharges) { final Set penaltyCharges = new HashSet<>(); for (final LoanCharge loanCharge : loanCharges) { if (loanCharge.isPenaltyCharge()) { @@ -627,7 +627,7 @@ private Set extractPenaltyCharges(final Set loanCharges) return penaltyCharges; } - private void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set charges, + protected void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, final Money feeCharges, final Set charges, final Integer installmentNumber) { Money amountRemaining = feeCharges; @@ -661,7 +661,7 @@ private void updateChargesPaidAmountBy(final LoanTransaction loanTransaction, fi } - private LoanCharge findEarliestUnpaidChargeFromUnOrderedSet(final Set charges, final MonetaryCurrency currency) { + protected LoanCharge findEarliestUnpaidChargeFromUnOrderedSet(final Set charges, final MonetaryCurrency currency) { LoanCharge earliestUnpaidCharge = null; LoanCharge installemntCharge = null; LoanInstallmentCharge chargePerInstallment = null; @@ -687,7 +687,7 @@ private LoanCharge findEarliestUnpaidChargeFromUnOrderedSet(final Set installments) { final LocalDate transactionDate = loanTransaction.getTransactionDate(); @@ -712,12 +712,12 @@ private void handleWriteOff(final LoanTransaction loanTransaction, final Monetar loanTransaction.updateComponentsAndTotal(principalPortion, interestPortion, feeChargesPortion, penaltychargesPortion); } - private void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency, Money overpaidAmount, + protected void handleChargeback(LoanTransaction loanTransaction, MonetaryCurrency currency, Money overpaidAmount, List installments) { processCreditTransaction(loanTransaction, overpaidAmount, currency, installments); } - private void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency, + protected void handleRefund(LoanTransaction loanTransaction, MonetaryCurrency currency, List installments, final Set charges) { List transactionMappings = new ArrayList<>(); final Comparator byDate = Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index aa236efde4a..6a9c9d8a5bc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -18,19 +18,37 @@ */ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl; +import java.math.BigDecimal; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.Comparator; import java.util.List; import java.util.Set; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.Setter; +import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.NotImplementedException; +import org.apache.commons.lang3.ObjectUtils; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule; +import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType; +import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; +import org.springframework.context.annotation.Profile; +//TODO: remove `test` profile when it is finished +@Profile("test") +@Slf4j public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor { public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy"; @@ -74,20 +92,285 @@ protected Money handleRefundTransactionPaymentOfInstallment(LoanRepaymentSchedul } @Override - public ChangedTransactionDetail reprocessLoanTranactions(LocalDate disbursementDate, List transactionsPostDisbursement, - MonetaryCurrency currency, List installments, Set charges) { - throw new NotImplementedException(); + public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, + List transactionsPostDisbursement, MonetaryCurrency currency, + List installments, Set charges) { + + // TODO: rewrite this whole logic step by step + if (charges != null) { + for (final LoanCharge loanCharge : charges) { + if (!loanCharge.isDueAtDisbursement()) { + loanCharge.resetPaidAmount(currency); + } + } + } + + for (final LoanRepaymentScheduleInstallment currentInstallment : installments) { + currentInstallment.resetDerivedComponents(); + currentInstallment.updateDerivedFields(currency, disbursementDate); + } + + // TODO: Remove this reprocess and add the charges to the installment in chronological order + final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper(); + wrapper.reprocess(currency, disbursementDate, installments, charges); + final ChangedTransactionDetail changedTransactionDetail = new ChangedTransactionDetail(); + for (final LoanTransaction loanTransaction : transactionsPostDisbursement) { + if (loanTransaction.getId() == null) { + processLatestTransaction(loanTransaction, currency, installments, charges, null); + loanTransaction.adjustInterestComponent(currency); + } else { + /** + * For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties) has + * changed.
+ **/ + final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction); + + // Reset derived component of new loan transaction and + // re-process transaction + processLatestTransaction(newLoanTransaction, currency, installments, charges, null); + newLoanTransaction.adjustInterestComponent(currency); + /** + * Check if the transaction amounts have changed. If so, reverse the original transaction and update + * changedTransactionDetail accordingly + **/ + if (LoanTransaction.transactionAmountsMatch(currency, loanTransaction, newLoanTransaction)) { + loanTransaction.updateLoanTransactionToRepaymentScheduleMappings( + newLoanTransaction.getLoanTransactionToRepaymentScheduleMappings()); + } else { + createNewTransaction(loanTransaction, newLoanTransaction, changedTransactionDetail); + } + } + } + reprocessInstallments(installments, currency); + return changedTransactionDetail; } @Override public void processLatestTransaction(LoanTransaction loanTransaction, MonetaryCurrency currency, List installments, Set charges, Money overpaidAmount) { - throw new NotImplementedException(); + switch (loanTransaction.getTypeOf()) { + case WRITEOFF -> handleWriteOff(loanTransaction, currency, installments); + case REFUND_FOR_ACTIVE_LOAN -> handleRefund(loanTransaction, currency, installments, charges); + case CHARGEBACK -> handleChargeback(loanTransaction, currency, overpaidAmount, installments); + + case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT, + WAIVE_INTEREST, RECOVERY_REPAYMENT -> + handleRepayment(loanTransaction, currency, installments, charges); + // TODO: Cover rest of the transaction types + default -> { + log.warn("Unhandled transaction processing for transaction type: {}", loanTransaction.getTypeOf()); + } + } } @Override public Money handleRepaymentSchedule(List transactionsPostDisbursement, MonetaryCurrency currency, List installments, Set loanCharges) { - return super.handleRepaymentSchedule(transactionsPostDisbursement, currency, installments, loanCharges); + throw new NotImplementedException(); + } + + private void handleRepayment(LoanTransaction loanTransaction, MonetaryCurrency currency, + List installments, Set charges) { + if (loanTransaction.isRepaymentLikeType() || loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) { + loanTransaction.resetDerivedComponents(); + } + Money transactionAmountUnprocessed = loanTransaction.getAmount(currency); + Money zero = Money.zero(currency); + List transactionMappings = new ArrayList<>(); + + List paymentAllocationRules = loanTransaction.getLoan().getPaymentAllocationRules(); + LoanPaymentAllocationRule defaultPaymentAllocationRule = paymentAllocationRules.stream() + .filter(e -> PaymentAllocationTransactionType.DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow(); + LoanPaymentAllocationRule paymentAllocationRule = paymentAllocationRules.stream() + .filter(e -> loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst() + .orElse(defaultPaymentAllocationRule); + Balances balances = new Balances(zero, zero, zero, zero); + for (PaymentAllocationType paymentAllocationType : paymentAllocationRule.getAllocationTypes()) { + FutureInstallmentAllocationRule futureInstallmentAllocationRule = paymentAllocationRule.getFutureInstallmentAllocationRule(); + LoanRepaymentScheduleInstallment currentInstallment = null; + Money paidPortion = zero; + do { + switch (paymentAllocationType.getDueType()) { + case PAST_DUE -> { + currentInstallment = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) + .filter(e -> loanTransaction.getTransactionDate().isAfter(e.getDueDate())) + .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); + if (currentInstallment != null) { + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( + transactionMappings, loanTransaction, currentInstallment, currency); + paidPortion = payAllocation(paymentAllocationType, currentInstallment, loanTransaction, + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances); + transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); + } + } + case DUE -> { + currentInstallment = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) + .filter(e -> loanTransaction.getTransactionDate().isEqual(e.getDueDate())) + .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null); + if (currentInstallment != null) { + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( + transactionMappings, loanTransaction, currentInstallment, currency); + paidPortion = payAllocation(paymentAllocationType, currentInstallment, loanTransaction, + transactionAmountUnprocessed, loanTransactionToRepaymentScheduleMapping, balances); + transactionAmountUnprocessed = transactionAmountUnprocessed.minus(paidPortion); + } + } + case IN_ADVANCE -> { + // For having similar logic we are populating installment list even when the future installment + // allocation rule is NEXT_INSTALLMENT or LAST_INSTALLMENT hence the list has only one element. + List currentInstallments = new ArrayList<>(); + if (FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule)) { + currentInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) + .filter(e -> loanTransaction.getTransactionDate().isBefore(e.getDueDate())).toList(); + } else if (FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule)) { + currentInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) + .filter(e -> loanTransaction.getTransactionDate().isBefore(e.getDueDate())) + .min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); + } else if (FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule)) { + currentInstallments = installments.stream().filter(LoanRepaymentScheduleInstallment::isNotFullyPaidOff) + .filter(e -> loanTransaction.getTransactionDate().isBefore(e.getDueDate())) + .max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList(); + } + int numberOfInstallments = currentInstallments.size(); + paidPortion = zero; + if (numberOfInstallments > 0) { + // This will be the same amount as transactionAmountUnprocessed in case of the future + // installment allocation is NEXT_INSTALLMENT or LAST_INSTALLMENT + Money evenPortion = transactionAmountUnprocessed.dividedBy(numberOfInstallments, MoneyHelper.getRoundingMode()); + // Adjustment might be needed due to the divide operation and the rounding mode + Money balanceAdjustment = transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments)); + for (LoanRepaymentScheduleInstallment internalCurrentInstallment : currentInstallments) { + currentInstallment = internalCurrentInstallment; + // Adjust the portion for the last installment + if (internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments - 1))) { + evenPortion = evenPortion.add(balanceAdjustment); + } + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = getTransactionMapping( + transactionMappings, loanTransaction, currentInstallment, currency); + Money internalPaidPortion = payAllocation(paymentAllocationType, currentInstallment, loanTransaction, + evenPortion, loanTransactionToRepaymentScheduleMapping, balances); + // Some extra logic to allocate as much as possible across the installments if the + // outstanding balances are different + if (internalPaidPortion.isGreaterThanZero()) { + paidPortion = internalPaidPortion; + } + transactionAmountUnprocessed = transactionAmountUnprocessed.minus(internalPaidPortion); + } + } else { + currentInstallment = null; + } + } + } + } + // We are allocating till there is no pending installment or there is no more unprocessed transaction amount + // or there is no more outstanding balance of the allocation type + while (currentInstallment != null && transactionAmountUnprocessed.isGreaterThanZero() && paidPortion.isGreaterThanZero()); + + } + loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(), balances.getAggregatedInterestPortion(), + balances.getAggregatedFeeChargesPortion(), balances.getAggregatedPenaltyChargesPortion()); + loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings); + + // TODO: rewrite to provide sorted list + final Set loanFees = extractFeeCharges(charges); + final Set loanPenalties = extractPenaltyCharges(charges); + + if (loanTransaction.isNotWaiver() && !loanTransaction.isAccrual()) { + Money feeCharges = loanTransaction.getFeeChargesPortion(currency); + Money penaltyCharges = loanTransaction.getPenaltyChargesPortion(currency); + if (feeCharges.isGreaterThanZero()) { + // TODO: Rewrite to exclude charge payment + updateChargesPaidAmountBy(loanTransaction, feeCharges, loanFees, null); + } + + if (penaltyCharges.isGreaterThanZero()) { + // TODO: Rewrite to exclude charge payment + updateChargesPaidAmountBy(loanTransaction, penaltyCharges, loanPenalties, null); + } + } + handleOverpayment(transactionAmountUnprocessed, loanTransaction); + } + + private LoanTransactionToRepaymentScheduleMapping getTransactionMapping( + List transactionMappings, LoanTransaction loanTransaction, + LoanRepaymentScheduleInstallment currentInstallment, MonetaryCurrency currency) { + Money zero = Money.zero(currency); + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping = transactionMappings.stream() + .filter(e -> loanTransaction.equals(e.getLoanTransaction())) + .filter(e -> currentInstallment.equals(e.getLoanRepaymentScheduleInstallment())).findFirst().orElse(null); + if (loanTransactionToRepaymentScheduleMapping == null) { + loanTransactionToRepaymentScheduleMapping = LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, + currentInstallment, zero, zero, zero, zero); + transactionMappings.add(loanTransactionToRepaymentScheduleMapping); + } + return loanTransactionToRepaymentScheduleMapping; + } + + private Money payAllocation(PaymentAllocationType paymentAllocationType, LoanRepaymentScheduleInstallment currentInstallment, + LoanTransaction loanTransaction, Money transactionAmountUnprocessed, + LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, Balances balances) { + LocalDate transactionDate = loanTransaction.getTransactionDate(); + Money zero = transactionAmountUnprocessed.zero(); + return switch (paymentAllocationType.getAllocationType()) { + case PENALTY -> { + Money subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountUnprocessed); + balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().add(subPenaltyPortion)); + addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, zero, subPenaltyPortion); + yield subPenaltyPortion; + } + case FEE -> { + Money subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountUnprocessed); + balances.setAggregatedFeeChargesPortion(balances.getAggregatedFeeChargesPortion().add(subFeePortion)); + addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero, subFeePortion, zero); + yield subFeePortion; + } + case INTEREST -> { + Money subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountUnprocessed); + balances.setAggregatedInterestPortion(balances.getAggregatedInterestPortion().add(subInterestPortion)); + addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, subInterestPortion, zero, zero); + yield subInterestPortion; + } + case PRINCIPAL -> { + Money subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountUnprocessed); + balances.setAggregatedPrincipalPortion(balances.getAggregatedPrincipalPortion().add(subPrincipalPortion)); + addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, subPrincipalPortion, zero, zero, zero); + yield subPrincipalPortion; + } + }; + } + + private void addToTransactionMapping(LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping, + Money principalPortion, Money interestPortion, Money feePortion, Money penaltyPortion) { + BigDecimal aggregatedPenalty = ObjectUtils + .defaultIfNull(loanTransactionToRepaymentScheduleMapping.getPenaltyChargesPortion(), BigDecimal.ZERO) + .add(penaltyPortion.getAmount()); + BigDecimal aggregatedFee = ObjectUtils + .defaultIfNull(loanTransactionToRepaymentScheduleMapping.getFeeChargesPortion(), BigDecimal.ZERO) + .add(feePortion.getAmount()); + BigDecimal aggregatedInterest = ObjectUtils + .defaultIfNull(loanTransactionToRepaymentScheduleMapping.getInterestPortion(), BigDecimal.ZERO) + .add(interestPortion.getAmount()); + BigDecimal aggregatedPrincipal = ObjectUtils + .defaultIfNull(loanTransactionToRepaymentScheduleMapping.getPrincipalPortion(), BigDecimal.ZERO) + .add(principalPortion.getAmount()); + loanTransactionToRepaymentScheduleMapping.setComponents(aggregatedPrincipal, aggregatedInterest, aggregatedFee, aggregatedPenalty); + } + + private void handleOverpayment(Money overpaymentPortion, LoanTransaction loanTransaction) { + if (overpaymentPortion.isGreaterThanZero()) { + onLoanOverpayment(loanTransaction, overpaymentPortion); + loanTransaction.updateOverPayments(overpaymentPortion); + } + } + + @AllArgsConstructor + @Getter + @Setter + private static class Balances { + + private Money aggregatedPrincipalPortion; + private Money aggregatedFeeChargesPortion; + private Money aggregatedInterestPortion; + private Money aggregatedPenaltyChargesPortion; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.java index 7916e894d35..10f37270859 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor.java @@ -100,7 +100,7 @@ protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanR boolean ignoreDueDateCheck = false; boolean rerun = false; - List orderedLoanChargesByDueDate = charges.stream().filter(LoanCharge::isActive).filter(LoanCharge::isNotFullyPaid) + List orderedLoanChargesByDueDate = charges.stream().filter(LoanCharge::isActive).filter(LoanCharge::isChargePending) .filter(loanCharge -> loanCharge.getEffectiveDueDate() == null || !loanCharge.getEffectiveDueDate().isAfter(transactionDate)) .sorted(LoanChargeEffectiveDueDateComparator.INSTANCE).toList(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor.java index a16e7f15f62..804d62b8b41 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor.java @@ -100,7 +100,7 @@ protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanR boolean ignoreDueDateCheck = false; boolean rerun = false; - List orderedLoanChargesByDueDate = charges.stream().filter(LoanCharge::isActive).filter(LoanCharge::isNotFullyPaid) + List orderedLoanChargesByDueDate = charges.stream().filter(LoanCharge::isActive).filter(LoanCharge::isChargePending) .filter(loanCharge -> loanCharge.getEffectiveDueDate() == null || !loanCharge.getEffectiveDueDate().isAfter(transactionDate)) .sorted(LoanChargeEffectiveDueDateComparator.INSTANCE).toList(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java index 8229dbdddd6..dc1a94e729c 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/AbstractLoanScheduleGenerator.java @@ -2808,7 +2808,7 @@ public LoanRepaymentScheduleInstallment calculatePrepaymentAmount(final Monetary loanRepaymentScheduleTransactionProcessor, onDate, calculateTill); List loanTransactions = loan.retrieveListOfTransactionsPostDisbursementExcludeAccruals(); - loanRepaymentScheduleTransactionProcessor.reprocessLoanTranactions(loanApplicationTerms.getExpectedDisbursementDate(), + loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loanApplicationTerms.getExpectedDisbursementDate(), loanTransactions, currency, loanScheduleDTO.getInstallments(), loan.getActiveCharges()); Money feeCharges = Money.zero(currency); Money penaltyCharges = Money.zero(currency); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessorTest.java index f24a7d7bd35..6f3b8993298 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessorTest.java @@ -437,13 +437,13 @@ public void duePaymentOfPenaltyAsInAdvanceTransaction() { LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class); Mockito.when(loanCharge1.isActive()).thenReturn(true); - Mockito.when(loanCharge1.isNotFullyPaid()).thenReturn(true); + Mockito.when(loanCharge1.isChargePending()).thenReturn(true); Mockito.when(loanCharge1.getEffectiveDueDate()).thenReturn(transactionDate); Mockito.when(loanCharge1.isPenaltyCharge()).thenReturn(true); Mockito.when(loanCharge1.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(two); LoanCharge loanCharge2 = Mockito.mock(LoanCharge.class); Mockito.when(loanCharge2.isActive()).thenReturn(true); - Mockito.when(loanCharge2.isNotFullyPaid()).thenReturn(true); + Mockito.when(loanCharge2.isChargePending()).thenReturn(true); Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.plusDays(1)); Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); @@ -478,13 +478,13 @@ public void duePaymentOfPenaltyAndFeeAsInAdvanceTransaction() { LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class); Mockito.when(loanCharge1.isActive()).thenReturn(true); - Mockito.when(loanCharge1.isNotFullyPaid()).thenReturn(true); + Mockito.when(loanCharge1.isChargePending()).thenReturn(true); Mockito.when(loanCharge1.getEffectiveDueDate()).thenReturn(transactionDate); Mockito.when(loanCharge1.isPenaltyCharge()).thenReturn(true); Mockito.when(loanCharge1.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(two); LoanCharge loanCharge2 = Mockito.mock(LoanCharge.class); Mockito.when(loanCharge2.isActive()).thenReturn(true); - Mockito.when(loanCharge2.isNotFullyPaid()).thenReturn(true); + Mockito.when(loanCharge2.isChargePending()).thenReturn(true); Mockito.when(loanCharge2.isPenaltyCharge()).thenReturn(false); Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.minusDays(1)); Mockito.when(loanCharge2.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(one); @@ -521,13 +521,13 @@ public void duePaymentOfHigherPenaltyAndHigherFeeAsInAdvanceTransaction() { LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class); Mockito.when(loanCharge1.isActive()).thenReturn(true); - Mockito.when(loanCharge1.isNotFullyPaid()).thenReturn(true); + Mockito.when(loanCharge1.isChargePending()).thenReturn(true); Mockito.when(loanCharge1.getEffectiveDueDate()).thenReturn(transactionDate); Mockito.when(loanCharge1.isPenaltyCharge()).thenReturn(true); Mockito.when(loanCharge1.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(eleven); LoanCharge loanCharge2 = Mockito.mock(LoanCharge.class); Mockito.when(loanCharge2.isActive()).thenReturn(true); - Mockito.when(loanCharge2.isNotFullyPaid()).thenReturn(true); + Mockito.when(loanCharge2.isChargePending()).thenReturn(true); Mockito.when(loanCharge2.isPenaltyCharge()).thenReturn(false); Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.minusDays(1)); Mockito.when(loanCharge2.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(eleven); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessorTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessorTest.java index f8cfc49f381..f817bab0037 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessorTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessorTest.java @@ -405,13 +405,13 @@ public void duePaymentOfPenaltyAsInAdvanceTransaction() { LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class); Mockito.when(loanCharge1.isActive()).thenReturn(true); - Mockito.when(loanCharge1.isNotFullyPaid()).thenReturn(true); + Mockito.when(loanCharge1.isChargePending()).thenReturn(true); Mockito.when(loanCharge1.getEffectiveDueDate()).thenReturn(transactionDate); Mockito.when(loanCharge1.isPenaltyCharge()).thenReturn(true); Mockito.when(loanCharge1.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(two); LoanCharge loanCharge2 = Mockito.mock(LoanCharge.class); Mockito.when(loanCharge2.isActive()).thenReturn(true); - Mockito.when(loanCharge2.isNotFullyPaid()).thenReturn(true); + Mockito.when(loanCharge2.isChargePending()).thenReturn(true); Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.plusDays(1)); Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); @@ -445,13 +445,13 @@ public void duePaymentOfPenaltyAndFeeAsInAdvanceTransaction() { LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class); Mockito.when(loanCharge1.isActive()).thenReturn(true); - Mockito.when(loanCharge1.isNotFullyPaid()).thenReturn(true); + Mockito.when(loanCharge1.isChargePending()).thenReturn(true); Mockito.when(loanCharge1.getEffectiveDueDate()).thenReturn(transactionDate); Mockito.when(loanCharge1.isPenaltyCharge()).thenReturn(true); Mockito.when(loanCharge1.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(two); LoanCharge loanCharge2 = Mockito.mock(LoanCharge.class); Mockito.when(loanCharge2.isActive()).thenReturn(true); - Mockito.when(loanCharge2.isNotFullyPaid()).thenReturn(true); + Mockito.when(loanCharge2.isChargePending()).thenReturn(true); Mockito.when(loanCharge2.isPenaltyCharge()).thenReturn(false); Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.minusDays(1)); Mockito.when(loanCharge2.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(one); @@ -485,13 +485,13 @@ public void duePaymentOfHigherPenaltyAndHigherFeeAsInAdvanceTransaction() { LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class); Mockito.when(loanCharge1.isActive()).thenReturn(true); - Mockito.when(loanCharge1.isNotFullyPaid()).thenReturn(true); + Mockito.when(loanCharge1.isChargePending()).thenReturn(true); Mockito.when(loanCharge1.getEffectiveDueDate()).thenReturn(transactionDate); Mockito.when(loanCharge1.isPenaltyCharge()).thenReturn(true); Mockito.when(loanCharge1.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(eleven); LoanCharge loanCharge2 = Mockito.mock(LoanCharge.class); Mockito.when(loanCharge2.isActive()).thenReturn(true); - Mockito.when(loanCharge2.isNotFullyPaid()).thenReturn(true); + Mockito.when(loanCharge2.isChargePending()).thenReturn(true); Mockito.when(loanCharge2.isPenaltyCharge()).thenReturn(false); Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.minusDays(1)); Mockito.when(loanCharge2.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(eleven); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java new file mode 100644 index 00000000000..bcd55592bbf --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -0,0 +1,1809 @@ +/** + * 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 org.junit.jupiter.api.Assertions.assertEquals; +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.util.Arrays; +import java.util.List; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.fineract.client.models.AdvancedPaymentData; +import org.apache.fineract.client.models.BusinessDateRequest; +import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.PaymentAllocationOrder; +import org.apache.fineract.client.models.PostClientsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdRequest; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; +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.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.integrationtests.common.BusinessDateHelper; +import org.apache.fineract.integrationtests.common.ClientHelper; +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.loans.LoanProductTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; +import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +@ExtendWith(LoanTestLifecycleExtension.class) +public class AdvancedPaymentAllocationLoanRepaymentScheduleTest { + + private static final Logger LOG = LoggerFactory.getLogger(AdvancedPaymentAllocationLoanRepaymentScheduleTest.class); + private static final String DATETIME_PATTERN = "dd MMMM yyyy"; + private static ResponseSpecification responseSpec; + private static RequestSpecification requestSpec; + private static BusinessDateHelper businessDateHelper; + private static LoanTransactionHelper loanTransactionHelper; + private static AccountHelper accountHelper; + private static Integer commonLoanProductId; + private static PostClientsResponse client; + + @BeforeAll + public static void setup() { + Utils.initializeRESTAssured(); + requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + requestSpec.header("Fineract-Platform-TenantId", "default"); + responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec); + businessDateHelper = new BusinessDateHelper(); + accountHelper = new AccountHelper(requestSpec, responseSpec); + ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec); + + final Account assetAccount = accountHelper.createAssetAccount(); + final Account incomeAccount = accountHelper.createIncomeAccount(); + final Account expenseAccount = accountHelper.createExpenseAccount(); + final Account overpaymentAccount = accountHelper.createLiabilityAccount(); + + commonLoanProductId = createLoanProduct("500", "15", "4", assetAccount, incomeAccount, expenseAccount, overpaymentAccount); + client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()); + } + + // UC1: Simple payments + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment + // 3. Pay installments on due dates + + @Test + public void uc1() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 250.0, 250.0, 250.0, 250.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 2, 125.0, 125.0, 0.0, 250.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 125.0, 375.0, 125.0, 375.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 3, 125.0, 125.0, 0.0, 125.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 4, 125.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + // UC2: Overpayment1 + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment + // 3. Overpay 2nd installment + // 4. Overpay loan + + @Test + public void uc2() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(150.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 225.0, 275.0, 225.0, 275.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 25.0, 100.0, 25.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 2, 150.0, 150.0, 0.0, 225.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 100.0, 400.0, 100.0, 400.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 25.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 25.0, 100.0, 25.0, 0.0); + validateLoanTransaction(loanDetails, 3, 125.0, 125.0, 0.0, 100.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, 25.0); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 25.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 25.0, 0.0); + validateLoanTransaction(loanDetails, 4, 125.0, 100.0, 25.0, 0.0); + assertTrue(loanDetails.getStatus().getOverpaid()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + // UC3: Overpayment2 + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment + // 3. Overpay 2nd installment + + @Test + public void uc3() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeGoodwillCredit(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(150.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 225.0, 275.0, 225.0, 275.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 25.0, 100.0, 25.0, 0.0); + validateLoanTransaction(loanDetails, 2, 150.0, 150.0, 0.0, 225.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 100.0, 400.0, 100.0, 400.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 25.0, 100.0, 25.0, 0.0); + validateLoanTransaction(loanDetails, 3, 125.0, 125.0, 0.0, 100.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, 25.0); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 25.0, 0.0); + validateLoanTransaction(loanDetails, 4, 125.0, 100.0, 25.0, 0.0); + assertTrue(loanDetails.getStatus().getOverpaid()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + // UC4: Delinquent balance + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment - fails + // 3. Pay 1st installment - fails + // 4. Pay rest manually + + @Test + public void uc4() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), loanDetails.getTransactions().get(1).getId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("01 January 2023") + .transactionAmount(0.0).locale("en")); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(125.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), loanDetails.getTransactions().get(2).getId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023") + .transactionAmount(0.0).locale("en")); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("20 January 2023").locale("en").transactionAmount(100.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 400.0, 100.0, 400.0, 100.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 100.0, 25.0, 0.0, 100.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 3, 100.0, 100.0, 0.0, 400.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(40.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 360.0, 140.0, 360.0, 140.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 15.0, 110.0, 0.0, 15.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 4, 40.0, 40.0, 0.0, 360.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(360.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 5, 360.0, 360.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC5: Refund past due + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment - fails + // 3. Partial Merchant refund + // 4. Pay rest on time + @Test + public void uc5() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), loanDetails.getTransactions().get(1).getId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("01 January 2023") + .transactionAmount(0.0).locale("en")); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeMerchantIssuedRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("8 January 2023").locale("en").transactionAmount(300.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 200.0, 300.0, 200.0, 300.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 58.33, 66.67, 58.33, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 58.33, 66.67, 58.33, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 58.34, 66.66, 58.34, 0.0); + validateLoanTransaction(loanDetails, 2, 300.0, 300.0, 0.0, 200.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(66.67)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 133.33, 366.67, 133.33, 366.67, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 58.33, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 58.33, 66.67, 58.33, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 58.34, 66.66, 58.34, 0.0); + validateLoanTransaction(loanDetails, 3, 66.67, 66.67, 0.0, 133.33); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(66.67)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 66.66, 433.34, 66.66, 433.34, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 58.33, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 58.33, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 58.34, 66.66, 58.34, 0.0); + validateLoanTransaction(loanDetails, 4, 66.67, 66.67, 0.0, 66.66); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(66.66)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 58.33, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 58.33, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 58.34, 0.0); + validateLoanTransaction(loanDetails, 5, 66.66, 66.66, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC7: Refund & due reamortization + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment + // 3. Pay 1st installment on time + // 4. Merchant issued refund + // 4. Pay rest on time + @Test + public void uc7() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + + validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 250.0, 250.0, 250.0, 250.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 2, 125.0, 125.0, 0.0, 250.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeMerchantIssuedRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(200.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 50.0, 450.0, 50.0, 450.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 100.0, 25.0, 100.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 100.0, 25.0, 100.0, 0.0); + validateLoanTransaction(loanDetails, 3, 200, 200.0, 0.0, 50.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(25.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 25.0, 475.0, 25.0, 475.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 100.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 100.0, 25.0, 100.0, 0.0); + validateLoanTransaction(loanDetails, 4, 25.0, 25.0, 0.0, 25.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(25.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 100.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 100.0, 0.0); + validateLoanTransaction(loanDetails, 5, 25.0, 25.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC8: Refund after due & past due + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment - fails + // 3. Pay 1st installment on time - fails + // 4. Merchant issued refund next day + // 4. Pay rest on time + @Test + public void uc8() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), loanDetails.getTransactions().get(1).getId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("01 January 2023") + .transactionAmount(0.0).locale("en")); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(125.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), loanDetails.getTransactions().get(2).getId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023") + .transactionAmount(0.0).locale("en")); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeMerchantIssuedRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("17 January 2023").locale("en").transactionAmount(300.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 200.0, 300.0, 200.0, 300.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 25.0, 100.0, 25.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 25.0, 100.0, 25.0, 0.0); + validateLoanTransaction(loanDetails, 3, 300.0, 300.0, 0.0, 200.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(100.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 100.0, 400.0, 100.0, 400.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 25.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 25.0, 100.0, 25.0, 0.0); + validateLoanTransaction(loanDetails, 4, 100.0, 100.0, 0.0, 100.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(100.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 25.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 25.0, 0.0); + validateLoanTransaction(loanDetails, 5, 100.0, 100.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC9: Refund next installment + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment + // 3. Refund + // 4. Pay rest on time + @Test + public void uc9() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makePayoutRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("05 January 2023").locale("en").transactionAmount(200.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 175.0, 325.0, 175.0, 325.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 75.0, 50.0, 75.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 2, 200.0, 200.0, 0.0, 175.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(50.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 125.0, 375.0, 125.0, 375.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 75.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 3, 50.0, 50.0, 0.0, 125.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 75.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 4, 125.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC10: Refund PD and next installment + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment + // 3. 1st installment on time - fails + // 4. Payout refund issued refund next day + // 4. Pay rest on time + @Test + public void uc10() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(125.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), loanDetails.getTransactions().get(2).getId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023") + .transactionAmount(0.0).locale("en")); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makePayoutRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("17 January 2023").locale("en").transactionAmount(200.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 175.0, 325.0, 175.0, 325.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 75.0, 50.0, 75.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 3, 200.0, 200.0, 0.0, 175.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(50.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 125.0, 375.0, 125.0, 375.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 75.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 4, 50.0, 50.0, 0.0, 125.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 75.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 5, 125.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC11: Refund Past, pay in advance installments + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment - fails + // 3. Payout refund issued refund next day + // 4. Pay rest on time + @Test + public void uc11() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), loanDetails.getTransactions().get(1).getId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("01 January 2023") + .transactionAmount(0.0).locale("en")); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makePayoutRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 January 2023").locale("en").transactionAmount(400.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 100.0, 400.0, 100.0, 400.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 25.0, 100.0, 25.0, 0.0); + validateLoanTransaction(loanDetails, 2, 400.0, 400.0, 0.0, 100.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(100.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 25.0, 0.0); + validateLoanTransaction(loanDetails, 3, 100.0, 100.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC12: Refund last installment + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment + // 3. Goodwill credit in advance + // 4. Pay rest on time + @Test + public void uc12() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeGoodwillCredit(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("08 January 2023").locale("en").transactionAmount(200.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 175.0, 325.0, 175.0, 325.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 75.0, 50.0, 75.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0); + validateLoanTransaction(loanDetails, 2, 200.0, 200.0, 0.0, 175.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 50.0, 450.0, 50.0, 450.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 75.0, 50.0, 75.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0); + validateLoanTransaction(loanDetails, 3, 125.0, 125.0, 0.0, 50.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(50.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 75.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0); + validateLoanTransaction(loanDetails, 3, 125.0, 125.0, 0.0, 50.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC13: Due apply last installment + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment + // 3. Pay first installment on time - fails + // 4. Goodwill credit(due and in advance) + // 5. Pay rest on time + @Test + public void uc13() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(125.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), loanDetails.getTransactions().get(2).getId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023") + .transactionAmount(0.0).locale("en")); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeGoodwillCredit(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("18 January 2023").locale("en").transactionAmount(200.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 175.0, 325.0, 175.0, 325.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 75.0, 50.0, 75.0, 0.0); + validateLoanTransaction(loanDetails, 3, 200.0, 200.0, 0.0, 175.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 50.0, 450.0, 50.0, 450.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 75.0, 50.0, 75.0, 0.0); + validateLoanTransaction(loanDetails, 4, 125.0, 125.0, 0.0, 50.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(50.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 75.0, 0.0); + validateLoanTransaction(loanDetails, 5, 50.0, 50.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC14: Refund PD + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment + // 3. Pay first installment on time - fails + // 4. Refund (DP due and in advance) + // 5. Pay rest on time + @Test + public void uc14() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), loanDetails.getTransactions().get(1).getId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("01 January 2023") + .transactionAmount(0.0).locale("en")); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(125.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), loanDetails.getTransactions().get(2).getId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023") + .transactionAmount(0.0).locale("en")); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeMerchantIssuedRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("17 January 2023").locale("en").transactionAmount(200.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 300.0, 200.0, 300.0, 200.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 75.0, 50.0, 0.0, 75.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 3, 200.0, 200.0, 0.0, 300.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(125.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 175.0, 325.0, 175.0, 325.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 75.0, 50.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 4, 125.0, 125.0, 0.0, 175.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(175.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 50.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 5, 175.0, 175.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC15: Goodwill credit PD + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment + // 3. Pay first installment on time - fails + // 4. Refund (DP due and in advance) + // 5. Pay rest on time + @Test + public void uc15() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), loanDetails.getTransactions().get(1).getId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("01 January 2023") + .transactionAmount(0.0).locale("en")); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeGoodwillCredit(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 January 2023").locale("en").transactionAmount(200.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 300.0, 200.0, 300.0, 200.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 75.0, 50.0, 75.0, 0.0); + validateLoanTransaction(loanDetails, 2, 200.0, 200.0, 0.0, 300.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(125.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 175.0, 325.0, 175.0, 325.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 75.0, 50.0, 75.0, 0.0); + validateLoanTransaction(loanDetails, 3, 125.0, 125.0, 0.0, 175.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(125.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 50.0, 450.0, 50.0, 450.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 75.0, 50.0, 75.0, 0.0); + validateLoanTransaction(loanDetails, 4, 125.0, 125.0, 0.0, 50.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(50.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 75.0, 0.0); + validateLoanTransaction(loanDetails, 5, 50.0, 50.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC17a: Full refund with CBR + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment + // 3. Pay first installment on time + // 4. Full merchant issued refund + // 5. CBR + @Test + public void uc17a() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeMerchantIssuedRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("08 January 2023").locale("en").transactionAmount(500.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, 125.0); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0); + validateLoanTransaction(loanDetails, 2, 500.0, 375.0, 125.0, 0.0); + assertTrue(loanDetails.getStatus().getOverpaid()); + + loanTransactionHelper.makeCreditBalanceRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("09 January 2023").locale("en").transactionAmount(125.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0); + validateLoanTransaction(loanDetails, 3, 125.0, 0.0, 125.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC17b: Full refund with CBR + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment + // 3. Pay first installment on time + // 4. Full payout refund + // 5. CBR + @Test + public void uc17b() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makePayoutRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("08 January 2023").locale("en").transactionAmount(500.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, 125.0); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0); + validateLoanTransaction(loanDetails, 2, 500.0, 375.0, 125.0, 0.0); + assertTrue(loanDetails.getStatus().getOverpaid()); + + loanTransactionHelper.makeCreditBalanceRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("09 January 2023").locale("en").transactionAmount(125.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0); + validateLoanTransaction(loanDetails, 3, 125.0, 0.0, 125.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC17c: Full refund with CBR + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment + // 3. Pay first installment on time + // 4. Full Goodwill credit + // 5. CBR + @Test + public void uc17c() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeGoodwillCredit(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("08 January 2023").locale("en").transactionAmount(500.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, 125.0); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0); + validateLoanTransaction(loanDetails, 2, 500.0, 375.0, 125.0, 0.0); + assertTrue(loanDetails.getStatus().getOverpaid()); + + loanTransactionHelper.makeCreditBalanceRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("09 January 2023").locale("en").transactionAmount(125.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0); + validateLoanTransaction(loanDetails, 3, 125.0, 0.0, 125.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC18: Full refund with CBR (N+1) + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment + // 3. Pay everything on time + // 4. Full merchant issued refund (after maturity date) + // 5. CBR + @Test + public void uc18() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.20").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 375.0, 125.0, 375.0, 125.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 1, 125.0, 125.0, 0.0, 375.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 250.0, 250.0, 250.0, 250.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 2, 125.0, 125.0, 0.0, 250.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 125.0, 375.0, 125.0, 375.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 3, 125.0, 125.0, 0.0, 125.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 4, 125.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + + loanTransactionHelper.makeMerchantIssuedRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("18 February 2023").locale("en").transactionAmount(500.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, 500.0); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 5, 500.0, 0.0, 500.0, 0.0); + assertTrue(loanDetails.getStatus().getOverpaid()); + + loanTransactionHelper.makeCreditBalanceRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("20 February 2023").locale("en").transactionAmount(500.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 6, 500.0, 0.0, 500.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC24: Merchant issued credit reverse-replay + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment - fails + // 3. Merchant issued credit + // 4. Payments + // 5. CBR + // 6. Merchant issued credit - reversal + // 7. Payments + @Test + @Disabled("Till the CBR support got implemented") + public void uc24() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.20").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), loanDetails.getTransactions().get(1).getId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023") + .transactionAmount(0.0).locale("en")); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeMerchantIssuedRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("02 January 2023").locale("en").transactionAmount(400.0)); + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 100.0, 400.0, 100.0, 400.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 91.67, 33.33, 91.67, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 91.67, 33.33, 91.67, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 91.66, 33.34, 91.66, 0.0); + validateLoanTransaction(loanDetails, 2, 400.0, 400.0, 0.0, 100.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("04 January 2023").locale("en").transactionAmount(50.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 50.0, 450.0, 50.0, 450.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 108.34, 16.66, 108.34, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 91.66, 33.34, 91.66, 0.0); + validateLoanTransaction(loanDetails, 3, 50.0, 50.0, 0.0, 50.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023").locale("en").transactionAmount(125.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, 75.0); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0); + validateLoanTransaction(loanDetails, 4, 125.0, 50.0, 75.0, 0.0); + assertTrue(loanDetails.getStatus().getOverpaid()); + + loanTransactionHelper.makeCreditBalanceRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("18 January 2023").locale("en").transactionAmount(75.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 125.0, 0.0); + validateLoanTransaction(loanDetails, 5, 75.0, 0.0, 75.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + + loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), loanDetails.getTransactions().get(2).getId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("20 January 2023") + .transactionAmount(0.0).locale("en")); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 400.0, 100.0, 400.0, 100.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 100.0, 25.0, 0.0, 100.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023").locale("en").transactionAmount(275.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 125.0, 375.0, 125.0, 375.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 6, 275.0, 275.0, 0.0, 125.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("15 February 2023").locale("en").transactionAmount(125.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 0.0, 500.0, 0.0, 500.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 125.0, 0.0, 0.0, 0.0); + validateLoanTransaction(loanDetails, 5, 125.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + // UC25: Merchant issued credit reverse-replay with uneven balances + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Disburse the loan + // 2. Pay down payment - fails + // 3. Merchant issued credit + // 4. Payments + // 5. Merchant issued credit + @Test + public void uc25() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.02.20").dateFormat("yyyy.MM.dd").locale("en")); + + final PostLoansResponse loanResponse = applyForLoanApplication(client.getClientId(), commonLoanProductId, 500L, 60, 15, 4, 0, + "01 January 2023", "01 January 2023"); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(500)).dateFormat(DATETIME_PATTERN) + .approvedOnDate("01 January 2023").locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), + new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) + .transactionAmount(BigDecimal.valueOf(500.00)).locale("en")); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + loanTransactionHelper.reverseLoanTransaction(loanResponse.getLoanId(), loanDetails.getTransactions().get(1).getId(), + new PostLoansLoanIdTransactionsTransactionIdRequest().dateFormat(DATETIME_PATTERN).transactionDate("16 January 2023") + .transactionAmount(0.0).locale("en")); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 500.0, 0.0, 500.0, 0.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 0.0, 125.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 0.0, 125.0, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeMerchantIssuedRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("02 January 2023").locale("en").transactionAmount(400.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 100.0, 400.0, 100.0, 400.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 91.67, 33.33, 91.67, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 91.67, 33.33, 91.67, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 91.66, 33.34, 91.66, 0.0); + validateLoanTransaction(loanDetails, 2, 400.0, 400.0, 0.0, 100.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeLoanRepayment(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("04 January 2023").locale("en").transactionAmount(50.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 50.0, 450.0, 50.0, 450.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 108.34, 16.66, 108.34, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 91.66, 33.34, 91.66, 0.0); + validateLoanTransaction(loanDetails, 3, 50.0, 50.0, 0.0, 50.0); + assertTrue(loanDetails.getStatus().getActive()); + + loanTransactionHelper.makeMerchantIssuedRefund(loanResponse.getLoanId(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("06 January 2023").locale("en").transactionAmount(40.0)); + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 10.0, 490.0, 10.0, 490.0, null); + validateRepaymentPeriod(loanDetails, 1, 125.0, 125.0, 0.0, 0.0, 125.0); + validateRepaymentPeriod(loanDetails, 2, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, 125.0, 125.0, 0.0, 125.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, 125.0, 115.0, 10.0, 115.0, 0.0); + validateLoanTransaction(loanDetails, 4, 40.0, 40.0, 0.0, 10.0); + assertTrue(loanDetails.getStatus().getActive()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + } + } + + private static void validateLoanSummaryBalances(GetLoansLoanIdResponse loanDetails, Double totalOutstanding, Double totalRepayment, + Double principalOutstanding, Double principalPaid, Double totalOverpaid) { + assertEquals(totalOutstanding, loanDetails.getSummary().getTotalOutstanding()); + assertEquals(totalRepayment, loanDetails.getSummary().getTotalRepayment()); + assertEquals(principalOutstanding, loanDetails.getSummary().getPrincipalOutstanding()); + assertEquals(principalPaid, loanDetails.getSummary().getPrincipalPaid()); + assertEquals(totalOverpaid, loanDetails.getTotalOverpaid()); + } + + private static 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 static 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 static AdvancedPaymentData createPaymentAllocation(String transactionType, String futureInstallmentAllocationRule) { + AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData(); + advancedPaymentData.setTransactionType(transactionType); + advancedPaymentData.setFutureInstallmentAllocationRule(futureInstallmentAllocationRule); + + 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 static Integer createLoanProduct(final String principal, final String repaymentAfterEvery, final String numberOfRepayments, + final Account... accounts) { + AdvancedPaymentData defaultAllocation = createDefaultPaymentAllocation(); + AdvancedPaymentData goodwillCreditAllocation = createPaymentAllocation("GOODWILL_CREDIT", "LAST_INSTALLMENT"); + AdvancedPaymentData merchantIssuedRefundAllocation = createPaymentAllocation("MERCHANT_ISSUED_REFUND", "REAMORTIZATION"); + AdvancedPaymentData payoutRefundAllocation = createPaymentAllocation("PAYOUT_REFUND", "NEXT_INSTALLMENT"); + LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); + final String loanProductJSON = new LoanProductTestBuilder().withMinPrincipal(principal).withPrincipal(principal) + .withRepaymentTypeAsDays().withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments) + .withEnableDownPayment(true, "25", true).withinterestRatePerPeriod("0").withInterestRateFrequencyTypeAsMonths() + .withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) + .withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts) + .addAdvancedPaymentAllocation(defaultAllocation, goodwillCreditAllocation, merchantIssuedRefundAllocation, + payoutRefundAllocation) + .withDaysInMonth("30").withDaysInYear("365").withMoratorium("0", "0").build(null); + return loanTransactionHelper.getLoanProductId(loanProductJSON); + } + + private static void validateRepaymentPeriod(GetLoansLoanIdResponse loanDetails, int index, double principalDue, double principalPaid, + double principalOutstanding, double paidInAdvance, double paidLate) { + assertEquals(principalDue, loanDetails.getRepaymentSchedule().getPeriods().get(index).getPrincipalDue()); + assertEquals(principalPaid, loanDetails.getRepaymentSchedule().getPeriods().get(index).getPrincipalPaid()); + assertEquals(principalOutstanding, loanDetails.getRepaymentSchedule().getPeriods().get(index).getPrincipalOutstanding()); + assertEquals(paidInAdvance, loanDetails.getRepaymentSchedule().getPeriods().get(index).getTotalPaidInAdvanceForPeriod()); + assertEquals(paidLate, loanDetails.getRepaymentSchedule().getPeriods().get(index).getTotalPaidLateForPeriod()); + } + + private static PostLoansResponse applyForLoanApplication(final Long clientId, final Integer loanProductId, final Long principal, + final int loanTermFrequency, final int repaymentAfterEvery, final int numberOfRepayments, final int interestRate, + final String expectedDisbursementDate, final String submittedOnDate) { + LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); + return loanTransactionHelper.applyLoan(new PostLoansRequest().clientId(clientId).productId(loanProductId.longValue()) + .expectedDisbursementDate(expectedDisbursementDate).dateFormat(DATETIME_PATTERN) + .transactionProcessingStrategyCode(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY) + .locale("en").submittedOnDate(submittedOnDate).amortizationType(1).interestRatePerPeriod(interestRate) + .interestCalculationPeriodType(1).interestType(0).repaymentFrequencyType(0).repaymentEvery(repaymentAfterEvery) + .repaymentFrequencyType(0).numberOfRepayments(numberOfRepayments).loanTermFrequency(loanTermFrequency) + .loanTermFrequencyType(0).principal(BigDecimal.valueOf(principal)).loanType("individual")); + } + + private static void validateLoanTransaction(GetLoansLoanIdResponse loanDetails, int index, double transactionAmount, + double principalPortion, double overPaidPortion, double loanBalance) { + assertEquals(transactionAmount, loanDetails.getTransactions().get(index).getAmount()); + assertEquals(principalPortion, loanDetails.getTransactions().get(index).getPrincipalPortion()); + assertEquals(overPaidPortion, loanDetails.getTransactions().get(index).getOverpaymentPortion()); + assertEquals(loanBalance, loanDetails.getTransactions().get(index).getOutstandingLoanBalance()); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index ffbf4a39d80..0b02bc1db59 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -61,6 +61,8 @@ import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTemplateResponse; import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse; import org.apache.fineract.client.models.GetPaymentTypesResponse; +import org.apache.fineract.client.models.PostLoanProductsRequest; +import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse; import org.apache.fineract.client.models.PostLoansLoanIdChargesRequest; @@ -1906,6 +1908,10 @@ public PutLoanProductsProductIdResponse updateLoanProduct(Long id, PutLoanProduc return ok(fineract().loanProducts.updateLoanProduct(id, requestModifyLoan)); } + public PostLoanProductsResponse createLoanProduct(PostLoanProductsRequest request) { + return ok(fineract().loanProducts.createLoanProduct(request)); + } + public PostLoansLoanIdTransactionsResponse makeLoanDownPayment(String loanExternalId, PostLoansLoanIdTransactionsRequest request) { return ok(fineract().loanTransactions.executeLoanTransaction1(loanExternalId, request, "downPayment")); }