From 1c2ff818ae88e25f14d4e2823c61d640591bd7c6 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Tue, 4 Jul 2023 21:19:42 +0200 Subject: [PATCH] FINERACT-1905: Distribution logic fix --- .../LoanRepaymentScheduleInstallment.java | 28 +- ...RepaymentScheduleTransactionProcessor.java | 121 +- ...RepaymentScheduleTransactionProcessor.java | 176 ++- ...ymentScheduleTransactionProcessorTest.java | 1065 +++++++++++++++++ ...ymentScheduleTransactionProcessorTest.java | 1029 ++++++++++++++++ ...teRespectiveLoanRepaymentScheduleTest.java | 445 ++++++- 6 files changed, 2701 insertions(+), 163 deletions(-) create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessorTest.java create mode 100644 fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessorTest.java diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java index 430197c8e23..c87fe1bd0e6 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanRepaymentScheduleInstallment.java @@ -410,6 +410,10 @@ public Money payPenaltyChargesComponent(final LocalDate transactionDate, final M final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); Money penaltyPortionOfTransaction = Money.zero(currency); + if (transactionAmountRemaining.isZero()) { + return penaltyPortionOfTransaction; + } + final Money penaltyChargesDue = getPenaltyChargesOutstanding(currency); if (transactionAmountRemaining.isGreaterThanOrEqualTo(penaltyChargesDue)) { this.penaltyChargesPaid = getPenaltyChargesPaid(currency).plus(penaltyChargesDue).getAmount(); @@ -432,7 +436,9 @@ public Money payFeeChargesComponent(final LocalDate transactionDate, final Money final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); Money feePortionOfTransaction = Money.zero(currency); - + if (transactionAmountRemaining.isZero()) { + return feePortionOfTransaction; + } final Money feeChargesDue = getFeeChargesOutstanding(currency); if (transactionAmountRemaining.isGreaterThanOrEqualTo(feeChargesDue)) { this.feeChargesPaid = getFeeChargesPaid(currency).plus(feeChargesDue).getAmount(); @@ -455,7 +461,9 @@ public Money payInterestComponent(final LocalDate transactionDate, final Money t final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); Money interestPortionOfTransaction = Money.zero(currency); - + if (transactionAmountRemaining.isZero()) { + return interestPortionOfTransaction; + } final Money interestDue = getInterestOutstanding(currency); if (transactionAmountRemaining.isGreaterThanOrEqualTo(interestDue)) { this.interestPaid = getInterestPaid(currency).plus(interestDue).getAmount(); @@ -478,7 +486,9 @@ public Money payPrincipalComponent(final LocalDate transactionDate, final Money final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); Money principalPortionOfTransaction = Money.zero(currency); - + if (transactionAmountRemaining.isZero()) { + return principalPortionOfTransaction; + } final Money principalDue = getPrincipalOutstanding(currency); if (transactionAmountRemaining.isGreaterThanOrEqualTo(principalDue)) { this.principalCompleted = getPrincipalCompleted(currency).plus(principalDue).getAmount(); @@ -500,7 +510,9 @@ public Money payPrincipalComponent(final LocalDate transactionDate, final Money public Money waiveInterestComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); Money waivedInterestPortionOfTransaction = Money.zero(currency); - + if (transactionAmountRemaining.isZero()) { + return waivedInterestPortionOfTransaction; + } final Money interestDue = getInterestOutstanding(currency); if (transactionAmountRemaining.isGreaterThanOrEqualTo(interestDue)) { this.interestWaived = getInterestWaived(currency).plus(interestDue).getAmount(); @@ -520,7 +532,9 @@ public Money waiveInterestComponent(final LocalDate transactionDate, final Money public Money waivePenaltyChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); Money waivedPenaltyChargesPortionOfTransaction = Money.zero(currency); - + if (transactionAmountRemaining.isZero()) { + return waivedPenaltyChargesPortionOfTransaction; + } final Money penanltiesDue = getPenaltyChargesOutstanding(currency); if (transactionAmountRemaining.isGreaterThanOrEqualTo(penanltiesDue)) { this.penaltyChargesWaived = getPenaltyChargesWaived(currency).plus(penanltiesDue).getAmount(); @@ -540,7 +554,9 @@ public Money waivePenaltyChargesComponent(final LocalDate transactionDate, final public Money waiveFeeChargesComponent(final LocalDate transactionDate, final Money transactionAmountRemaining) { final MonetaryCurrency currency = transactionAmountRemaining.getCurrency(); Money waivedFeeChargesPortionOfTransaction = Money.zero(currency); - + if (transactionAmountRemaining.isZero()) { + return waivedFeeChargesPortionOfTransaction; + } final Money feesDue = getFeeChargesOutstanding(currency); if (transactionAmountRemaining.isGreaterThanOrEqualTo(feesDue)) { this.feeChargesWaived = getFeeChargesWaived(currency).plus(feesDue).getAmount(); 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 74c338fbf8a..f9e03865218 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 @@ -56,15 +56,6 @@ public String getName() { return STRATEGY_NAME; } - @Override - protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex, - final List installments, final LocalDate transactionDate) { - - final LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex); - - return transactionDate.isBefore(currentInstallment.getDueDate()); - } - /** * For early/'in advance' repayments */ @@ -73,35 +64,10 @@ protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanR final List installments, final LoanTransaction loanTransaction, final Money paymentInAdvance, List transactionMappings, Set charges) { - return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, paymentInAdvance, 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 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; + final MonetaryCurrency currency = paymentInAdvance.getCurrency(); + Money transactionAmountRemaining = paymentInAdvance; Money principalPortion = Money.zero(currency); Money interestPortion = Money.zero(currency); Money feeChargesPortion = Money.zero(currency); @@ -156,7 +122,7 @@ protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepa calculatedPenaltyCharge = transactionAmountRemaining; } } else { - calculatedPenaltyCharge = transactionAmountUnprocessed; + calculatedPenaltyCharge = transactionAmountRemaining; } subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, calculatedPenaltyCharge); transactionAmountRemaining = transactionAmountRemaining.minus(subPenaltyPortion); @@ -168,7 +134,7 @@ protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepa calculatedFeeCharge = transactionAmountRemaining; } } else { - calculatedFeeCharge = transactionAmountUnprocessed; + calculatedFeeCharge = transactionAmountRemaining; } subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, calculatedFeeCharge); transactionAmountRemaining = transactionAmountRemaining.minus(subFeePortion); @@ -200,6 +166,85 @@ protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepa 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 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 9a47114045f..6a8bf1e6a37 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 @@ -56,25 +56,17 @@ public String getName() { return STRATEGY_NAME; } - @Override - protected boolean isTransactionInAdvanceOfInstallment(final int currentInstallmentIndex, - final List installments, final LocalDate transactionDate) { - - final LoanRepaymentScheduleInstallment currentInstallment = installments.get(currentInstallmentIndex); - - return transactionDate.isBefore(currentInstallment.getDueDate()); - } - /** * For early/'in advance' repayments */ @Override protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, - final List installments, final LoanTransaction loanTransaction, final Money paymentInAdvance, + final List installments, final LoanTransaction loanTransaction, final Money inAdvancePayment, List transactionMappings, Set charges) { final LocalDate transactionDate = loanTransaction.getTransactionDate(); - final MonetaryCurrency currency = paymentInAdvance.getCurrency(); - Money transactionAmountRemaining = paymentInAdvance; + + final MonetaryCurrency currency = inAdvancePayment.getCurrency(); + Money transactionAmountRemaining = inAdvancePayment; Money principalPortion = Money.zero(currency); Money interestPortion = Money.zero(currency); Money feeChargesPortion = Money.zero(currency); @@ -104,27 +96,70 @@ protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanR } loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); } else { - // In advance penalty, interest, principal, fee + // Due penalty, interest, principal, fee, In advance penalty, interest, principal, fee + boolean ignoreDueDateCheck = false; + boolean rerun = false; - Money subPenaltyPortion; - subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(subPenaltyPortion); - penaltyChargesPortion = penaltyChargesPortion.add(subPenaltyPortion); + List orderedLoanChargesByDueDate = charges.stream().filter(LoanCharge::isActive).filter(LoanCharge::isNotFullyPaid) + .filter(loanCharge -> loanCharge.getEffectiveDueDate() == null + || !loanCharge.getEffectiveDueDate().isAfter(transactionDate)) + .sorted(LoanChargeEffectiveDueDateComparator.INSTANCE).toList(); + Money calculatedPenaltyCharge = Money.zero(currency); + Money calculatedFeeCharge = Money.zero(currency); + // Calculate the amount of due charges + for (LoanCharge charge : orderedLoanChargesByDueDate) { + if (charge.isPenaltyCharge()) { + calculatedPenaltyCharge = calculatedPenaltyCharge.add(charge.getAmount(currency)); + } else { + calculatedFeeCharge = calculatedFeeCharge.add(charge.getAmount(currency)); + } + } - Money subInterestPortion; - subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(subInterestPortion); - interestPortion = interestPortion.add(subInterestPortion); + do { + Money subPenaltyPortion; + if (!ignoreDueDateCheck) { + if (calculatedPenaltyCharge.isGreaterThan(transactionAmountRemaining)) { + calculatedPenaltyCharge = transactionAmountRemaining; + } + } else { + calculatedPenaltyCharge = transactionAmountRemaining; + } + subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, calculatedPenaltyCharge); + transactionAmountRemaining = transactionAmountRemaining.minus(subPenaltyPortion); + penaltyChargesPortion = penaltyChargesPortion.add(subPenaltyPortion); - Money subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(subPrincipalPortion); - principalPortion = principalPortion.add(subPrincipalPortion); + Money subInterestPortion; + if (ignoreDueDateCheck || !transactionDate.isBefore(currentInstallment.getDueDate())) { + subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(subInterestPortion); + interestPortion = interestPortion.add(subInterestPortion); + } - Money subFeePortion; - subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(subFeePortion); - feeChargesPortion = feeChargesPortion.add(subFeePortion); + if (ignoreDueDateCheck || !transactionDate.isBefore(currentInstallment.getDueDate())) { + Money subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(subPrincipalPortion); + principalPortion = principalPortion.add(subPrincipalPortion); + } + Money subFeePortion; + if (!ignoreDueDateCheck) { + if (calculatedFeeCharge.isGreaterThan(transactionAmountRemaining)) { + calculatedFeeCharge = transactionAmountRemaining; + } + } else { + calculatedFeeCharge = transactionAmountRemaining; + } + subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, calculatedFeeCharge); + transactionAmountRemaining = transactionAmountRemaining.minus(subFeePortion); + feeChargesPortion = feeChargesPortion.add(subFeePortion); + + // If the transactionAmountRemaining is greater than zero, rerun the allocation without due date check + // to distribute the in advance portions + if (transactionAmountRemaining.isGreaterThanZero()) { + ignoreDueDateCheck = true; + } + rerun = !rerun; + } while (ignoreDueDateCheck && rerun); loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); } if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) { @@ -139,12 +174,11 @@ protected Money handleTransactionThatIsPaymentInAdvanceOfInstallment(final LoanR */ @Override protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, - final List installments, final LoanTransaction loanTransaction, - final Money transactionAmountUnprocessed, List transactionMappings, - Set charges) { + final List installments, final LoanTransaction loanTransaction, final Money latePayment, + List transactionMappings, Set charges) { - return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, transactionAmountUnprocessed, - transactionMappings, charges); + return handleTransactionThatIsOnTimePaymentOfInstallment(currentInstallment, loanTransaction, latePayment, transactionMappings, + charges); } /** @@ -152,13 +186,13 @@ protected Money handleTransactionThatIsALateRepaymentOfInstallment(final LoanRep */ @Override protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepaymentScheduleInstallment currentInstallment, - final LoanTransaction loanTransaction, final Money transactionAmountUnprocessed, + final LoanTransaction loanTransaction, final Money onTimePayment, List transactionMappings, Set charges) { final LocalDate transactionDate = loanTransaction.getTransactionDate(); - final MonetaryCurrency currency = transactionAmountUnprocessed.getCurrency(); - Money transactionAmountRemaining = transactionAmountUnprocessed; + final MonetaryCurrency currency = onTimePayment.getCurrency(); + Money transactionAmountRemaining = onTimePayment; Money principalPortion = Money.zero(currency); Money interestPortion = Money.zero(currency); Money feeChargesPortion = Money.zero(currency); @@ -188,68 +222,22 @@ protected Money handleTransactionThatIsOnTimePaymentOfInstallment(final LoanRepa } loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); } else { - // Due penalty, interest, principal, fee, In advance penalty, interest, principal, fee - boolean ignoreDueDateCheck = false; - boolean rerun = false; - - List orderedLoanChargesByDueDate = charges.stream().filter(LoanCharge::isActive).filter(LoanCharge::isNotFullyPaid) - .filter(loanCharge -> loanCharge.getEffectiveDueDate() == null - || !loanCharge.getEffectiveDueDate().isAfter(transactionDate)) - .sorted(LoanChargeEffectiveDueDateComparator.INSTANCE).toList(); - Money calculatedPenaltyCharge = Money.zero(currency); - Money calculatedFeeCharge = Money.zero(currency); - // Calculate the amount of due charges - for (LoanCharge charge : orderedLoanChargesByDueDate) { - if (charge.isPenaltyCharge()) { - calculatedPenaltyCharge = calculatedPenaltyCharge.add(charge.getAmount(currency)); - } else { - calculatedFeeCharge = calculatedFeeCharge.add(charge.getAmount(currency)); - } - } - - do { - Money subPenaltyPortion; - if (!ignoreDueDateCheck) { - if (calculatedPenaltyCharge.isGreaterThan(transactionAmountRemaining)) { - calculatedPenaltyCharge = transactionAmountRemaining; - } - } else { - calculatedPenaltyCharge = transactionAmountUnprocessed; - } - subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, calculatedPenaltyCharge); - transactionAmountRemaining = transactionAmountRemaining.minus(subPenaltyPortion); - penaltyChargesPortion = penaltyChargesPortion.add(subPenaltyPortion); + Money subPenaltyPortion = currentInstallment.payPenaltyChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(subPenaltyPortion); + penaltyChargesPortion = penaltyChargesPortion.add(subPenaltyPortion); - Money subInterestPortion; - if (ignoreDueDateCheck || !transactionDate.isBefore(currentInstallment.getDueDate())) { - subInterestPortion = currentInstallment.payInterestComponent(transactionDate, transactionAmountRemaining); - transactionAmountRemaining = transactionAmountRemaining.minus(subInterestPortion); - interestPortion = interestPortion.add(subInterestPortion); - } + 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 subPrincipalPortion = currentInstallment.payPrincipalComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(subPrincipalPortion); + principalPortion = principalPortion.add(subPrincipalPortion); - Money subFeePortion; - if (!ignoreDueDateCheck) { - if (calculatedFeeCharge.isGreaterThan(transactionAmountRemaining)) { - calculatedFeeCharge = transactionAmountRemaining; - } - } else { - calculatedFeeCharge = transactionAmountUnprocessed; - } - subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, calculatedFeeCharge); - transactionAmountRemaining = transactionAmountRemaining.minus(subFeePortion); - feeChargesPortion = feeChargesPortion.add(subFeePortion); + Money subFeePortion = currentInstallment.payFeeChargesComponent(transactionDate, transactionAmountRemaining); + transactionAmountRemaining = transactionAmountRemaining.minus(subFeePortion); + feeChargesPortion = feeChargesPortion.add(subFeePortion); - // If the transactionAmountRemaining is greater than zero, rerun the allocation without due date check - // to distribute the in advance portions - if (transactionAmountRemaining.isGreaterThanZero()) { - ignoreDueDateCheck = true; - } - rerun = !rerun; - } while (ignoreDueDateCheck && rerun); loanTransaction.updateComponents(principalPortion, interestPortion, feeChargesPortion, penaltyChargesPortion); } if (principalPortion.plus(interestPortion).plus(feeChargesPortion).plus(penaltyChargesPortion).isGreaterThanZero()) { 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 new file mode 100644 index 00000000000..f24a7d7bd35 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessorTest.java @@ -0,0 +1,1065 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.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.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.domain.ActionContext; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +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.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessorTest { + + private static final MonetaryCurrency MONETARY_CURRENCY = new MonetaryCurrency("USD", 2, 1); + private static final MockedStatic MONEY_HELPER = Mockito.mockStatic(MoneyHelper.class); + private final LocalDate transactionDate = LocalDate.of(2023, 7, 11); + private final LocalDate firstInstallmentToDate = LocalDate.of(2023, 7, 11); + 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.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; + @Mock + private Set charges; + @Mock + private Office office; + @Mock + private Loan loan; + @Mock + private List transactionMappings; + + @BeforeAll + public static void init() { + MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN); + } + + @AfterAll + public static void destruct() { + MONEY_HELPER.close(); + } + + @BeforeEach + public void setUp() { + underTest = new DuePenFeeIntPriInAdvancePriPenFeeIntLoanRepaymentScheduleTransactionProcessor(); + Mockito.reset(charges, transactionMappings); + + ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); + ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); + ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, transactionDate))); + } + + // IN ADVANCE + @Test + public void inAdvancePaymentOfPrincipal() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), + 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)); + } + + @Test + public void inAdvancePaymentOfInterest() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), + 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() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), + 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() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + 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)); + 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 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() { + 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), + BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), 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); + + // 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() { + 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), + BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), 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); + + // 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() { + 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), + BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), 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); + + // 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() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + 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)); + 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 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 = 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 + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), + BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), 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); + + // 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 = 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 + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), + BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), 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); + + // 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 = 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 + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(2L), + BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), 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); + + // 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)); + } + + // ON TIME + @Test + public void onTimePaymentOfPrincipal() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), + 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)); + } + + @Test + public void onTimePaymentOfInterest() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), + 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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), + 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() { + Money transactionAmount = ten; + 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)); + LoanTransaction loanTransaction = Mockito + .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)).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() { + 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)); + LoanTransaction loanTransaction = Mockito + .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)).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() { + Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(5)); + 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)); + LoanTransaction loanTransaction = Mockito + .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)).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() { + Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(7)); + 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)); + LoanTransaction loanTransaction = Mockito + .spy(LoanTransaction.repayment(office, transactionAmount, null, firstInstallmentDueDate, ExternalId.empty())); + 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() { + Money transactionAmount = ten; + 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)); + LoanTransaction loanTransaction = Mockito + .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)).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)); + } + + // LATE + @Test + public void latePaymentOfPrincipal() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), + 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)); + } + + @Test + public void latePaymentOfInterest() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), + 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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), + 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() { + Money transactionAmount = ten; + 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)); + LoanTransaction loanTransaction = Mockito + .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)).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() { + 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)); + LoanTransaction loanTransaction = Mockito + .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)).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() { + Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(5)); + 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)); + LoanTransaction loanTransaction = Mockito + .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)).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() { + Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(7)); + 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)); + LoanTransaction loanTransaction = Mockito + .spy(LoanTransaction.repayment(office, transactionAmount, null, lateDate, ExternalId.empty())); + 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() { + Money transactionAmount = ten; + 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)); + LoanTransaction loanTransaction = Mockito + .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)).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)); + } +} 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 new file mode 100644 index 00000000000..f8cfc49f381 --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessorTest.java @@ -0,0 +1,1029 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.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.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.stream.Stream; +import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; +import org.apache.fineract.infrastructure.core.domain.ActionContext; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.organisation.office.domain.Office; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +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.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +public class DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessorTest { + + private static final MonetaryCurrency MONETARY_CURRENCY = new MonetaryCurrency("USD", 2, 1); + private static final MockedStatic MONEY_HELPER = Mockito.mockStatic(MoneyHelper.class); + private final LocalDate transactionDate = LocalDate.of(2023, 7, 11); + private final LocalDate firstInstallmentToDate = LocalDate.of(2023, 7, 11); + 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.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 DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor underTest; + @Mock + private Set charges; + @Mock + private Office office; + @Mock + private Loan loan; + @Mock + private List transactionMappings; + + @BeforeAll + public static void init() { + MONEY_HELPER.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.HALF_EVEN); + } + + @AfterAll + public static void destruct() { + MONEY_HELPER.close(); + } + + @BeforeEach + public void setUp() { + underTest = new DuePenIntPriFeeInAdvancePenIntPriFeeLoanRepaymentScheduleTransactionProcessor(); + Mockito.reset(charges, transactionMappings); + + ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null)); + ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT); + ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, transactionDate))); + } + + // IN ADVANCE + @Test + public void inAdvancePaymentOfPrincipal() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), + 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)); + } + + @Test + public void inAdvancePaymentOfInterest() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), + 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() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), + 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() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + 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)); + 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 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() { + 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), + BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), 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); + + // 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() { + 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), + BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), 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); + + // 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() { + 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), + BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), 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); + + // 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() { + Mockito.when(charges.stream()).thenReturn(Stream.empty()); + Money transactionAmount = ten; + 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)); + 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(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 = 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 + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), + BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), 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); + + // 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 = 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 + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(2L), + BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), 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); + + // 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 = 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 + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(2L), + BigDecimal.valueOf(2L), BigDecimal.valueOf(2L), 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); + + // 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)); + } + + // ON TIME + @Test + public void onTimePaymentOfPrincipal() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), + 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)); + } + + @Test + public void onTimePaymentOfInterest() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), + 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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), + 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() { + Money transactionAmount = ten; + 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)); + LoanTransaction loanTransaction = Mockito + .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() { + 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)); + LoanTransaction loanTransaction = Mockito + .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() { + Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(5)); + 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)); + LoanTransaction loanTransaction = Mockito + .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() { + Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(8)); + 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)); + LoanTransaction loanTransaction = Mockito + .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() { + Money transactionAmount = ten; + 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)); + LoanTransaction loanTransaction = Mockito + .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)); + } + + // LATE + @Test + public void latePaymentOfPrincipal() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), + 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)); + } + + @Test + public void latePaymentOfInterest() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(5L), + 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() { + Money transactionAmount = ten; + LoanRepaymentScheduleInstallment installment = Mockito + .spy(new LoanRepaymentScheduleInstallment(loan, 1, firstInstallmentToDate, firstInstallmentDueDate, BigDecimal.valueOf(3L), + 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() { + Money transactionAmount = ten; + 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)); + LoanTransaction loanTransaction = Mockito + .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() { + 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)); + LoanTransaction loanTransaction = Mockito + .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() { + Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(5)); + 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)); + LoanTransaction loanTransaction = Mockito + .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() { + Money transactionAmount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(8)); + 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)); + LoanTransaction loanTransaction = Mockito + .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() { + Money transactionAmount = ten; + 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)); + LoanTransaction loanTransaction = Mockito + .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)); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DueDateRespectiveLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DueDateRespectiveLoanRepaymentScheduleTest.java index 03bd6635998..26459b6337b 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/DueDateRespectiveLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/DueDateRespectiveLoanRepaymentScheduleTest.java @@ -19,6 +19,7 @@ package org.apache.fineract.integrationtests; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNull; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -30,8 +31,12 @@ import io.restassured.specification.ResponseSpecification; import java.time.LocalDate; import java.util.HashMap; +import java.util.List; import org.apache.fineract.client.models.BusinessDateRequest; import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest; +import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdResponse; +import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType; @@ -49,6 +54,7 @@ import org.apache.fineract.integrationtests.common.loans.LoanStatusChecker; import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.apache.fineract.integrationtests.inlinecob.InlineLoanCOBHelper; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; @@ -64,7 +70,7 @@ public class DueDateRespectiveLoanRepaymentScheduleTest { private BusinessDateHelper businessDateHelper; private LoanTransactionHelper loanTransactionHelper; private LoanRescheduleRequestHelper loanRescheduleRequestHelper; - + private InlineLoanCOBHelper inlineLoanCOBHelper; private AccountHelper accountHelper; @BeforeEach @@ -78,6 +84,7 @@ public void setup() { this.loanRescheduleRequestHelper = new LoanRescheduleRequestHelper(this.requestSpec, this.responseSpec); this.businessDateHelper = new BusinessDateHelper(); this.accountHelper = new AccountHelper(this.requestSpec, this.responseSpec); + inlineLoanCOBHelper = new InlineLoanCOBHelper(requestSpec, responseSpec); } // Scenario1: @@ -104,12 +111,12 @@ public void scenario1() { Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", true)); - final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "1", "1", "0", + final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "30", "1", "0", LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023"); - final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "1", "1", "1", "0", + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "30", "30", "1", "0", LoanApplicationTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, "01 January 2023", "01 January 2023"); @@ -216,12 +223,12 @@ public void scenario2() { Integer fee = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false)); - final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "1", "1", "0", + final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "30", "1", "0", LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023"); - final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "1", "1", "1", "0", + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "30", "30", "1", "0", LoanApplicationTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, "01 January 2023", "01 January 2023"); @@ -324,12 +331,12 @@ public void scenario3() { Integer fee = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false)); - final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "1", "1", "0", + final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "30", "1", "0", LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023"); - final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "1", "1", "1", "0", + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "30", "30", "1", "0", LoanApplicationTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, "01 January 2023", "01 January 2023"); @@ -441,12 +448,12 @@ public void scenario4() { Integer fee = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false)); - final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "1", "3", "0", + final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "30", "3", "0", LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023"); - final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "3", "1", "3", "0", + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "90", "30", "3", "0", LoanApplicationTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, "01 January 2023", "01 January 2023"); @@ -530,12 +537,12 @@ public void scenario5() { Integer fee = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "50", false)); - final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "1", "3", "0", + final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "30", "3", "0", LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023"); - final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "3", "1", "3", "0", + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "90", "30", "3", "0", LoanApplicationTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, "01 January 2023", "01 January 2023"); @@ -678,12 +685,12 @@ public void scenario6() { Integer fee = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "20", false)); - final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "1", "1", "0", + final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "30", "1", "0", LoanProductTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023"); - final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "1", "1", "1", "0", + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "30", "30", "1", "0", LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY, "01 January 2023", "01 January 2023"); @@ -753,12 +760,12 @@ public void scenario7() { Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "15", true)); - final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "1", "1", "0", + final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "30", "1", "0", LoanProductTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023"); - final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "1", "1", "1", "0", + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "30", "30", "1", "0", LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY, "01 January 2023", "01 January 2023"); @@ -890,12 +897,12 @@ public void scenario8() { ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "20", false)); Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "15", true)); - final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "1", "1", "0", + final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "30", "1", "0", LoanProductTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023"); - final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "1", "1", "1", "0", + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "30", "30", "1", "0", LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY, "01 January 2023", "01 January 2023"); @@ -909,7 +916,7 @@ public void scenario8() { loanStatusHashMap = loanTransactionHelper.disburseLoanWithTransactionAmount("01 January 2023", loanID, "1000"); LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); - final String requestJSON = new LoanRescheduleRequestTestBuilder().updateRescheduleFromDate("1 February 2023") + final String requestJSON = new LoanRescheduleRequestTestBuilder().updateRescheduleFromDate("31 January 2023") .updateAdjustedDueDate("01 March 2023").updateSubmittedOnDate("25 January 2023").updateGraceOnPrincipal(null) .updateGraceOnInterest(null).updateExtraTerms(null).build(loanID.toString()); final HashMap map = new HashMap<>(); @@ -1114,12 +1121,12 @@ public void scenario9() { ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "20", false)); Integer penalty = ChargesHelper.createCharges(requestSpec, responseSpec, ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "15", true)); - final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "1", "1", "0", + final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "30", "1", "0", LoanProductTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY, assetAccount, incomeAccount, expenseAccount, overpaymentAccount); final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023"); - final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "1", "1", "1", "0", + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "30", "30", "1", "0", LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY, "01 January 2023", "01 January 2023"); @@ -1133,7 +1140,7 @@ public void scenario9() { loanStatusHashMap = loanTransactionHelper.disburseLoanWithTransactionAmount("01 January 2023", loanID, "1000"); LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); - final String requestJSON = new LoanRescheduleRequestTestBuilder().updateRescheduleFromDate("1 February 2023") + final String requestJSON = new LoanRescheduleRequestTestBuilder().updateRescheduleFromDate("31 January 2023") .updateAdjustedDueDate("01 March 2023").updateSubmittedOnDate("25 January 2023").updateGraceOnPrincipal(null) .updateGraceOnInterest(null).updateExtraTerms(null).build(loanID.toString()); final HashMap map = new HashMap<>(); @@ -1397,13 +1404,401 @@ public void scenario9() { } } + // Scenario10: + // DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY + // 1. Disburse the loan + // 2. Snooze fee + // 3. Merchant issued refund (partial) + // 4. Charge adjustment (same day) + // 5. Merchant issued refund (rest) + @Test + public void scenario10() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.05.14").dateFormat("yyyy.MM.dd").locale("en")); + GlobalConfigurationHelper.updateChargeAccrualDateConfiguration(requestSpec, responseSpec, "submitted-date"); + + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + Integer fee = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "3.65", false)); + final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "30", "1", "0", + LoanProductTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY, + assetAccount, incomeAccount, expenseAccount, overpaymentAccount); + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023"); + + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "30", "30", "1", "0", + LoanApplicationTestBuilder.DUE_PENALTY_INTEREST_PRINCIPAL_FEE_IN_ADVANCE_PENALTY_INTEREST_PRINCIPAL_FEE_STRATEGY, + "14 May 2023", "14 May 2023"); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = loanTransactionHelper.approveLoan("14 May 2023", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + loanStatusHashMap = loanTransactionHelper.disburseLoanWithTransactionAmount("14 May 2023", loanID, "127.95"); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.06.11").dateFormat("yyyy.MM.dd").locale("en")); + + final String requestJSON = new LoanRescheduleRequestTestBuilder().updateRescheduleFromDate("13 June 2023") + .updateAdjustedDueDate("13 July 2023").updateSubmittedOnDate("11 June 2023").updateGraceOnPrincipal(null) + .updateGraceOnInterest(null).updateExtraTerms(null).build(loanID.toString()); + + final HashMap map = new HashMap<>(); + map.put("locale", "en"); + map.put("dateFormat", "dd MMMM yyyy"); + map.put("approvedOnDate", "11 June 2023"); + final String aproveRequestJSON = new Gson().toJson(map); + + Integer loanRescheduleRequestId = this.loanRescheduleRequestHelper.createLoanRescheduleRequest(requestJSON); + this.loanRescheduleRequestHelper.approveLoanRescheduleRequest(loanRescheduleRequestId, aproveRequestJSON); + Integer penalty1LoanChargeId = loanTransactionHelper.addChargesForLoan(loanID, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(fee), "13 July 2023", "3.65")); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.06.12").dateFormat("yyyy.MM.dd").locale("en")); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanID.longValue())); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails((long) loanID); + assertEquals(131.6, loanDetails.getSummary().getTotalOutstanding()); + assertEquals(131.6, loanDetails.getRepaymentSchedule().getTotalOutstanding()); + assertEquals(3.65, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesDue()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesPaid()); + assertEquals(3.65, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesDue()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesPaid()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding()); + assertEquals(127.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid()); + assertEquals(127.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding()); + assertTrue(loanDetails.getStatus().getActive()); + + assertNull(loanDetails.getTransactions().get(0).getReversedOnDate()); + assertTrue(loanDetails.getTransactions().get(0).getTransactionRelations().isEmpty()); + assertTrue(loanDetails.getTransactions().get(0).getType().getDisbursement()); + assertEquals(127.95, loanDetails.getTransactions().get(0).getAmount()); + assertEquals(0.0, loanDetails.getTransactions().get(0).getPrincipalPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(0).getPenaltyChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(0).getOverpaymentPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(0).getInterestPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(0).getFeeChargesPortion()); + assertEquals(127.95, loanDetails.getTransactions().get(0).getOutstandingLoanBalance()); + + assertNull(loanDetails.getTransactions().get(1).getReversedOnDate()); + assertTrue(loanDetails.getTransactions().get(1).getTransactionRelations().isEmpty()); + assertTrue(loanDetails.getTransactions().get(1).getType().getAccrual()); + assertEquals(3.65, loanDetails.getTransactions().get(1).getAmount()); + assertEquals(0.0, loanDetails.getTransactions().get(1).getPrincipalPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(1).getPenaltyChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(1).getOverpaymentPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(1).getInterestPortion()); + assertEquals(3.65, loanDetails.getTransactions().get(1).getFeeChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(1).getOutstandingLoanBalance()); + + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.06.17").dateFormat("yyyy.MM.dd").locale("en")); + PostLoansLoanIdTransactionsResponse merchantIssuedRefund1 = loanTransactionHelper.makeMerchantIssuedRefund(Long.valueOf(loanID), + new PostLoansLoanIdTransactionsRequest().locale("en").dateFormat("dd MMMM yyyy").transactionDate("17 June 2023") + .transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails((long) loanID); + assertEquals(6.6, loanDetails.getSummary().getTotalOutstanding()); + assertEquals(6.6, loanDetails.getRepaymentSchedule().getTotalOutstanding()); + assertEquals(3.65, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesDue()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesPaid()); + assertEquals(3.65, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesDue()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesPaid()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding()); + assertEquals(127.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue()); + assertEquals(125.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid()); + assertEquals(2.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding()); + assertTrue(loanDetails.getStatus().getActive()); + + assertNull(loanDetails.getTransactions().get(2).getReversedOnDate()); + assertTrue(loanDetails.getTransactions().get(2).getTransactionRelations().isEmpty()); + assertTrue(loanDetails.getTransactions().get(2).getType().getMerchantIssuedRefund()); + assertEquals(125.0, loanDetails.getTransactions().get(2).getAmount()); + assertEquals(125.0, loanDetails.getTransactions().get(2).getPrincipalPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(2).getPenaltyChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(2).getOverpaymentPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(2).getInterestPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(2).getFeeChargesPortion()); + assertEquals(2.95, loanDetails.getTransactions().get(2).getOutstandingLoanBalance()); + + PostLoansLoanIdChargesChargeIdResponse chargeAdjustmentResponse = this.loanTransactionHelper.chargeAdjustment((long) loanID, + (long) penalty1LoanChargeId, new PostLoansLoanIdChargesChargeIdRequest().amount(3.65)); + + loanDetails = loanTransactionHelper.getLoanDetails((long) loanID); + assertEquals(2.95, loanDetails.getSummary().getTotalOutstanding()); + assertEquals(2.95, loanDetails.getRepaymentSchedule().getTotalOutstanding()); + assertEquals(3.65, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesDue()); + assertEquals(0.70, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesPaid()); + assertEquals(2.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesDue()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesPaid()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding()); + assertEquals(127.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue()); + assertEquals(127.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding()); + assertTrue(loanDetails.getStatus().getActive()); + + assertNull(loanDetails.getTransactions().get(3).getReversedOnDate()); + assertFalse(loanDetails.getTransactions().get(3).getTransactionRelations().isEmpty()); + assertEquals((long) penalty1LoanChargeId, + loanDetails.getTransactions().get(3).getTransactionRelations().iterator().next().getToLoanCharge()); + assertTrue(loanDetails.getTransactions().get(3).getType().getChargeAdjustment()); + assertEquals(3.65, loanDetails.getTransactions().get(3).getAmount()); + assertEquals(2.95, loanDetails.getTransactions().get(3).getPrincipalPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(3).getPenaltyChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(3).getOverpaymentPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(3).getInterestPortion()); + assertEquals(0.7, loanDetails.getTransactions().get(3).getFeeChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(3).getOutstandingLoanBalance()); + + PostLoansLoanIdTransactionsResponse merchantIssuedRefund2 = loanTransactionHelper.makeMerchantIssuedRefund(Long.valueOf(loanID), + new PostLoansLoanIdTransactionsRequest().locale("en").dateFormat("dd MMMM yyyy").transactionDate("17 June 2023") + .transactionAmount(2.95)); + + loanDetails = loanTransactionHelper.getLoanDetails((long) loanID); + assertEquals(0.0, loanDetails.getSummary().getTotalOutstanding()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getTotalOutstanding()); + assertEquals(3.65, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesDue()); + assertEquals(3.65, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesPaid()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesDue()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesPaid()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding()); + assertEquals(127.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue()); + assertEquals(127.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding()); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + + assertNull(loanDetails.getTransactions().get(4).getReversedOnDate()); + assertTrue(loanDetails.getTransactions().get(4).getTransactionRelations().isEmpty()); + assertTrue(loanDetails.getTransactions().get(4).getType().getMerchantIssuedRefund()); + assertEquals(2.95, loanDetails.getTransactions().get(4).getAmount()); + assertEquals(0.0, loanDetails.getTransactions().get(4).getPrincipalPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(4).getPenaltyChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(4).getOverpaymentPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(4).getInterestPortion()); + assertEquals(2.95, loanDetails.getTransactions().get(4).getFeeChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(4).getOutstandingLoanBalance()); + + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + GlobalConfigurationHelper.updateChargeAccrualDateConfiguration(requestSpec, responseSpec, "due-date"); + } + } + + // Scenario11: + // DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY + // 1. Disburse the loan + // 2. Snooze fee + // 3. Merchant issued refund (partial) + // 4. Charge adjustment (same day) + // 5. Merchant issued refund (rest) + @Test + public void scenario11() { + try { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.TRUE); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.05.14").dateFormat("yyyy.MM.dd").locale("en")); + GlobalConfigurationHelper.updateChargeAccrualDateConfiguration(requestSpec, responseSpec, "submitted-date"); + + final Account assetAccount = this.accountHelper.createAssetAccount(); + final Account incomeAccount = this.accountHelper.createIncomeAccount(); + final Account expenseAccount = this.accountHelper.createExpenseAccount(); + final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); + + Integer fee = ChargesHelper.createCharges(requestSpec, responseSpec, + ChargesHelper.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT, "3.65", false)); + final Integer loanProductID = createLoanProductWithNoAccountingNoInterest("1000", "30", "1", "0", + LoanProductTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, + assetAccount, incomeAccount, expenseAccount, overpaymentAccount); + final Integer clientID = ClientHelper.createClient(requestSpec, responseSpec, "01 January 2023"); + + final Integer loanID = applyForLoanApplication(clientID, loanProductID, "1000", "30", "30", "1", "0", + LoanApplicationTestBuilder.DUE_PENALTY_FEE_INTEREST_PRINCIPAL_IN_ADVANCE_PRINCIPAL_PENALTY_FEE_INTEREST_STRATEGY, + "14 May 2023", "14 May 2023"); + + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(requestSpec, responseSpec, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + loanStatusHashMap = loanTransactionHelper.approveLoan("14 May 2023", loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + loanStatusHashMap = loanTransactionHelper.disburseLoanWithTransactionAmount("14 May 2023", loanID, "127.95"); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.06.11").dateFormat("yyyy.MM.dd").locale("en")); + + final String requestJSON = new LoanRescheduleRequestTestBuilder().updateRescheduleFromDate("13 June 2023") + .updateAdjustedDueDate("13 July 2023").updateSubmittedOnDate("11 June 2023").updateGraceOnPrincipal(null) + .updateGraceOnInterest(null).updateExtraTerms(null).build(loanID.toString()); + + final HashMap map = new HashMap<>(); + map.put("locale", "en"); + map.put("dateFormat", "dd MMMM yyyy"); + map.put("approvedOnDate", "11 June 2023"); + final String aproveRequestJSON = new Gson().toJson(map); + + Integer loanRescheduleRequestId = this.loanRescheduleRequestHelper.createLoanRescheduleRequest(requestJSON); + this.loanRescheduleRequestHelper.approveLoanRescheduleRequest(loanRescheduleRequestId, aproveRequestJSON); + Integer penalty1LoanChargeId = loanTransactionHelper.addChargesForLoan(loanID, + LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(String.valueOf(fee), "13 July 2023", "3.65")); + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.06.12").dateFormat("yyyy.MM.dd").locale("en")); + inlineLoanCOBHelper.executeInlineCOB(List.of(loanID.longValue())); + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails((long) loanID); + assertEquals(131.6, loanDetails.getSummary().getTotalOutstanding()); + assertEquals(131.6, loanDetails.getRepaymentSchedule().getTotalOutstanding()); + assertEquals(3.65, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesDue()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesPaid()); + assertEquals(3.65, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesDue()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesPaid()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding()); + assertEquals(127.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid()); + assertEquals(127.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding()); + assertTrue(loanDetails.getStatus().getActive()); + + assertNull(loanDetails.getTransactions().get(0).getReversedOnDate()); + assertTrue(loanDetails.getTransactions().get(0).getTransactionRelations().isEmpty()); + assertTrue(loanDetails.getTransactions().get(0).getType().getDisbursement()); + assertEquals(127.95, loanDetails.getTransactions().get(0).getAmount()); + assertEquals(0.0, loanDetails.getTransactions().get(0).getPrincipalPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(0).getPenaltyChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(0).getOverpaymentPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(0).getInterestPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(0).getFeeChargesPortion()); + assertEquals(127.95, loanDetails.getTransactions().get(0).getOutstandingLoanBalance()); + + assertNull(loanDetails.getTransactions().get(1).getReversedOnDate()); + assertTrue(loanDetails.getTransactions().get(1).getTransactionRelations().isEmpty()); + assertTrue(loanDetails.getTransactions().get(1).getType().getAccrual()); + assertEquals(3.65, loanDetails.getTransactions().get(1).getAmount()); + assertEquals(0.0, loanDetails.getTransactions().get(1).getPrincipalPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(1).getPenaltyChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(1).getOverpaymentPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(1).getInterestPortion()); + assertEquals(3.65, loanDetails.getTransactions().get(1).getFeeChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(1).getOutstandingLoanBalance()); + + businessDateHelper.updateBusinessDate(new BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName()) + .date("2023.06.17").dateFormat("yyyy.MM.dd").locale("en")); + + PostLoansLoanIdTransactionsResponse merchantIssuedRefund1 = loanTransactionHelper.makeMerchantIssuedRefund(Long.valueOf(loanID), + new PostLoansLoanIdTransactionsRequest().locale("en").dateFormat("dd MMMM yyyy").transactionDate("17 June 2023") + .transactionAmount(125.0)); + + loanDetails = loanTransactionHelper.getLoanDetails((long) loanID); + assertEquals(6.6, loanDetails.getSummary().getTotalOutstanding()); + assertEquals(6.6, loanDetails.getRepaymentSchedule().getTotalOutstanding()); + assertEquals(3.65, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesDue()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesPaid()); + assertEquals(3.65, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesDue()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesPaid()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding()); + assertEquals(127.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue()); + assertEquals(125.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid()); + assertEquals(2.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding()); + assertTrue(loanDetails.getStatus().getActive()); + + assertNull(loanDetails.getTransactions().get(2).getReversedOnDate()); + assertTrue(loanDetails.getTransactions().get(2).getTransactionRelations().isEmpty()); + assertTrue(loanDetails.getTransactions().get(2).getType().getMerchantIssuedRefund()); + assertEquals(125.0, loanDetails.getTransactions().get(2).getAmount()); + assertEquals(125.0, loanDetails.getTransactions().get(2).getPrincipalPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(2).getPenaltyChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(2).getOverpaymentPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(2).getInterestPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(2).getFeeChargesPortion()); + assertEquals(2.95, loanDetails.getTransactions().get(2).getOutstandingLoanBalance()); + + PostLoansLoanIdChargesChargeIdResponse chargeAdjustmentResponse = this.loanTransactionHelper.chargeAdjustment((long) loanID, + (long) penalty1LoanChargeId, new PostLoansLoanIdChargesChargeIdRequest().amount(3.65)); + + loanDetails = loanTransactionHelper.getLoanDetails((long) loanID); + assertEquals(2.95, loanDetails.getSummary().getTotalOutstanding()); + assertEquals(2.95, loanDetails.getRepaymentSchedule().getTotalOutstanding()); + assertEquals(3.65, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesDue()); + assertEquals(0.70, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesPaid()); + assertEquals(2.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesDue()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesPaid()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding()); + assertEquals(127.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue()); + assertEquals(127.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding()); + assertTrue(loanDetails.getStatus().getActive()); + + assertNull(loanDetails.getTransactions().get(3).getReversedOnDate()); + assertFalse(loanDetails.getTransactions().get(3).getTransactionRelations().isEmpty()); + assertEquals((long) penalty1LoanChargeId, + loanDetails.getTransactions().get(3).getTransactionRelations().iterator().next().getToLoanCharge()); + assertTrue(loanDetails.getTransactions().get(3).getType().getChargeAdjustment()); + assertEquals(3.65, loanDetails.getTransactions().get(3).getAmount()); + assertEquals(2.95, loanDetails.getTransactions().get(3).getPrincipalPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(3).getPenaltyChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(3).getOverpaymentPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(3).getInterestPortion()); + assertEquals(0.7, loanDetails.getTransactions().get(3).getFeeChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(3).getOutstandingLoanBalance()); + + PostLoansLoanIdTransactionsResponse merchantIssuedRefund2 = loanTransactionHelper.makeMerchantIssuedRefund(Long.valueOf(loanID), + new PostLoansLoanIdTransactionsRequest().locale("en").dateFormat("dd MMMM yyyy").transactionDate("17 June 2023") + .transactionAmount(2.95)); + + loanDetails = loanTransactionHelper.getLoanDetails((long) loanID); + assertEquals(0.0, loanDetails.getSummary().getTotalOutstanding()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getTotalOutstanding()); + assertEquals(3.65, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesDue()); + assertEquals(3.65, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesPaid()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getFeeChargesOutstanding()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesDue()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesPaid()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPenaltyChargesOutstanding()); + assertEquals(127.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalDue()); + assertEquals(127.95, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalPaid()); + assertEquals(0.0, loanDetails.getRepaymentSchedule().getPeriods().get(1).getPrincipalOutstanding()); + assertTrue(loanDetails.getStatus().getClosedObligationsMet()); + + assertNull(loanDetails.getTransactions().get(4).getReversedOnDate()); + assertTrue(loanDetails.getTransactions().get(4).getTransactionRelations().isEmpty()); + assertTrue(loanDetails.getTransactions().get(4).getType().getMerchantIssuedRefund()); + assertEquals(2.95, loanDetails.getTransactions().get(4).getAmount()); + assertEquals(0.0, loanDetails.getTransactions().get(4).getPrincipalPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(4).getPenaltyChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(4).getOverpaymentPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(4).getInterestPortion()); + assertEquals(2.95, loanDetails.getTransactions().get(4).getFeeChargesPortion()); + assertEquals(0.0, loanDetails.getTransactions().get(4).getOutstandingLoanBalance()); + } finally { + GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec, responseSpec, Boolean.FALSE); + GlobalConfigurationHelper.updateChargeAccrualDateConfiguration(requestSpec, responseSpec, "due-date"); + } + } + private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID, final String principal, final String loanTermFrequency, final String repaymentAfterEvery, final String numberOfRepayments, final String interestRate, final String repaymentStrategy, final String expectedDisbursementDate, final String submittedOnDate) { LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principal) - .withLoanTermFrequency(loanTermFrequency).withLoanTermFrequencyAsMonths().withNumberOfRepayments(numberOfRepayments) - .withRepaymentEveryAfter(repaymentAfterEvery).withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod(interestRate) + .withLoanTermFrequency(loanTermFrequency).withLoanTermFrequencyAsDays().withNumberOfRepayments(numberOfRepayments) + .withRepaymentEveryAfter(repaymentAfterEvery).withRepaymentFrequencyTypeAsDays().withInterestRatePerPeriod(interestRate) .withInterestTypeAsFlatBalance().withAmortizationTypeAsEqualPrincipalPayments().withRepaymentStrategy(repaymentStrategy) .withInterestCalculationPeriodTypeSameAsRepaymentPeriod().withExpectedDisbursementDate(expectedDisbursementDate) .withSubmittedOnDate(submittedOnDate).withLoanType("individual").build(clientID.toString(), loanProductID.toString(), null); @@ -1413,8 +1808,8 @@ private Integer applyForLoanApplication(final Integer clientID, final Integer lo private Integer createLoanProductWithNoAccountingNoInterest(final String principal, final String repaymentAfterEvery, final String numberOfRepayments, final String interestRate, final String repaymentStrategy, final Account... accounts) { LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); - final String loanProductJSON = new LoanProductTestBuilder().withPrincipal(principal).withRepaymentTypeAsMonth() - .withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments).withRepaymentTypeAsMonth() + final String loanProductJSON = new LoanProductTestBuilder().withPrincipal(principal).withRepaymentTypeAsDays() + .withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments) .withinterestRatePerPeriod(interestRate).withInterestRateFrequencyTypeAsMonths().withRepaymentStrategy(repaymentStrategy) .withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts) .withDaysInMonth("30").withDaysInYear("365").withMoratorium("0", "0").build(null);