From d8882ac471ea85c60d98aa36d9b473b5fa3f2fce Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Mon, 7 Aug 2023 17:07:54 +0200 Subject: [PATCH] Revert "FINERACT-1905: Same day repayment + charge handling improvement" This reverts commit 15838e5406ecda2457d9310b01cc3de241a57389 Except LoanCharge and liquibase changes. Those are unrelated. not causing any issues, and better to leave them for compatibility --- .../portfolio/loanaccount/domain/Loan.java | 2 +- .../loanaccount/domain/LoanTransaction.java | 8 +- ...RepaymentScheduleTransactionProcessor.java | 6 +- ...RepaymentScheduleTransactionProcessor.java | 198 +++-- ...RepaymentScheduleTransactionProcessor.java | 198 +++-- .../domain/AbstractLoanScheduleGenerator.java | 2 +- ...ymentScheduleTransactionProcessorTest.java | 796 ++++++++++-------- ...ymentScheduleTransactionProcessorTest.java | 790 +++++++++-------- .../ClientLoanIntegrationTest.java | 339 +------- 9 files changed, 1140 insertions(+), 1199 deletions(-) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index 154c821a6..7d78c4a1a 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 @@ -5741,7 +5741,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final List copyTransactions = new ArrayList<>(); if (!allNonContraTransactionsPostDisbursement.isEmpty()) { for (LoanTransaction loanTransaction : allNonContraTransactionsPostDisbursement) { - copyTransactions.add(LoanTransaction.copyTransactionPropertiesForReprocessing(loanTransaction)); + copyTransactions.add(LoanTransaction.copyTransactionProperties(loanTransaction)); } loanRepaymentScheduleTransactionProcessor.handleTransaction(getDisbursementDate(), copyTransactions, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges()); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index ed78c84e7..9a2116c56 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -269,13 +269,11 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom { return new LoanTransaction(null, office, LoanTransactionType.REFUND, paymentDetail, amount.getAmount(), paymentDate, externalId); } - public static LoanTransaction copyTransactionPropertiesForReprocessing(final LoanTransaction loanTransaction) { - LoanTransaction loanTransactionCopy = new LoanTransaction(loanTransaction.loan, loanTransaction.office, loanTransaction.typeOf, - loanTransaction.dateOf, loanTransaction.amount, loanTransaction.principalPortion, loanTransaction.interestPortion, + public static LoanTransaction copyTransactionProperties(final LoanTransaction loanTransaction) { + return new LoanTransaction(loanTransaction.loan, loanTransaction.office, loanTransaction.typeOf, loanTransaction.dateOf, + loanTransaction.amount, loanTransaction.principalPortion, loanTransaction.interestPortion, loanTransaction.feeChargesPortion, loanTransaction.penaltyChargesPortion, loanTransaction.overPaymentPortion, loanTransaction.reversed, loanTransaction.paymentDetail, loanTransaction.externalId); - loanTransactionCopy.setCreatedDate(loanTransaction.getCreatedDateTime()); - return loanTransactionCopy; } public static LoanTransaction accrueLoanCharge(final Loan loan, final Office office, final Money amount, final LocalDate applyDate, 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 0f21e9093..039da3468 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 @@ -175,7 +175,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen * For existing transactions, check if the re-payment breakup (principal, interest, fees, penalties) * has changed.
**/ - final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionPropertiesForReprocessing(loanTransaction); + final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction); // Reset derived component of new loan transaction and // re-process transaction @@ -214,7 +214,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen private void recalculateChargeOffTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction loanTransaction, MonetaryCurrency currency, List installments) { - final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionPropertiesForReprocessing(loanTransaction); + final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction); newLoanTransaction.resetDerivedComponents(); // determine how much is outstanding total and breakdown for principal, interest and charges Money principalPortion = Money.zero(currency); @@ -280,7 +280,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen if (loanTransaction.getId() == null) { return; } - final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionPropertiesForReprocessing(loanTransaction); + final LoanTransaction newLoanTransaction = LoanTransaction.copyTransactionProperties(loanTransaction); List mergedList = getMergedTransactionList(transactionsToBeProcessed, changedTransactionDetail); Money overpaidAmount = calculateOverpaidAmount(loanTransaction, mergedList, installments, currency); 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 92ee4a598..f9e038652 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 @@ -61,81 +61,9 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio */ @Override protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, - final List installments, final LoanTransaction loanTransaction, - final Money transactionAmountUnprocessed, List transactionMappings, - Set charges) { - return doTransactionAmountAllocation(currentInstallment, loanTransaction, transactionAmountUnprocessed, transactionMappings, - charges); - } - - /** - * For late repayments - */ - @Override - protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, - final List installments, final LoanTransaction loanTransaction, - final Money transactionAmountUnprocessed, List transactionMappings, - Set charges) { - return doTransactionAmountAllocation(currentInstallment, loanTransaction, transactionAmountUnprocessed, transactionMappings, - charges); - } - - /** - * For normal on-time repayments - */ - @Override - protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, - final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed, + final List installments, final LoanTransaction loanTransaction, final Money paymentInAdvance, List transactionMappings, Set charges) { - return doTransactionAmountAllocation(currentInstallment, loanTransaction, transactionAmountUnprocessed, transactionMappings, - charges); - } - @Override - protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) { - // TODO - KW - dont do anything with loan over-payment for now - } - - @Override - protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, - final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed, - List transactionMappings) { - - final LocalDate transactionDate = loanTransaction.getTransactionDate(); - final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); - Money transactionAmountRemaining = transactionAmountUnprocessed; - Money principalPortion = Money.zero(currency); - Money interestPortion = Money.zero(currency); - Money feeChargesPortion = Money.zero(currency); - Money penaltyChargesPortion = Money.zero(currency); - - principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); - - if (transactionAmountRemaining.isGreaterThanZero()) { - interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); - } - - if (transactionAmountRemaining.isGreaterThanZero()) { - feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); - } - - if (transactionAmountRemaining.isGreaterThanZero()) { - penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); - } - loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); - if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) { - transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment, - principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion)); - } - return transactionAmountRemaining; - } - - private static Money doTransactionAmountAllocation(LoanRepaymentScheduleInstallment currentInstallment, LoanTransaction loanTransaction, - Money paymentInAdvance, List transactionMappings, Set charges) { final LocalDate transactionDate = loanTransaction.getTransactionDate(); final MonetaryCurrency currency = paymentInAdvance.getCurrency(); @@ -175,8 +103,6 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio List orderedLoanChargesByDueDate = charges.stream().filter(LoanCharge::isActive).filter(LoanCharge::isNotFullyPaid) .filter(loanCharge -> loanCharge.getEffectiveDueDate() == null || !loanCharge.getEffectiveDueDate().isAfter(transactionDate)) - .filter(loanCharge -> loanCharge.getCreatedDate().isEmpty() || loanTransaction.getCreatedDate().isEmpty() - || !loanCharge.getCreatedDate().get().isAfter(loanTransaction.getCreatedDate().get())) .sorted(LoanChargeEffectiveDueDateComparator.INSTANCE).toList(); Money calculatedPenaltyCharge = Money.zero(currency); Money calculatedFeeCharge = Money.zero(currency); @@ -239,4 +165,126 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio } return transactionAmountRemaining; } + + /** + * For late repayments + */ + @Override + protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final List installments, final LoanTransaction loanTransaction, + final Money transactionAmountUnprocessed, List transactionMappings, + Set charges) { + + return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed, + transactionMappings, charges); + } + + /** + * For normal on-time repayments + */ + @Override + protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed, + List transactionMappings, Set charges) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + + final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money principalPortion = Money.zero(currency); + Money interestPortion = Money.zero(currency); + Money feeChargesPortion = Money.zero(currency); + Money penaltyChargesPortion = Money.zero(currency); + + if (loanTransaction.isChargesWaiver()) { + penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate, + loanTransaction.getPenaltyChargesPortion(currency)); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + + feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate, + loanTransaction.getFeeChargesPortion(currency)); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + + } else if (loanTransaction.isInterestWaiver()) { + interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + } else if (loanTransaction.isChargePayment()) { + if (loanTransaction.isPenaltyPayment()) { + penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + } else { + feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + } + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + } else { + Money subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(subPenaltyPortion); + penaltyChargesPortion = penaltyChargesPortion.add(subPenaltyPortion); + + Money subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(subFeePortion); + feeChargesPortion = feeChargesPortion.add(subFeePortion); + + Money subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(subInterestPortion); + interestPortion = interestPortion.add(subInterestPortion); + + Money subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(subPrincipalPortion); + principalPortion = principalPortion.add(subPrincipalPortion); + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + } + if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) { + transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment, + principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion)); + } + return transactionAmountRemaining; + } + + @Override + protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) { + // TODO - KW - dont do anything with loan over-payment for now + } + + @Override + protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed, + List transactionMappings) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money principalPortion = Money.zero(currency); + Money interestPortion = Money.zero(currency); + Money feeChargesPortion = Money.zero(currency); + Money penaltyChargesPortion = Money.zero(currency); + + principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + + if (transactionAmountRemaining.isGreaterThanZero()) { + interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + } + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) { + transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment, + principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion)); + } + return transactionAmountRemaining; + } } 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 1c1abc011..6a8bf1e6a 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 @@ -61,81 +61,8 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio */ @Override protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, - final List installments, final LoanTransaction loanTransaction, - final Money transactionAmountUnprocessed, List transactionMappings, - Set charges) { - return doTransactionAmountAllocation(currentInstallment, loanTransaction, transactionAmountUnprocessed, transactionMappings, - charges); - } - - /** - * For late repayments - */ - @Override - protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, - final List installments, final LoanTransaction loanTransaction, - final Money transactionAmountUnprocessed, List transactionMappings, - Set charges) { - return doTransactionAmountAllocation(currentInstallment, loanTransaction, transactionAmountUnprocessed, transactionMappings, - charges); - } - - /** - * For normal on-time repayments - */ - @Override - protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, - final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed, + final List installments, final LoanTransaction loanTransaction, final Money inAdvancePayment, List transactionMappings, Set charges) { - return doTransactionAmountAllocation(currentInstallment, loanTransaction, transactionAmountUnprocessed, transactionMappings, - charges); - } - - @Override - protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) { - // TODO - KW - dont do anything with loan over-payment for now - } - - @Override - protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, - final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed, - List transactionMappings) { - - final LocalDate transactionDate = loanTransaction.getTransactionDate(); - final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); - Money transactionAmountRemaining = transactionAmountUnprocessed; - Money principalPortion = Money.zero(currency); - Money interestPortion = Money.zero(currency); - Money feeChargesPortion = Money.zero(currency); - Money penaltyChargesPortion = Money.zero(currency); - - principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); - - if (transactionAmountRemaining.isGreaterThanZero()) { - interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); - } - - if (transactionAmountRemaining.isGreaterThanZero()) { - feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); - } - - if (transactionAmountRemaining.isGreaterThanZero()) { - penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); - } - loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); - if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) { - transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment, - principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion)); - } - return transactionAmountRemaining; - } - - private static Money doTransactionAmountAllocation(LoanRepaymentScheduleInstallment currentInstallment, LoanTransaction loanTransaction, - Money inAdvancePayment, List transactionMappings, Set charges) { final LocalDate transactionDate = loanTransaction.getTransactionDate(); final MonetaryCurrency currency = inAdvancePayment.getCurrency(); @@ -176,8 +103,6 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio List orderedLoanChargesByDueDate = charges.stream().filter(LoanCharge::isActive).filter(LoanCharge::isNotFullyPaid) .filter(loanCharge -> loanCharge.getEffectiveDueDate() == null || !loanCharge.getEffectiveDueDate().isAfter(transactionDate)) - .filter(loanCharge -> loanCharge.getCreatedDate().isEmpty() || loanTransaction.getCreatedDate().isEmpty() - || loanCharge.getCreatedDate().get().isBefore(loanTransaction.getCreatedDate().get())) .sorted(LoanChargeEffectiveDueDateComparator.INSTANCE).toList(); Money calculatedPenaltyCharge = Money.zero(currency); Money calculatedFeeCharge = Money.zero(currency); @@ -243,4 +168,125 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio } return transactionAmountRemaining; } + + /** + * For late repayments + */ + @Override + protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final List installments, final LoanTransaction loanTransaction, final Money latePayment, + List transactionMappings, Set charges) { + + return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, latePayment, transactionMappings, + charges); + } + + /** + * For normal on-time repayments + */ + @Override + protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money onTimePayment, + List transactionMappings, Set charges) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + + final MonetaryCurrency currency = onTimePayment.getCurrency(); + Money transactionAmountRemaining = onTimePayment; + Money principalPortion = Money.zero(currency); + Money interestPortion = Money.zero(currency); + Money feeChargesPortion = Money.zero(currency); + Money penaltyChargesPortion = Money.zero(currency); + + if (loanTransaction.isChargesWaiver()) { + penaltyChargesPortion = currentInstallment.waivePenaltyChargesComponent(transactionDate, + loanTransaction.getPenaltyChargesPortion(currency)); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + + feeChargesPortion = currentInstallment.waiveFeeChargesComponent(transactionDate, + loanTransaction.getFeeChargesPortion(currency)); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + + } else if (loanTransaction.isInterestWaiver()) { + interestPortion = currentInstallment.waiveInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + } else if (loanTransaction.isChargePayment()) { + if (loanTransaction.isPenaltyPayment()) { + penaltyChargesPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + } else { + feeChargesPortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + } + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + } else { + Money subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(subPenaltyPortion); + penaltyChargesPortion = penaltyChargesPortion.add(subPenaltyPortion); + + Money subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(subInterestPortion); + interestPortion = interestPortion.add(subInterestPortion); + + Money subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(subPrincipalPortion); + principalPortion = principalPortion.add(subPrincipalPortion); + + Money subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(subFeePortion); + feeChargesPortion = feeChargesPortion.add(subFeePortion); + + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + } + if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) { + transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment, + principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion)); + } + return transactionAmountRemaining; + } + + @Override + protected void onLoanOverpayment(final LoanTransaction loanTransaction, final Money loanOverPaymentAmount) { + // TODO - KW - dont do anything with loan over-payment for now + } + + @Override + protected Money handleRefundTransactionPaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, + final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed, + List transactionMappings) { + + final LocalDate transactionDate = loanTransaction.getTransactionDate(); + final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); + Money transactionAmountRemaining = transactionAmountUnprocessed; + Money principalPortion = Money.zero(currency); + Money interestPortion = Money.zero(currency); + Money feeChargesPortion = Money.zero(currency); + Money penaltyChargesPortion = Money.zero(currency); + + principalPortion = currentInstallment.unpayPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(principalPortion); + + if (transactionAmountRemaining.isGreaterThanZero()) { + interestPortion = currentInstallment.unpayInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(interestPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + feeChargesPortion = currentInstallment.unpayFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(feeChargesPortion); + } + + if (transactionAmountRemaining.isGreaterThanZero()) { + penaltyChargesPortion = currentInstallment.unpayPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(penaltyChargesPortion); + } + loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); + if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) { + transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction, currentInstallment, + principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion)); + } + return transactionAmountRemaining; + } } 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 ca1db29b7..f6193dfe2 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 @@ -2098,7 +2098,7 @@ public abstract class AbstractLoanScheduleGenerator implements LoanScheduleGener for (LoanTransaction loanTransaction : transactions) { if (loanTransaction.isPaymentTransaction()) { recalculationDetails.add(new RecalculationDetail(loanTransaction.getTransactionDate(), - LoanTransaction.copyTransactionPropertiesForReprocessing(loanTransaction))); + LoanTransaction.copyTransactionProperties(loanTransaction))); } } final boolean applyInterestRecalculation = loanApplicationTerms.isInterestRecalculationEnabled(); 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 94b83af34..f24a7d7bd 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 @@ -18,17 +18,15 @@ */ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.refEq; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; @@ -45,7 +43,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -66,12 +63,15 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio private final LocalDate firstInstallmentDueDate = LocalDate.of(2023, 7, 31); private final LocalDate lateDate = firstInstallmentDueDate.plusDays(1); private final Money zero = Money.zero(MONETARY_CURRENCY); - private final Money one = Money.of(MONETARY_CURRENCY, BigDecimal.ONE); + private final Money one = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(1)); private final Money two = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(2)); private final Money three = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(3)); private final Money four = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(4)); private final Money five = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(5)); private final Money six = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(6)); + private final Money seven = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(7)); + private final Money eight = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(8)); + private final Money nine = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(9)); private final Money ten = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(10)); private final Money eleven = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(11)); private DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor underTest; @@ -111,12 +111,28 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // Full amount as this is the last of due and first of in advance, but only 5 is outstanding for this + // installment, so 5 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 5, but no outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(five)); + // In advance with value of 5, but no outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(five)); + // In advance with value of 5, but no outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(five)); + // Full amount as this is the last of due and first of in advance, but 0 is outstanding for this installment, so + // 5 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(five)); // Principal 5, interest 0, fee 0, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(five), refEq(zero), refEq(zero), refEq(zero)); } @@ -126,91 +142,160 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.valueOf(5L), BigDecimal.ZERO, BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(5L), BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // Full amount as this is the last of due and first of in advance, but 0 is outstanding for this installment, so + // 10 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 10, but no outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 5, but no outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 5, but only 5 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(ten)); + // Full amount as this is the last of due and first of in advance, but 0 is outstanding for this installment, so + // 5 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(five)); // Principal 0, interest 5, fee 0, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(five), refEq(zero), refEq(zero)); } @Test public void inAdvancePaymentOfFee() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, false, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.ZERO, BigDecimal.valueOf(5L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // Full amount as this is the last of due and first of in advance, but no outstanding + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 10, but no outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 10, but no only 5 is outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 5, but no outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(five)); + // Full amount as this is the last of due and first of in advance, but 0 is outstanding for this installment, so + // 5 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(five)); // Principal 0, interest 0, fee 5, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(five), refEq(zero)); } @Test public void inAdvancePaymentOfPenalty() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, true, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // Full amount as this is the last of due and first of in advance, but no outstanding + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 10, but only 5 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 5, but no outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(five)); + // In advance with value of 5, but no outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(five)); + // Full amount as this is the last of due and first of in advance, but 0 is outstanding for this installment, so + // 5 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(five)); // Principal 0, interest 0, fee 0, penalty 5 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(zero), refEq(five)); } @Test public void inAdvancePaymentOfPrincipalAndPenalty() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, true, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(2)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // Full amount as this is the last of due and first of in advance, but only 5 is outstanding for this + // installment, so 5 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 5, but no outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(five)); + // In advance with value of 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(zero)); + // Full amount as this is the last of due and first of in advance, but 0 is outstanding for this installment, so + // 0 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(zero)); // Principal 5, interest 0, fee 0, penalty 5 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(five), refEq(zero), refEq(zero), refEq(five)); } @Test public void inAdvancePaymentOfPrincipalAndPenaltyAndFee() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, true, six, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(2L), BigDecimal.valueOf(6L), false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(2L), BigDecimal.valueOf(6L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // Full amount as this is the last of due and first of in advance, but only 3 is outstanding for this + // installment, so 7 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 7, but only 6 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(seven)); + // In advance with value of 1, and 2 is outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(one)); + // In advance with value of 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(zero)); + // Full amount as this is the last of due and first of in advance, but 0 is outstanding for this installment, so + // 0 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(zero)); // Principal 3, interest 0, fee 1, penalty 6 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(zero), refEq(one), refEq(six)); } @Test public void inAdvancePaymentOfPrincipalAndPenaltyAndFeeAndInterest() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -220,15 +305,29 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // Full amount as this is the last of due and first of in advance, but only 3 is outstanding for this + // installment, so 7 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 7, but only 4 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(seven)); + // In advance with value of 3, but only 2 is outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(three)); + // In advance with value of 1, and 2 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(one)); + // Full amount as this is the last of due and first of in advance, but 0 is outstanding for this installment, so + // 0 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(zero)); // Principal 3, interest 1, fee 2, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(one), refEq(two), refEq(four)); } @Test public void inAdvancePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPartialPrincipal() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(2)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -238,15 +337,19 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // Full amount as this is the last of due and first of in advance, but 3 is outstanding for this installment + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(two)); // Principal 2, interest 0, fee 0, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(zero), refEq(zero), refEq(zero)); } @Test public void inAdvancePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPrincipalAndPartialPenalty() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(4)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -256,15 +359,24 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(2)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // Full amount as this is the last of due and first of in advance, but only 3 is outstanding for this + // installment, so 1 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(four)); + // In advance with value of 1, and 4 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(one)); + // In advance with value of 0, and 2 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(zero)); // Principal 3, interest 0, fee 0, penalty 1 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(zero), refEq(zero), refEq(one)); } @Test public void inAdvancePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPrincipalAndPenaltyAndPartialFee() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(8)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -274,15 +386,26 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // Full amount as this is the last of due and first of in advance, but only 3 is outstanding for this + // installment, so 5 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(eight)); + // In advance with value of 5, but only 4 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(five)); + // In advance with value of 1, and 2 is outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(one)); + // In advance with value of 0, and 2 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(zero)); // Principal 3, interest 0, fee 1, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(zero), refEq(one), refEq(four)); } @Test public void inAdvancePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPrincipalAndPenaltyAndFeeAndPartialInterest() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -292,14 +415,37 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // Full amount as this is the last of due and first of in advance, but only 3 is outstanding for this + // installment, so 5 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 7, but only 4 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(seven)); + // In advance with value of 3, but only 2 is outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(three)); + // In advance with value of 1, and 2 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(one)); // Principal 3, interest 1, fee 2, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(one), refEq(two), refEq(four)); } @Test public void duePaymentOfPenaltyAsInAdvanceTransaction() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, two, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, two, null); + + LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class); + Mockito.when(loanCharge1.isActive()).thenReturn(true); + Mockito.when(loanCharge1.isNotFullyPaid()).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.getEffectiveDueDate()).thenReturn(transactionDate.plusDays(1)); + Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito @@ -310,14 +456,39 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated two + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(two)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // With value of 8 as this is the last of due and first of in advance, but only 3 is outstanding for this + // installment, so 5 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(eight)); + // In advance with value of 5, and 2 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(five)); + // In advance with value of 3, but only 2 is outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(three)); + // In advance with value of 1, and 2 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(one)); // Principal 3, interest 1, fee 2, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(one), refEq(two), refEq(four)); } @Test public void duePaymentOfPenaltyAndFeeAsInAdvanceTransaction() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, two, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate.minusDays(1), false, one, null); + + LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class); + Mockito.when(loanCharge1.isActive()).thenReturn(true); + Mockito.when(loanCharge1.isNotFullyPaid()).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.isPenaltyCharge()).thenReturn(false); + Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.minusDays(1)); + Mockito.when(loanCharge2.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(one); + Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito @@ -328,14 +499,39 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated two + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(two)); + // Calculated one + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(one)); + // With value of 7 as this is the last of due and first of in advance, but only 3 is outstanding for this + // installment, so 4 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(seven)); + // In advance with value of 4, and 2 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(four)); + // In advance with value of 2, but only 1 is outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(two)); + // In advance with value of 1, and 2 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(one)); // Principal 3, interest 1, fee 2, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(one), refEq(two), refEq(four)); } @Test public void duePaymentOfHigherPenaltyAndHigherFeeAsInAdvanceTransaction() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, eleven, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate.minusDays(1), false, eleven, null); + + LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class); + Mockito.when(loanCharge1.isActive()).thenReturn(true); + Mockito.when(loanCharge1.isNotFullyPaid()).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.isPenaltyCharge()).thenReturn(false); + Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.minusDays(1)); + Mockito.when(loanCharge2.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(eleven); + Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito @@ -346,96 +542,39 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated eleven, overridden by Unprocessed (ten) + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(ten)); + // Calculated eleven, overridden by Unprocessed (six) + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(six)); + // In advance with value of 4, and 2 is outstanding of principal + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(four)); + // In advance with value of 2, and 2 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(two)); + // Principal 2, interest 2, fee 2, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(two), refEq(two), refEq(four)); } - @Test - public void inAdvancePaymentOfPrincipalAndPenaltyWherePenaltyIsSameDayButLaterThanRepayment() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, one, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, true, three, OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = four; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(4L), false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); - Mockito.when(loanTransaction.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 1, ZoneOffset.UTC))); - underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 3, interest 0, fee 0, penalty 1 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(zero), refEq(zero), refEq(one)); - } - - @Test - public void inAdvancePaymentOfPrincipalAndPenaltyWherePenaltyIsSameDayButEarlierThanRepayment() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, one, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, true, three, OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = four; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(4L), false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); - Mockito.when(loanTransaction.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 3, ZoneOffset.UTC))); - underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 0, interest 0, fee 0, penalty 4 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(zero), refEq(four)); - } - - @Test - public void inAdvancePaymentOfPrincipalAndPenaltyWherePenaltyIsSameDayButNoChargeCreatedDate() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, one, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, true, three, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = five; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(4L), false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); - underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 1, interest 0, fee 0, penalty 4 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(one), refEq(zero), refEq(zero), refEq(four)); - } - - @Test - public void inAdvancePaymentOfPrincipalAndPenaltyWherePenaltyIsSameDayButNoTransactionCreatedDate() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, one, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, true, three, OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = five; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(4L), false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); - underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 1, interest 0, fee 0, penalty 4 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(one), refEq(zero), refEq(zero), refEq(four)); - } - // ON TIME @Test public void onTimePaymentOfPrincipal() { Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(ten)); // Principal 5, interest 0, fee 0, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(five), refEq(zero), refEq(zero), refEq(zero)); } @@ -444,91 +583,119 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio public void onTimePaymentOfInterest() { Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.valueOf(5L), BigDecimal.ZERO, BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(5L), BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(five)); // Principal 0, interest 5, fee 0, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(five), refEq(zero), refEq(zero)); } @Test public void onTimePaymentOfFee() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentToDate, false, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.ZERO, BigDecimal.valueOf(5L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(five)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(five)); // Principal 0, interest 0, fee 5, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(five), refEq(zero)); } @Test public void onTimePaymentOfPenalty() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentToDate, true, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(five)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(five)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(five)); // Principal 0, interest 0, fee 0, penalty 5 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(zero), refEq(five)); } @Test public void onTimePaymentOfPrincipalAndPenalty() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentToDate, true, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(five)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(five)); + // Unprocessed: 5, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(five)); // Principal 5, interest 0, fee 0, penalty 5 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(five), refEq(zero), refEq(zero), refEq(five)); } @Test public void onTimePaymentOfPrincipalAndPenaltyAndFee() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentToDate, true, six, null); - LoanCharge loanCharge2 = createLoanCharge(firstInstallmentToDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(2L), BigDecimal.valueOf(6L), false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(2L), BigDecimal.valueOf(6L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 6 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 4, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(four)); + // Unprocessed: 2, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(two)); + // Unprocessed: 2, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(two)); // Principal 2, interest 0, fee 2, penalty 6 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(zero), refEq(two), refEq(six)); } @Test public void onTimePaymentOfPrincipalAndPenaltyAndFeeAndInterest() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentToDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(firstInstallmentToDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -538,16 +705,21 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 6, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(six)); + // Unprocessed: 4, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(four)); + // Unprocessed: 2, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(two)); // Principal 2, interest 2, fee 2, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(two), refEq(two), refEq(four)); } @Test public void onTimePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPartialPenalty() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentToDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(firstInstallmentToDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.ONE); + Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(1)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), BigDecimal.valueOf(4L), false, null, BigDecimal.ZERO)); @@ -556,15 +728,20 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 1, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(one)); + // Unprocessed: 0, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(zero)); + // Unprocessed: 0, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(zero)); + // Unprocessed: 0, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(zero)); // Principal 0, interest 0, fee 0, penalty 1 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(zero), refEq(one)); } @Test public void onTimePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPenaltyAndPartialFee() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentToDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(firstInstallmentToDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(5)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -574,15 +751,20 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 5, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(five)); + // Unprocessed: 1, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(one)); + // Unprocessed: 0, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(zero)); + // Unprocessed: 0, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(zero)); // Principal 0, interest 0, fee 1, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(one), refEq(four)); } @Test public void onTimePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPenaltyAndFeeAndPartialInterest() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentToDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(firstInstallmentToDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(7)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -592,15 +774,20 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 7, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(seven)); + // Unprocessed: 3, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(three)); + // Unprocessed: 1, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(one)); + // Unprocessed: 0, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(zero)); // Principal 0, interest 1, fee 2, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(one), refEq(two), refEq(four)); } @Test public void onTimePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPenaltyAndFeeAndInterestAndPartialPrincipal() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentToDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(firstInstallmentToDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -610,99 +797,38 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 6, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(six)); + // Unprocessed: 4, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(four)); + // Unprocessed: 2, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(two)); // Principal 2, interest 2, fee 2, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(two), refEq(two), refEq(four)); } - @Test - public void onTimePaymentOfPrincipalAndFeeWhereFeeIsSameDayButEarlierThanRepayment() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentToDate, false, one, null); - LoanCharge loanCharge2 = createLoanCharge(firstInstallmentToDate, false, three, - OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = four; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentToDate, ExternalId.empty())); - Mockito.when(loanTransaction.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 3, ZoneOffset.UTC))); - underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, - charges); - - // Principal 0, interest 0, fee 4, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(four), refEq(zero)); - } - - @Test - public void onTimePaymentOfPrincipalAndFeeWhereFeeIsSameDayButLaterThanRepayment() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentToDate, false, one, null); - LoanCharge loanCharge2 = createLoanCharge(firstInstallmentToDate, false, three, - OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = four; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentToDate, ExternalId.empty())); - Mockito.when(loanTransaction.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 1, ZoneOffset.UTC))); - underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, - charges); - - // Principal 3, interest 0, fee 1, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(zero), refEq(one), refEq(zero)); - } - - @Test - public void onTimePaymentOfPrincipalAndFeeWhereFeeIsSameDayButNoChargeCreatedDate() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentToDate, false, one, null); - LoanCharge loanCharge2 = createLoanCharge(firstInstallmentToDate, false, three, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = five; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentToDate, ExternalId.empty())); - underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, - charges); - - // Principal 1, interest 0, fee 4, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(one), refEq(zero), refEq(four), refEq(zero)); - } - - @Test - public void onTimePaymentOfPrincipalAndFeeWhereFeeIsSameDayButNoTransactionCreatedDate() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentToDate, false, one, null); - LoanCharge loanCharge2 = createLoanCharge(firstInstallmentToDate, false, three, - OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = five; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentToDate, ExternalId.empty())); - underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, - charges); - - // Principal 1, interest 0, fee 4, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(one), refEq(zero), refEq(four), refEq(zero)); - } - // LATE @Test public void latePaymentOfPrincipal() { Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(ten)); // Principal 5, interest 0, fee 0, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(five), refEq(zero), refEq(zero), refEq(zero)); } @@ -711,91 +837,119 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio public void latePaymentOfInterest() { Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.valueOf(5L), BigDecimal.ZERO, BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(5L), BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(five)); // Principal 0, interest 5, fee 0, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(five), refEq(zero), refEq(zero)); } @Test public void latePaymentOfFee() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, false, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.ZERO, BigDecimal.valueOf(5L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(five)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(five)); // Principal 0, interest 0, fee 5, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(five), refEq(zero)); } @Test public void latePaymentOfPenalty() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(five)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(five)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(five)); // Principal 0, interest 0, fee 0, penalty 5 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(zero), refEq(five)); } @Test public void latePaymentOfPrincipalAndPenalty() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(five)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(five)); + // Unprocessed: 5, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(five)); // Principal 5, interest 0, fee 0, penalty 5 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(five), refEq(zero), refEq(zero), refEq(five)); } @Test public void latePaymentOfPrincipalAndPenaltyAndFee() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, six, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(2L), BigDecimal.valueOf(6L), false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(2L), BigDecimal.valueOf(6L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 6 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 4, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(four)); + // Unprocessed: 2, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(two)); + // Unprocessed: 2, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(two)); // Principal 2, interest 0, fee 2, penalty 6 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(zero), refEq(two), refEq(six)); } @Test public void latePaymentOfPrincipalAndPenaltyAndFeeAndInterest() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -805,16 +959,21 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 6, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(six)); + // Unprocessed: 4, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(four)); + // Unprocessed: 2, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(two)); // Principal 2, interest 2, fee 2, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(two), refEq(two), refEq(four)); } @Test public void latePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPartialPenalty() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.ONE); + Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(1)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), BigDecimal.valueOf(4L), false, null, BigDecimal.ZERO)); @@ -823,15 +982,20 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 1, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(one)); + // Unprocessed: 0, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(zero)); + // Unprocessed: 0, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(zero)); + // Unprocessed: 0, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(zero)); // Principal 0, interest 0, fee 0, penalty 1 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(zero), refEq(one)); } @Test public void latePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPenaltyAndPartialFee() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(5)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -841,15 +1005,20 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 5, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(five)); + // Unprocessed: 1, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(one)); + // Unprocessed: 0, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(zero)); + // Unprocessed: 0, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(zero)); // Principal 0, interest 0, fee 1, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(one), refEq(four)); } @Test public void latePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPenaltyAndFeeAndPartialInterest() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(7)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -859,15 +1028,20 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 7, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(seven)); + // Unprocessed: 3, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(three)); + // Unprocessed: 1, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(one)); + // Unprocessed: 0, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(zero)); // Principal 0, interest 1, fee 2, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(one), refEq(two), refEq(four)); } @Test public void latePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPenaltyAndFeeAndInterestAndPartialPrincipal() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -877,95 +1051,15 @@ public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Unprocessed: 10, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 6, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(six)); + // Unprocessed: 4, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(four)); + // Unprocessed: 2, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(two)); // Principal 2, interest 2, fee 2, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(two), refEq(two), refEq(four)); } - - @Test - public void latePaymentOfPrincipalAndFeeWhereFeeIsSameDayButEarlierThanRepayment() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, false, one, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, three, OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = four; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); - Mockito.when(loanTransaction.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 3, ZoneOffset.UTC))); - underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 0, interest 0, fee 4, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(four), refEq(zero)); - } - - @Test - public void latePaymentOfPrincipalAndFeeWhereFeeIsSameDayButLaterThanRepayment() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, false, one, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, three, OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = four; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); - Mockito.when(loanTransaction.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 1, ZoneOffset.UTC))); - underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 3, interest 0, fee 1, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(zero), refEq(one), refEq(zero)); - } - - @Test - public void latePaymentOfPrincipalAndFeeWhereFeeIsSameDayButNoChargeCreatedDate() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, false, one, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, three, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = five; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); - underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 1, interest 0, fee 4, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(one), refEq(zero), refEq(four), refEq(zero)); - } - - @Test - public void latePaymentOfPrincipalAndFeeWhereFeeIsSameDayButNoTransactionCreatedDate() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, false, one, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, three, OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = five; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); - underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 1, interest 0, fee 4, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(one), refEq(zero), refEq(four), refEq(zero)); - } - - @NotNull - private LoanCharge createLoanCharge(LocalDate dueDate, boolean isPenalty, Money amount, OffsetDateTime createdDate) { - LoanCharge loanCharge = Mockito.mock(LoanCharge.class); - Mockito.lenient().when(loanCharge.isActive()).thenReturn(true); - Mockito.lenient().when(loanCharge.isNotFullyPaid()).thenReturn(true); - Mockito.lenient().when(loanCharge.getEffectiveDueDate()).thenReturn(dueDate); - Mockito.lenient().when(loanCharge.isPenaltyCharge()).thenReturn(isPenalty); - if (createdDate != null) { - Mockito.lenient().when(loanCharge.getCreatedDate()).thenReturn(Optional.of(createdDate)); - } - Mockito.lenient().when(loanCharge.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(amount); - return loanCharge; - } } 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 bc63ce162..f8cfc49f3 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 @@ -18,17 +18,15 @@ */ package org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.ArgumentMatchers.refEq; import java.math.BigDecimal; import java.math.RoundingMode; import java.time.LocalDate; -import java.time.OffsetDateTime; -import java.time.ZoneOffset; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.Set; import java.util.stream.Stream; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; @@ -45,7 +43,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; -import org.jetbrains.annotations.NotNull; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.BeforeEach; @@ -72,6 +69,9 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio private final Money four = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(4)); private final Money five = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(5)); private final Money six = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(6)); + private final Money seven = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(7)); + private final Money eight = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(8)); + private final Money nine = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(9)); private final Money ten = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(10)); private final Money eleven = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(11)); @@ -112,12 +112,24 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // In advance with value of 5, but no outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 5, but no outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 5, but 0 is outstanding for this installment, so 5 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 5, but no outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(five)); // Principal 5, interest 0, fee 0, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(five), refEq(zero), refEq(zero), refEq(zero)); } @@ -127,91 +139,142 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.valueOf(5L), BigDecimal.ZERO, BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(5L), BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // In advance with value of 10, but no outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 10, but only 5 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 5, but 0 is outstanding for this installment, so 5 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(five)); + // In advance with value of 5, but no outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(five)); // Principal 0, interest 5, fee 0, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(five), refEq(zero), refEq(zero)); } @Test public void inAdvancePaymentOfFee() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, false, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.ZERO, BigDecimal.valueOf(5L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // In advance with value of 10, but no outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 10, but no outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 10, but 0 is outstanding for this installment, so 10 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 10, but no only 5 is outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(ten)); // Principal 0, interest 0, fee 5, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(five), refEq(zero)); } @Test public void inAdvancePaymentOfPenalty() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, true, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // In advance with value of 10, but only 5 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 5, but no outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(five)); + // In advance with value of 5, but 0 is outstanding for this installment, so 5 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(five)); + // In advance with value of 5, but no outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(five)); // Principal 0, interest 0, fee 0, penalty 5 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(zero), refEq(five)); } @Test public void inAdvancePaymentOfPrincipalAndPenalty() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, true, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(2)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // In advance with value of 10, and 5 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(five)); + // In advance with value of 5, but only 5 is outstanding for this installment, so 0 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(five)); // Principal 5, interest 0, fee 0, penalty 5 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(five), refEq(zero), refEq(zero), refEq(five)); } @Test public void inAdvancePaymentOfPrincipalAndPenaltyAndFee() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, true, six, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(2L), BigDecimal.valueOf(6L), false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(2L), BigDecimal.valueOf(6L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // In advance with value of 10, but only 6 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 4, but no outstanding + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(four)); + // In advance with value of 4, but only 3 is outstanding for this installment, so 1 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(four)); + // In advance with value of 1, and 2 is outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(one)); // Principal 3, interest 0, fee 1, penalty 6 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(zero), refEq(one), refEq(six)); } @Test public void inAdvancePaymentOfPrincipalAndPenaltyAndFeeAndInterest() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -221,15 +284,25 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); - // Principal 3, interest 2, fee 1, penalty 4 + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // In advance with value of 10, but only 4 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 6, but only 2 is outstanding + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(six)); + // In advance with value of 4, but only 3 is outstanding for this installment, so 1 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(four)); + // In advance with value of 1, and 2 is outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(one)); + // Principal 3, interest 1, fee 2, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(two), refEq(one), refEq(four)); } @Test public void inAdvancePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPartialPenalty() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(2)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -238,15 +311,18 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // In advance with value of 2, but 4 is outstanding for this installment + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(two)); // Principal 0, interest 0, fee 0, penalty 2 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(zero), refEq(two)); } @Test public void inAdvancePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPenaltyAndPartialInterest() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(5)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -255,15 +331,24 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // In advance with value of 5, and 4 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(five)); + // In advance with value of 1, and 2 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(one)); + // In advance with value of 0, and 3 is outstanding for this installment, so 0 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(2)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); // Principal 0, interest 1, fee 0, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(one), refEq(zero), refEq(four)); } @Test public void inAdvancePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPenaltyAndInterestAndPartialPrincipal() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(7)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -272,15 +357,24 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // In advance with value of 7, and 4 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(seven)); + // In advance with value of 3, and 2 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(three)); + // In advance with value of 1, and 3 is outstanding for this installment, so 0 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(one)); + // Calculated zero + Mockito.verify(installment, Mockito.times(2)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); // Principal 1, interest 2, fee 0, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(one), refEq(two), refEq(zero), refEq(four)); } @Test public void inAdvancePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPenaltyAndInterestAndPrincipalAndPartialFee() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); + Mockito.when(charges.stream()).thenReturn(Stream.empty()); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -289,14 +383,37 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); - // Principal 3, interest 2, fee 1, penalty 4 + + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(zero)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(one)); + // In advance with value of 10, and 4 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(ten)); + // In advance with value of 6, and 2 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(six)); + // In advance with value of 4, and 3 is outstanding for this installment, so 1 is unprocessed + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(four)); + // In advance with value of 1, and outstanding is 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(one)); + // Principal 3, interest 1, fee 1, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(two), refEq(one), refEq(four)); } @Test public void duePaymentOfPenaltyAsInAdvanceTransaction() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, two, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, two, null); + + LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class); + Mockito.when(loanCharge1.isActive()).thenReturn(true); + Mockito.when(loanCharge1.isNotFullyPaid()).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.getEffectiveDueDate()).thenReturn(transactionDate.plusDays(1)); + Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito @@ -306,14 +423,39 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Calculated two + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(two)); + // Calculated zero + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(zero)); + // In advance with value of 8, and 2 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(eight)); + // In advance with value of 6, and 2 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(six)); + // In advance with value of 4, and 3 is outstanding of principal + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(four)); + // In advance with value of 1, but only 2 is outstanding of fee + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(one)); // Principal 3, interest 2, fee 1, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(two), refEq(one), refEq(four)); } @Test public void duePaymentOfPenaltyAndFeeAsInAdvanceTransaction() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, two, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate.minusDays(1), false, one, null); + + LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class); + Mockito.when(loanCharge1.isActive()).thenReturn(true); + Mockito.when(loanCharge1.isNotFullyPaid()).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.isPenaltyCharge()).thenReturn(false); + Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.minusDays(1)); + Mockito.when(loanCharge2.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(one); + Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito @@ -323,14 +465,37 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Calculated two + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(two)); + // Calculated one + Mockito.verify(installment, Mockito.times(2)).payFeeChargesComponent(eq(transactionDate), refEq(one)); + // In advance with value of 7, and 2 is outstanding of penalty + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(seven)); + // In advance with value of 5, and 2 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(five)); + // In advance with value of 3, and 2 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(three)); // Principal 2, interest 2, fee 2, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(two), refEq(two), refEq(four)); } @Test public void duePaymentOfHigherPenaltyAndHigherFeeAsInAdvanceTransaction() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, eleven, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate.minusDays(1), false, eleven, null); + + LoanCharge loanCharge1 = Mockito.mock(LoanCharge.class); + Mockito.when(loanCharge1.isActive()).thenReturn(true); + Mockito.when(loanCharge1.isNotFullyPaid()).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.isPenaltyCharge()).thenReturn(false); + Mockito.when(loanCharge2.getEffectiveDueDate()).thenReturn(transactionDate.minusDays(1)); + Mockito.when(loanCharge2.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(eleven); + Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito @@ -340,95 +505,40 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Calculated eleven, overriden by unprocessed + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(transactionDate), refEq(ten)); + // Calculated eleven, overriden by unprocessed + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(transactionDate), refEq(six)); + // In advance with value of 4, and 2 is outstanding of interest + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(transactionDate), refEq(four)); + // In advance with value of 2, and 2 is outstanding of principal + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(transactionDate), refEq(two)); + // Principal 2, interest 2, fee 2, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(two), refEq(two), refEq(four)); } - @Test - public void inAdvancePaymentOfPrincipalAndPenaltyWherePenaltyIsSameDayButEarlierThanRepayment() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, one, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, true, three, OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = five; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(4L), false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); - Mockito.when(loanTransaction.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 3, ZoneOffset.UTC))); - underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 1, interest 0, fee 0, penalty 4 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(one), refEq(zero), refEq(zero), refEq(four)); - } - - @Test - public void inAdvancePaymentOfPrincipalAndPenaltyWherePenaltyIsSameDayButLaterThanRepayment() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, one, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, true, three, OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = five; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(4L), false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); - Mockito.when(loanTransaction.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 1, ZoneOffset.UTC))); - underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 1, interest 0, fee 0, penalty 4 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(one), refEq(zero), refEq(zero), refEq(four)); - } - - @Test - public void inAdvancePaymentOfPrincipalAndPenaltyWherePenaltyIsSameDayButNoChargeCreatedDate() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, one, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, true, three, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = five; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(4L), false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); - underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 1, interest 0, fee 0, penalty 4 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(one), refEq(zero), refEq(zero), refEq(four)); - } - - @Test - public void inAdvancePaymentOfPrincipalAndPenaltyWherePenaltyIsSameDayButNoTransactionCreatedDate() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, one, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, true, three, OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = five; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(4L), false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, transactionDate, ExternalId.empty())); - underTest.handleTransactionThatIsPaymentInAdvanceOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 1, interest 0, fee 0, penalty 4 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(one), refEq(zero), refEq(zero), refEq(four)); - } - // ON TIME @Test public void onTimePaymentOfPrincipal() { Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(five)); // Principal 5, interest 0, fee 0, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(five), refEq(zero), refEq(zero), refEq(zero)); } @@ -437,86 +547,119 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio public void onTimePaymentOfInterest() { Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.valueOf(5L), BigDecimal.ZERO, BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(5L), BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(five)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(five)); // Principal 0, interest 5, fee 0, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(five), refEq(zero), refEq(zero)); } @Test public void onTimePaymentOfFee() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, false, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.ZERO, BigDecimal.valueOf(5L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); // Principal 0, interest 0, fee 5, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(five), refEq(zero)); } @Test public void onTimePaymentOfPenalty() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(five)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(five)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(five)); // Principal 0, interest 0, fee 0, penalty 5 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(zero), refEq(five)); } @Test public void onTimePaymentOfPrincipalAndPenalty() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(five)); + // Unprocessed: 5, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(five)); + // Unprocessed: 0, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(zero)); // Principal 5, interest 0, fee 0, penalty 5 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(five), refEq(zero), refEq(zero), refEq(five)); } @Test public void onTimePaymentOfPrincipalAndPenaltyAndFee() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, six, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(2L), BigDecimal.valueOf(6L), false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(2L), BigDecimal.valueOf(6L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 6 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 4, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(four)); + // Unprocessed: 4, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(four)); + // Unprocessed: 1, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(one)); // Principal 3, interest 0, fee 1, penalty 6 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(zero), refEq(one), refEq(six)); } @Test public void onTimePaymentOfPrincipalAndPenaltyAndFeeAndInterest() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -525,16 +668,22 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 6, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(six)); + // Unprocessed: 4, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(four)); + // Unprocessed: 1, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(one)); // Principal 3, interest 2, fee 1, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(two), refEq(one), refEq(four)); } @Test public void onTimePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPartialPenalty() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.ONE); + Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(1)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), BigDecimal.valueOf(4L), false, null, BigDecimal.ZERO)); @@ -542,15 +691,21 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 1, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(one)); + // Unprocessed: 0, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(zero)); + // Unprocessed: 0, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(zero)); + // Unprocessed: 0, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(zero)); // Principal 0, interest 0, fee 0, penalty 1 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(zero), refEq(one)); } @Test public void onTimePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPenaltyAndPartialInterest() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(5)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -559,15 +714,21 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 5, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(five)); + // Unprocessed: 1, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(one)); + // Unprocessed: 0, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(zero)); + // Unprocessed: 0, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(zero)); // Principal 0, interest 1, fee 0, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(one), refEq(zero), refEq(four)); } @Test public void onTimePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPenaltyAndInterestAndPartialPrincipal() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(8)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -576,15 +737,21 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 8, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(eight)); + // Unprocessed: 4, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(four)); + // Unprocessed: 2, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(two)); + // Unprocessed: 0, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(zero)); // Principal 2, interest 2, fee 0, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(two), refEq(zero), refEq(four)); } @Test public void onTimePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPenaltyAndInterestAndPrincipalAndPartialFee() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -593,98 +760,39 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(firstInstallmentDueDate), refEq(ten)); + // Unprocessed: 6, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(firstInstallmentDueDate), refEq(six)); + // Unprocessed: 4, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(firstInstallmentDueDate), refEq(four)); + // Unprocessed: 1, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(firstInstallmentDueDate), refEq(one)); // Principal 3, interest 2, fee 1, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(two), refEq(one), refEq(four)); } - @Test - public void onTimePaymentOfPrincipalAndFeeWhereFeeIsSameDayButEarlierThanRepayment() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentDueDate, false, one, null); - LoanCharge loanCharge2 = createLoanCharge(firstInstallmentDueDate, false, three, - OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = four; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(2L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); - Mockito.when(loanTransaction.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 3, ZoneOffset.UTC))); - underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, - charges); - - // Principal 2, interest 0, fee 2, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(zero), refEq(two), refEq(zero)); - } - - @Test - public void onTimePaymentOfPrincipalAndFeeWhereFeeIsSameDayButLaterThanRepayment() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentDueDate, false, one, null); - LoanCharge loanCharge2 = createLoanCharge(firstInstallmentDueDate, false, three, - OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = four; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); - Mockito.when(loanTransaction.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 1, ZoneOffset.UTC))); - underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, - charges); - - // Principal 3, interest 0, fee 1, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(zero), refEq(one), refEq(zero)); - } - - @Test - public void onTimePaymentOfPrincipalAndFeeWhereFeeIsSameDayButNoChargeCreatedDate() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentDueDate, false, one, null); - LoanCharge loanCharge2 = createLoanCharge(firstInstallmentDueDate, false, three, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = five; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(2L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); - underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, - charges); - - // Principal 2, interest 0, fee 3, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(zero), refEq(three), refEq(zero)); - } - - @Test - public void ontTimePaymentOfPrincipalAndFeeWhereFeeIsSameDayButNoTransactionCreatedDate() { - LoanCharge loanCharge1 = createLoanCharge(firstInstallmentDueDate, false, one, null); - LoanCharge loanCharge2 = createLoanCharge(firstInstallmentDueDate, false, three, - OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = five; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(2L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); - underTest.handleTransactionThatIsOnTimePaymentOfInstallment(installment, loanTransaction, transactionAmount, transactionMappings, - charges); - - // Principal 2, interest 0, fee 3, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(zero), refEq(three), refEq(zero)); - } - // LATE @Test public void latePaymentOfPrincipal() { Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(five)); // Principal 5, interest 0, fee 0, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(five), refEq(zero), refEq(zero), refEq(zero)); } @@ -693,86 +801,119 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio public void latePaymentOfInterest() { Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.valueOf(5L), BigDecimal.ZERO, BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(5L), BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(five)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(five)); // Principal 0, interest 5, fee 0, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(five), refEq(zero), refEq(zero)); } @Test public void latePaymentOfFee() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, false, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.ZERO, BigDecimal.valueOf(5L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 10, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(ten)); // Principal 0, interest 0, fee 5, penalty 0 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(five), refEq(zero)); } @Test public void latePaymentOfPenalty() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.ZERO, - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(0L), + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(five)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(five)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(five)); // Principal 0, interest 0, fee 0, penalty 5 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(zero), refEq(five)); } @Test public void latePaymentOfPrincipalAndPenalty() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, five, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), - BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(0L), BigDecimal.valueOf(5L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 5, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(five)); + // Unprocessed: 5, outstanding: 5 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(five)); + // Unprocessed: 0, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(zero)); // Principal 5, interest 0, fee 0, penalty 5 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(five), refEq(zero), refEq(zero), refEq(five)); } @Test public void latePaymentOfPrincipalAndPenaltyAndFee() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, six, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(2L), BigDecimal.valueOf(6L), false, null, BigDecimal.ZERO)); + BigDecimal.valueOf(0L), BigDecimal.valueOf(2L), BigDecimal.valueOf(6L), false, null, BigDecimal.ZERO)); LoanTransaction loanTransaction = Mockito .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 6 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 4, outstanding: 0 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(four)); + // Unprocessed: 4, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(four)); + // Unprocessed: 1, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(one)); // Principal 3, interest 0, fee 1, penalty 6 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(zero), refEq(one), refEq(six)); } @Test public void latePaymentOfPrincipalAndPenaltyAndFeeAndInterest() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -781,16 +922,22 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 6, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(six)); + // Unprocessed: 4, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(four)); + // Unprocessed: 1, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(one)); // Principal 3, interest 2, fee 1, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(two), refEq(one), refEq(four)); } @Test public void latePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPartialPenalty() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.ONE); + Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(1)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), BigDecimal.valueOf(4L), false, null, BigDecimal.ZERO)); @@ -798,15 +945,21 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 1, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(one)); + // Unprocessed: 0, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(zero)); + // Unprocessed: 0, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(zero)); + // Unprocessed: 0, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(zero)); // Principal 0, interest 0, fee 0, penalty 1 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(zero), refEq(zero), refEq(one)); } @Test public void latePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPenaltyAndPartialInterest() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(5)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -815,15 +968,21 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 5, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(five)); + // Unprocessed: 1, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(one)); + // Unprocessed: 0, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(zero)); + // Unprocessed: 0, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(zero)); // Principal 0, interest 1, fee 0, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(zero), refEq(one), refEq(zero), refEq(four)); } @Test public void latePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPenaltyAndInterestAndPartialPrincipal() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(8)); LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -832,15 +991,21 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 8, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(eight)); + // Unprocessed: 4, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(four)); + // Unprocessed: 2, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(two)); + // Unprocessed: 0, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(zero)); // Principal 2, interest 2, fee 0, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(two), refEq(zero), refEq(four)); } @Test public void latePaymentOfPrincipalAndPenaltyAndFeeAndInterestButNotEnoughOnlyForPenaltyAndInterestAndPrincipalAndPartialFee() { - LoanCharge loanCharge1 = createLoanCharge(transactionDate, true, four, null); - LoanCharge loanCharge2 = createLoanCharge(transactionDate, false, two, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); Money transactionAmount = ten; LoanRepaymentScheduleInstallment installment = Mockito .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), @@ -849,95 +1014,16 @@ public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactio .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, transactionMappings, charges); + + // Unprocessed: 10, outstanding: 4 + Mockito.verify(installment, Mockito.times(1)).payPenaltyChargesComponent(eq(lateDate), refEq(ten)); + // Unprocessed: 6, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payInterestComponent(eq(lateDate), refEq(six)); + // Unprocessed: 4, outstanding: 3 + Mockito.verify(installment, Mockito.times(1)).payPrincipalComponent(eq(lateDate), refEq(four)); + // Unprocessed: 1, outstanding: 2 + Mockito.verify(installment, Mockito.times(1)).payFeeChargesComponent(eq(lateDate), refEq(one)); // Principal 3, interest 2, fee 1, penalty 4 Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(two), refEq(one), refEq(four)); } - - @Test - public void latePaymentOfPrincipalAndFeeWhereFeeIsSameDayButLaterThanRepayment() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, false, one, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, three, OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = four; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); - Mockito.when(loanTransaction.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 1, ZoneOffset.UTC))); - underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 3, interest 0, fee 1, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(zero), refEq(one), refEq(zero)); - } - - @Test - public void latePaymentOfPrincipalAndFeeWhereFeeIsSameDayButEarlierThanRepayment() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, false, one, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, three, OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = four; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(2L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); - Mockito.when(loanTransaction.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 3, ZoneOffset.UTC))); - underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 2, interest 0, fee 2, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(two), refEq(zero), refEq(two), refEq(zero)); - } - - @Test - public void latePaymentOfPrincipalAndFeeWhereFeeIsSameDayButNoChargeCreatedDate() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, false, one, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, three, null); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = five; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); - underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 3, interest 0, fee 2, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(zero), refEq(two), refEq(zero)); - } - - @Test - public void latePaymentOfPrincipalAndFeeWhereFeeIsSameDayButNoTransactionCreatedDate() { - LoanCharge loanCharge1 = createLoanCharge(lateDate, false, three, null); - LoanCharge loanCharge2 = createLoanCharge(lateDate, false, one, OffsetDateTime.of(2023, 1, 1, 1, 1, 1, 2, ZoneOffset.UTC)); - Mockito.when(charges.stream()).thenReturn(Stream.of(loanCharge1, loanCharge2)); - Money transactionAmount = five; - LoanRepaymentScheduleInstallment installment = Mockito - .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), - BigDecimal.ZERO, BigDecimal.valueOf(4L), BigDecimal.ZERO, false, null, BigDecimal.ZERO)); - LoanTransaction loanTransaction = Mockito - .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); - underTest.handleTransactionThatIsALateRepaymentOfInstallment(installment, null, loanTransaction, transactionAmount, - transactionMappings, charges); - - // Principal 3, interest 0, fee 2, penalty 0 - Mockito.verify(loanTransaction, Mockito.times(1)).updateComponents(refEq(three), refEq(zero), refEq(two), refEq(zero)); - } - - @NotNull - private LoanCharge createLoanCharge(LocalDate dueDate, boolean isPenalty, Money amount, OffsetDateTime createdDate) { - LoanCharge loanCharge = Mockito.mock(LoanCharge.class); - Mockito.lenient().when(loanCharge.isActive()).thenReturn(true); - Mockito.lenient().when(loanCharge.isNotFullyPaid()).thenReturn(true); - Mockito.lenient().when(loanCharge.getEffectiveDueDate()).thenReturn(dueDate); - Mockito.lenient().when(loanCharge.isPenaltyCharge()).thenReturn(isPenalty); - if (createdDate != null) { - Mockito.lenient().when(loanCharge.getCreatedDate()).thenReturn(Optional.of(createdDate)); - } - Mockito.lenient().when(loanCharge.getAmount(refEq(MONETARY_CURRENCY))).thenReturn(amount); - return loanCharge; - } } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java index ed8a3afe5..77c2ece77 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java @@ -6802,8 +6802,7 @@ public class ClientLoanIntegrationTest { final PostClientsRequest newClient = createRandomClientWithDate("01 January 2023"); final PostClientsResponse clientResponse = CLIENT_HELPER.createClient(newClient); LOG.info("-----------------------------------NEW LOAN PRODUCT-----------------------------------------"); - PostLoanProductsRequest loanProductsRequest = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct( - LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY); + PostLoanProductsRequest loanProductsRequest = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct(); final PostLoanProductsResponse loanProductResponse = LOAN_PRODUCT_HELPER.createLoanProduct(loanProductsRequest); LOG.info("-----------------------------------CREATE CHARGES-----------------------------------------"); PostChargesResponse penaltyCharge = CHARGES_HELPER.createCharges(new PostChargesRequest().penalty(true).amount(10.0) @@ -6863,337 +6862,6 @@ public class ClientLoanIntegrationTest { } } - @Test - public void testLoanTransactionOrderAndRepaymentAllocationWithSameDayChargeBeforeRepayment() { - try { - List dueDateRelatedRepaymentStrategies = Arrays.asList( - LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, - LoanProductTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY); - for (String repaymentStrategyCode : dueDateRelatedRepaymentStrategies) { - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(REQUEST_SPEC, RESPONSE_SPEC, "42", true); - GlobalConfigurationHelper.updateIsBusinessDateEnabled(REQUEST_SPEC, RESPONSE_SPEC, Boolean.TRUE); - BUSINESS_DATE_HELPER.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) - .date("01 January 2023").dateFormat(DATETIME_PATTERN).locale("en")); - LOG.info("-----------------------------------NEW CLIENT-----------------------------------------"); - final PostClientsRequest newClient = createRandomClientWithDate("01 January 2023"); - final PostClientsResponse clientResponse = CLIENT_HELPER.createClient(newClient); - LOG.info("-----------------------------------NEW LOAN PRODUCT-----------------------------------------"); - PostLoanProductsRequest loanProductsRequest = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct( - repaymentStrategyCode); - final PostLoanProductsResponse loanProductResponse = LOAN_PRODUCT_HELPER.createLoanProduct(loanProductsRequest); - LOG.info("-----------------------------------CREATE CHARGES-----------------------------------------"); - PostChargesResponse penaltyCharge = CHARGES_HELPER.createCharges( - new PostChargesRequest().penalty(true).amount(10.0).chargeCalculationType(ChargeCalculationType.FLAT.getValue()) - .chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE.getValue()) - .chargePaymentMode(ChargePaymentMode.REGULAR.getValue()).currencyCode("USD") - .name(Utils.randomStringGenerator("PENALTY_" + Calendar.getInstance().getTimeInMillis(), 5)) - .chargeAppliesTo(1).locale("en").active(true)); - LOG.info("-----------------------------------SUBMIT LOAN-----------------------------------------"); - final PostLoansResponse loanApplicationResult = applyForLoanApplicationForOnePeriod30DaysLongNoInterestPeriodicAccrual( - clientResponse.getResourceId(), loanProductResponse.getResourceId(), "01 January 2023", - LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY); - LOG.info("-----------------------------------APPROVE LOAN-----------------------------------------"); - PostLoansLoanIdResponse approvedLoanResult = LOAN_TRANSACTION_HELPER.approveLoan(loanApplicationResult.getResourceId(), - new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000.0)).dateFormat(DATETIME_PATTERN) - .approvedOnDate("01 January 2023").locale("en")); - LOG.info("-------------------------------DISBURSE LOAN-------------------------------------------"); - String loanDisbursementUUID = UUID.randomUUID().toString(); - PostLoansLoanIdResponse disbursedLoanResult = LOAN_TRANSACTION_HELPER.disburseLoan(loanApplicationResult.getResourceId(), - new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) - .transactionAmount(BigDecimal.valueOf(1000.00)).locale("en").externalId(loanDisbursementUUID)); - Long loanId = disbursedLoanResult.getResourceId(); - BUSINESS_DATE_HELPER.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) - .date("07 January 2023").dateFormat(DATETIME_PATTERN).locale("en")); - LOG.info("-------------------------------ADD CHARGES-------------------------------------------"); - PostLoansLoanIdChargesResponse penaltyLoanChargeResult = LOAN_TRANSACTION_HELPER.addChargesForLoan(loanId, - new PostLoansLoanIdChargesRequest().chargeId(penaltyCharge.getResourceId()).dateFormat(DATETIME_PATTERN) - .locale("en").amount(10.0).dueDate("10 January 2023")); - LOG.info("-------------------------------DO A PARTIAL REPAYMENT-------------------------------------------"); - String firstRepaymentUUID = UUID.randomUUID().toString(); - PostLoansLoanIdTransactionsResponse firstRepaymentResult = LOAN_TRANSACTION_HELPER.makeLoanRepayment(loanId, - new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("07 January 2023") - .locale("en").transactionAmount(9.0).externalId(firstRepaymentUUID)); - LOG.info("-------------------------------CHECK LOAN TRANSACTION ALLOCATION-------------------------------------------"); - checkLoanTransactionAllocation(loanId, firstRepaymentResult.getResourceExternalId(), 9.0, 0.0, 0.0, 0.0, 9.0, 0.0); - } - } finally { - GlobalConfigurationHelper.updateIsBusinessDateEnabled(REQUEST_SPEC, RESPONSE_SPEC, Boolean.FALSE); - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(REQUEST_SPEC, RESPONSE_SPEC, "42", false); - } - } - - @Test - public void testLoanTransactionOrderAndRepaymentAllocationWithSameDayChargeAfterRepayment() { - try { - List dueDateRelatedRepaymentStrategies = Arrays.asList( - LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, - LoanProductTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY); - for (String repaymentStrategyCode : dueDateRelatedRepaymentStrategies) { - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(REQUEST_SPEC, RESPONSE_SPEC, "42", true); - GlobalConfigurationHelper.updateIsBusinessDateEnabled(REQUEST_SPEC, RESPONSE_SPEC, Boolean.TRUE); - BUSINESS_DATE_HELPER.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) - .date("01 January 2023").dateFormat(DATETIME_PATTERN).locale("en")); - LOG.info("-----------------------------------NEW CLIENT-----------------------------------------"); - final PostClientsRequest newClient = createRandomClientWithDate("01 January 2023"); - final PostClientsResponse clientResponse = CLIENT_HELPER.createClient(newClient); - LOG.info("-----------------------------------NEW LOAN PRODUCT-----------------------------------------"); - PostLoanProductsRequest loanProductsRequest = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct( - repaymentStrategyCode); - final PostLoanProductsResponse loanProductResponse = LOAN_PRODUCT_HELPER.createLoanProduct(loanProductsRequest); - LOG.info("-----------------------------------CREATE CHARGES-----------------------------------------"); - PostChargesResponse penaltyCharge = CHARGES_HELPER.createCharges( - new PostChargesRequest().penalty(false).amount(10.0).chargeCalculationType(ChargeCalculationType.FLAT.getValue()) - .chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE.getValue()) - .chargePaymentMode(ChargePaymentMode.REGULAR.getValue()).currencyCode("USD") - .name(Utils.randomStringGenerator("PENALTY_" + Calendar.getInstance().getTimeInMillis(), 5)) - .chargeAppliesTo(1).locale("en").active(true)); - LOG.info("-----------------------------------SUBMIT LOAN-----------------------------------------"); - final PostLoansResponse loanApplicationResult = applyForLoanApplicationForOnePeriod30DaysLongNoInterestPeriodicAccrual( - clientResponse.getResourceId(), loanProductResponse.getResourceId(), "01 January 2023", - LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY); - LOG.info("-----------------------------------APPROVE LOAN-----------------------------------------"); - PostLoansLoanIdResponse approvedLoanResult = LOAN_TRANSACTION_HELPER.approveLoan(loanApplicationResult.getResourceId(), - new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000.0)).dateFormat(DATETIME_PATTERN) - .approvedOnDate("01 January 2023").locale("en")); - LOG.info("-------------------------------DISBURSE LOAN-------------------------------------------"); - String loanDisbursementUUID = UUID.randomUUID().toString(); - PostLoansLoanIdResponse disbursedLoanResult = LOAN_TRANSACTION_HELPER.disburseLoan(loanApplicationResult.getResourceId(), - new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) - .transactionAmount(BigDecimal.valueOf(1000.00)).locale("en").externalId(loanDisbursementUUID)); - Long loanId = disbursedLoanResult.getResourceId(); - BUSINESS_DATE_HELPER.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) - .date("07 January 2023").dateFormat(DATETIME_PATTERN).locale("en")); - LOG.info("-------------------------------DO A PARTIAL REPAYMENT-------------------------------------------"); - String firstRepaymentUUID = UUID.randomUUID().toString(); - PostLoansLoanIdTransactionsResponse firstRepaymentResult = LOAN_TRANSACTION_HELPER.makeLoanRepayment(loanId, - new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("07 January 2023") - .locale("en").transactionAmount(9.0).externalId(firstRepaymentUUID)); - LOG.info("-------------------------------ADD CHARGES-------------------------------------------"); - PostLoansLoanIdChargesResponse penaltyLoanChargeResult = LOAN_TRANSACTION_HELPER.addChargesForLoan(loanId, - new PostLoansLoanIdChargesRequest().chargeId(penaltyCharge.getResourceId()).dateFormat(DATETIME_PATTERN) - .locale("en").amount(10.0).dueDate("10 January 2023")); - LOG.info("-------------------------------CHECK LOAN TRANSACTION ALLOCATION-------------------------------------------"); - checkLoanTransactionAllocation(loanId, firstRepaymentResult.getResourceExternalId(), 9.0, 9.0, 0.0, 0.0, 0.0, 0.0); - } - } finally { - GlobalConfigurationHelper.updateIsBusinessDateEnabled(REQUEST_SPEC, RESPONSE_SPEC, Boolean.FALSE); - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(REQUEST_SPEC, RESPONSE_SPEC, "42", false); - } - } - - @Test - public void testLoanTransactionOrderAndRepaymentAllocationWithSameDayChargeBeforeRepaymentOnInstallmentDueDate() { - try { - List dueDateRelatedRepaymentStrategies = Arrays.asList( - LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, - LoanProductTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY); - for (String repaymentStrategyCode : dueDateRelatedRepaymentStrategies) { - LOG.info("-----------------------------------REPAYMENT SCEDULE CODE : {}-----------------------------------------", - repaymentStrategyCode); - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(REQUEST_SPEC, RESPONSE_SPEC, "42", true); - GlobalConfigurationHelper.updateIsBusinessDateEnabled(REQUEST_SPEC, RESPONSE_SPEC, Boolean.TRUE); - BUSINESS_DATE_HELPER.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) - .date("01 January 2023").dateFormat(DATETIME_PATTERN).locale("en")); - LOG.info("-----------------------------------NEW CLIENT-----------------------------------------"); - final PostClientsRequest newClient = createRandomClientWithDate("01 January 2023"); - final PostClientsResponse clientResponse = CLIENT_HELPER.createClient(newClient); - LOG.info("-----------------------------------NEW LOAN PRODUCT-----------------------------------------"); - PostLoanProductsRequest loanProductsRequest = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct( - repaymentStrategyCode); - final PostLoanProductsResponse loanProductResponse = LOAN_PRODUCT_HELPER.createLoanProduct(loanProductsRequest); - LOG.info("-----------------------------------CREATE CHARGES-----------------------------------------"); - PostChargesResponse penaltyCharge = CHARGES_HELPER.createCharges( - new PostChargesRequest().penalty(true).amount(10.0).chargeCalculationType(ChargeCalculationType.FLAT.getValue()) - .chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE.getValue()) - .chargePaymentMode(ChargePaymentMode.REGULAR.getValue()).currencyCode("USD") - .name(Utils.randomStringGenerator("PENALTY_" + Calendar.getInstance().getTimeInMillis(), 5)) - .chargeAppliesTo(1).locale("en").active(true)); - LOG.info("-----------------------------------SUBMIT LOAN-----------------------------------------"); - final PostLoansResponse loanApplicationResult = applyForLoanApplicationForOnePeriod30DaysLongNoInterestPeriodicAccrual( - clientResponse.getResourceId(), loanProductResponse.getResourceId(), "01 January 2023", - LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY); - LOG.info("-----------------------------------APPROVE LOAN-----------------------------------------"); - PostLoansLoanIdResponse approvedLoanResult = LOAN_TRANSACTION_HELPER.approveLoan(loanApplicationResult.getResourceId(), - new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000.0)).dateFormat(DATETIME_PATTERN) - .approvedOnDate("01 January 2023").locale("en")); - LOG.info("-------------------------------DISBURSE LOAN-------------------------------------------"); - String loanDisbursementUUID = UUID.randomUUID().toString(); - PostLoansLoanIdResponse disbursedLoanResult = LOAN_TRANSACTION_HELPER.disburseLoan(loanApplicationResult.getResourceId(), - new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) - .transactionAmount(BigDecimal.valueOf(1000.00)).locale("en").externalId(loanDisbursementUUID)); - Long loanId = disbursedLoanResult.getResourceId(); - BUSINESS_DATE_HELPER.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) - .date("31 January 2023").dateFormat(DATETIME_PATTERN).locale("en")); - LOG.info("-------------------------------ADD CHARGES-------------------------------------------"); - PostLoansLoanIdChargesResponse penaltyLoanChargeResult = LOAN_TRANSACTION_HELPER.addChargesForLoan(loanId, - new PostLoansLoanIdChargesRequest().chargeId(penaltyCharge.getResourceId()).dateFormat(DATETIME_PATTERN) - .locale("en").amount(10.0).dueDate("31 January 2023")); - LOG.info("-------------------------------DO A PARTIAL REPAYMENT-------------------------------------------"); - String firstRepaymentUUID = UUID.randomUUID().toString(); - PostLoansLoanIdTransactionsResponse firstRepaymentResult = LOAN_TRANSACTION_HELPER.makeLoanRepayment(loanId, - new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023") - .locale("en").transactionAmount(9.0).externalId(firstRepaymentUUID)); - LOG.info("-------------------------------CHECK LOAN TRANSACTION ALLOCATION-------------------------------------------"); - checkLoanTransactionAllocation(loanId, firstRepaymentResult.getResourceExternalId(), 9.0, 0.0, 0.0, 0.0, 9.0, 0.0); - } - } finally { - GlobalConfigurationHelper.updateIsBusinessDateEnabled(REQUEST_SPEC, RESPONSE_SPEC, Boolean.FALSE); - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(REQUEST_SPEC, RESPONSE_SPEC, "42", false); - } - } - - @Test - public void testLoanTransactionOrderAndRepaymentAllocationWithSameDayChargeAfterRepaymentOnInstallmentDueDate() { - try { - List dueDateRelatedRepaymentStrategies = Arrays.asList( - LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, - LoanProductTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY); - for (String repaymentStrategyCode : dueDateRelatedRepaymentStrategies) { - LOG.info("-----------------------------------REPAYMENT SCEDULE CODE : {}-----------------------------------------", - repaymentStrategyCode); - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(REQUEST_SPEC, RESPONSE_SPEC, "42", true); - GlobalConfigurationHelper.updateIsBusinessDateEnabled(REQUEST_SPEC, RESPONSE_SPEC, Boolean.TRUE); - BUSINESS_DATE_HELPER.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) - .date("01 January 2023").dateFormat(DATETIME_PATTERN).locale("en")); - LOG.info("-----------------------------------NEW CLIENT-----------------------------------------"); - final PostClientsRequest newClient = createRandomClientWithDate("01 January 2023"); - final PostClientsResponse clientResponse = CLIENT_HELPER.createClient(newClient); - LOG.info("-----------------------------------NEW LOAN PRODUCT-----------------------------------------"); - PostLoanProductsRequest loanProductsRequest = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct( - repaymentStrategyCode); - final PostLoanProductsResponse loanProductResponse = LOAN_PRODUCT_HELPER.createLoanProduct(loanProductsRequest); - LOG.info("-----------------------------------CREATE CHARGES-----------------------------------------"); - PostChargesResponse penaltyCharge = CHARGES_HELPER.createCharges( - new PostChargesRequest().penalty(true).amount(10.0).chargeCalculationType(ChargeCalculationType.FLAT.getValue()) - .chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE.getValue()) - .chargePaymentMode(ChargePaymentMode.REGULAR.getValue()).currencyCode("USD") - .name(Utils.randomStringGenerator("PENALTY_" + Calendar.getInstance().getTimeInMillis(), 5)) - .chargeAppliesTo(1).locale("en").active(true)); - LOG.info("-----------------------------------SUBMIT LOAN-----------------------------------------"); - final PostLoansResponse loanApplicationResult = applyForLoanApplicationForOnePeriod30DaysLongNoInterestPeriodicAccrual( - clientResponse.getResourceId(), loanProductResponse.getResourceId(), "01 January 2023", - LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY); - LOG.info("-----------------------------------APPROVE LOAN-----------------------------------------"); - PostLoansLoanIdResponse approvedLoanResult = LOAN_TRANSACTION_HELPER.approveLoan(loanApplicationResult.getResourceId(), - new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000.0)).dateFormat(DATETIME_PATTERN) - .approvedOnDate("01 January 2023").locale("en")); - LOG.info("-------------------------------DISBURSE LOAN-------------------------------------------"); - String loanDisbursementUUID = UUID.randomUUID().toString(); - PostLoansLoanIdResponse disbursedLoanResult = LOAN_TRANSACTION_HELPER.disburseLoan(loanApplicationResult.getResourceId(), - new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) - .transactionAmount(BigDecimal.valueOf(1000.00)).locale("en").externalId(loanDisbursementUUID)); - Long loanId = disbursedLoanResult.getResourceId(); - BUSINESS_DATE_HELPER.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) - .date("31 January 2023").dateFormat(DATETIME_PATTERN).locale("en")); - LOG.info("-------------------------------DO A PARTIAL REPAYMENT-------------------------------------------"); - String firstRepaymentUUID = UUID.randomUUID().toString(); - PostLoansLoanIdTransactionsResponse firstRepaymentResult = LOAN_TRANSACTION_HELPER.makeLoanRepayment(loanId, - new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("31 January 2023") - .locale("en").transactionAmount(9.0).externalId(firstRepaymentUUID)); - LOG.info("-------------------------------ADD CHARGES-------------------------------------------"); - PostLoansLoanIdChargesResponse penaltyLoanChargeResult = LOAN_TRANSACTION_HELPER.addChargesForLoan(loanId, - new PostLoansLoanIdChargesRequest().chargeId(penaltyCharge.getResourceId()).dateFormat(DATETIME_PATTERN) - .locale("en").amount(10.0).dueDate("31 January 2023")); - LOG.info("-------------------------------CHECK LOAN TRANSACTION ALLOCATION-------------------------------------------"); - checkLoanTransactionAllocation(loanId, firstRepaymentResult.getResourceExternalId(), 9.0, 9.0, 0.0, 0.0, 0.0, 0.0); - } - } finally { - GlobalConfigurationHelper.updateIsBusinessDateEnabled(REQUEST_SPEC, RESPONSE_SPEC, Boolean.FALSE); - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(REQUEST_SPEC, RESPONSE_SPEC, "42", false); - } - } - - @Test - public void testLoanTransactionOrderAndRepaymentAllocationMultipleActions() { - try { - List dueDateRelatedRepaymentStrategies = Arrays.asList( - LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, - LoanProductTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY); - for (String repaymentStrategyCode : dueDateRelatedRepaymentStrategies) { - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(REQUEST_SPEC, RESPONSE_SPEC, "42", true); - GlobalConfigurationHelper.updateIsBusinessDateEnabled(REQUEST_SPEC, RESPONSE_SPEC, Boolean.TRUE); - BUSINESS_DATE_HELPER.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) - .date("01 January 2023").dateFormat(DATETIME_PATTERN).locale("en")); - LOG.info("-----------------------------------NEW CLIENT-----------------------------------------"); - final PostClientsRequest newClient = createRandomClientWithDate("01 January 2023"); - final PostClientsResponse clientResponse = CLIENT_HELPER.createClient(newClient); - LOG.info("-----------------------------------NEW LOAN PRODUCT-----------------------------------------"); - PostLoanProductsRequest loanProductsRequest = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct( - repaymentStrategyCode); - final PostLoanProductsResponse loanProductResponse = LOAN_PRODUCT_HELPER.createLoanProduct(loanProductsRequest); - LOG.info("-----------------------------------CREATE CHARGES-----------------------------------------"); - PostChargesResponse penaltyCharge = CHARGES_HELPER.createCharges( - new PostChargesRequest().penalty(false).amount(10.0).chargeCalculationType(ChargeCalculationType.FLAT.getValue()) - .chargeTimeType(ChargeTimeType.SPECIFIED_DUE_DATE.getValue()) - .chargePaymentMode(ChargePaymentMode.REGULAR.getValue()).currencyCode("USD") - .name(Utils.randomStringGenerator("PENALTY_" + Calendar.getInstance().getTimeInMillis(), 5)) - .chargeAppliesTo(1).locale("en").active(true)); - LOG.info("-----------------------------------SUBMIT LOAN-----------------------------------------"); - final PostLoansResponse loanApplicationResult = applyForLoanApplicationForOnePeriod30DaysLongNoInterestPeriodicAccrual( - clientResponse.getResourceId(), loanProductResponse.getResourceId(), "01 January 2023", - LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY); - LOG.info("-----------------------------------APPROVE LOAN-----------------------------------------"); - PostLoansLoanIdResponse approvedLoanResult = LOAN_TRANSACTION_HELPER.approveLoan(loanApplicationResult.getResourceId(), - new PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000.0)).dateFormat(DATETIME_PATTERN) - .approvedOnDate("01 January 2023").locale("en")); - LOG.info("-------------------------------DISBURSE LOAN-------------------------------------------"); - String loanDisbursementUUID = UUID.randomUUID().toString(); - PostLoansLoanIdResponse disbursedLoanResult = LOAN_TRANSACTION_HELPER.disburseLoan(loanApplicationResult.getResourceId(), - new PostLoansLoanIdRequest().actualDisbursementDate("01 January 2023").dateFormat(DATETIME_PATTERN) - .transactionAmount(BigDecimal.valueOf(1000.00)).locale("en").externalId(loanDisbursementUUID)); - Long loanId = disbursedLoanResult.getResourceId(); - BUSINESS_DATE_HELPER.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) - .date("07 January 2023").dateFormat(DATETIME_PATTERN).locale("en")); - LOG.info("-------------------------------DO A PARTIAL REPAYMENT-------------------------------------------"); - String firstRepaymentUUID = UUID.randomUUID().toString(); - PostLoansLoanIdTransactionsResponse firstRepaymentResult = LOAN_TRANSACTION_HELPER.makeLoanRepayment(loanId, - new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("07 January 2023") - .locale("en").transactionAmount(9.0).externalId(firstRepaymentUUID)); - LOG.info("-------------------------------ADD CHARGES-------------------------------------------"); - PostLoansLoanIdChargesResponse penaltyLoanChargeResult = LOAN_TRANSACTION_HELPER.addChargesForLoan(loanId, - new PostLoansLoanIdChargesRequest().chargeId(penaltyCharge.getResourceId()).dateFormat(DATETIME_PATTERN) - .locale("en").amount(10.0).dueDate("07 January 2023")); - LOG.info("-------------------------------CHECK LOAN TRANSACTION ALLOCATION-------------------------------------------"); - checkLoanTransactionAllocation(loanId, firstRepaymentResult.getResourceExternalId(), 9.0, 9.0, 0.0, 0.0, 0.0, 0.0); - LOG.info("-------------------------------DO A PARTIAL REPAYMENT-------------------------------------------"); - String secondRepaymentUUID = UUID.randomUUID().toString(); - PostLoansLoanIdTransactionsResponse secondRepaymentResult = LOAN_TRANSACTION_HELPER.makeLoanRepayment(loanId, - new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("06 January 2023") - .locale("en").transactionAmount(9.0).externalId(secondRepaymentUUID)); - LOG.info("-------------------------------CHECK LOAN TRANSACTION ALLOCATION-------------------------------------------"); - checkLoanTransactionAllocation(loanId, secondRepaymentResult.getResourceExternalId(), 9.0, 9.0, 0.0, 0.0, 0.0, 0.0); - LOG.info("-------------------------------CHECK LOAN TRANSACTION ALLOCATION-------------------------------------------"); - checkLoanTransactionAllocation(loanId, firstRepaymentResult.getResourceExternalId(), 9.0, 9.0, 0.0, 0.0, 0.0, 0.0); - LOG.info("-------------------------------DO A PARTIAL REPAYMENT-------------------------------------------"); - String thirdRepaymentUUID = UUID.randomUUID().toString(); - PostLoansLoanIdTransactionsResponse thirdRepaymentResult = LOAN_TRANSACTION_HELPER.makeLoanRepayment(loanId, - new PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate("07 January 2023") - .locale("en").transactionAmount(9.0).externalId(thirdRepaymentUUID)); - LOG.info("-------------------------------CHECK LOAN TRANSACTION ALLOCATION-------------------------------------------"); - checkLoanTransactionAllocation(loanId, thirdRepaymentResult.getResourceExternalId(), 9.0, 0.0, 0.0, 9.0, 0.0, 0.0); - LOG.info("-------------------------------CHECK LOAN TRANSACTION ALLOCATION-------------------------------------------"); - checkLoanTransactionAllocation(loanId, secondRepaymentResult.getResourceExternalId(), 9.0, 9.0, 0.0, 0.0, 0.0, 0.0); - LOG.info("-------------------------------CHECK LOAN TRANSACTION ALLOCATION-------------------------------------------"); - checkLoanTransactionAllocation(loanId, firstRepaymentResult.getResourceExternalId(), 9.0, 9.0, 0.0, 0.0, 0.0, 0.0); - } - } finally { - GlobalConfigurationHelper.updateIsBusinessDateEnabled(REQUEST_SPEC, RESPONSE_SPEC, Boolean.FALSE); - GlobalConfigurationHelper.updateEnabledFlagForGlobalConfiguration(REQUEST_SPEC, RESPONSE_SPEC, "42", false); - } - } - - private void checkLoanTransactionAllocation(Long loanId, String transactionExternalId, double amount, double principalPortion, - double interestPortion, double feePortion, double penaltyPortion, double overpaymentPortion) { - GetLoansLoanIdTransactionsTransactionIdResponse transactionDetails = LOAN_TRANSACTION_HELPER.getLoanTransactionDetails(loanId, - transactionExternalId); - assertEquals(amount, transactionDetails.getAmount()); - assertEquals(principalPortion, transactionDetails.getPrincipalPortion()); - assertEquals(interestPortion, transactionDetails.getInterestPortion()); - assertEquals(feePortion, transactionDetails.getFeeChargesPortion()); - assertEquals(penaltyPortion, transactionDetails.getPenaltyChargesPortion()); - assertEquals(overpaymentPortion, transactionDetails.getOverpaymentPortion()); - } - private void checkLoanTransactionOrder(Long loanId, String... transactionUUIDs) { LOG.info("-------------------------------CHECK LOAN TRANSACTION ORDER-------------------------------------------"); GetLoansLoanIdResponse loanDetailsResult = LOAN_TRANSACTION_HELPER.getLoanDetails(loanId); @@ -8355,7 +8023,7 @@ public class ClientLoanIntegrationTest { .principal(BigDecimal.valueOf(1000.0)).loanType("individual")); } - private PostLoanProductsRequest createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct(String repaymentStrategyCode) { + private PostLoanProductsRequest createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() { return new PostLoanProductsRequest().name(Utils.uniqueRandomStringGenerator("LOAN_PRODUCT_", 6))// .shortName(Utils.uniqueRandomStringGenerator("", 4))// .description("Loan Product Description")// @@ -8382,7 +8050,8 @@ public class ClientLoanIntegrationTest { .interestType(0)// .isEqualAmortization(false)// .interestCalculationPeriodType(1)// - .transactionProcessingStrategyCode(repaymentStrategyCode)// + .transactionProcessingStrategyCode( + LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY)// .daysInYearType(1)// .daysInMonthType(1)// .canDefineInstallmentAmount(true)//