diff --git a/README.md b/README.md index 57d2da7358b..11bdfc37ebe 100644 --- a/README.md +++ b/README.md @@ -22,15 +22,15 @@ If you are interested in contributing to this project, but perhaps don't quite k REQUIREMENTS ============ * `Java >= 17` (Azul Zulu JVM is tested by our CI on GitHub Actions) -* MariaDB `11.2` +* MariaDB `11.4` You can run the required version of the database server in a container, instead of having to install it, like this: - docker run --name mariadb-11.2 -p 3306:3306 -e MARIADB_ROOT_PASSWORD=mysql -d mariadb:11.2 + docker run --name mariadb-11.4 -p 3306:3306 -e MARIADB_ROOT_PASSWORD=mysql -d mariadb:11.4 and stop and destroy it like this: - docker rm -f mariadb-11.2 + docker rm -f mariadb-11.4
Beware that this database container database keeps its state inside the container and not on the host filesystem. It is lost when you destroy (rm) this container. This is typically fine for development. See [Caveats: Where to Store Data on the database container documentation](https://hub.docker.com/_/mariadb) re. how to make it persistent instead of ephemeral.
diff --git a/config/docker/compose/mariadb.yml b/config/docker/compose/mariadb.yml index 2c56ec15553..6756974f784 100644 --- a/config/docker/compose/mariadb.yml +++ b/config/docker/compose/mariadb.yml @@ -18,7 +18,7 @@ version: "3.8" services: mariadb: container_name: mariadb - image: mariadb:11.2 + image: mariadb:11.4 volumes: - ${PWD}/config/docker/mysql/conf.d/server_collation.cnf:/etc/mysql/conf.d/server_collation.cnf:ro - ${PWD}/config/docker/mysql/docker-entrypoint-initdb.d:/docker-entrypoint-initdb.d:Z,ro diff --git a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java index 10eb848a134..10e596954c4 100644 --- a/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java +++ b/fineract-core/src/main/java/org/apache/fineract/infrastructure/core/service/DateUtils.java @@ -79,6 +79,11 @@ public static OffsetDateTime getOffsetDateTimeOfTenant(ChronoUnit truncate) { return truncate == null ? now : now.truncatedTo(truncate); } + @NotNull + public static OffsetDateTime getOffsetDateTimeOfTenantFromLocalDate(@NotNull final LocalDate date) { + return OffsetDateTime.of(date.atStartOfDay(), getOffsetDateTimeOfTenant().getOffset()); + } + public static LocalDateTime getLocalDateTimeOfSystem() { return getLocalDateTimeOfSystem(null); } diff --git a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature index 3b7649d6a9d..722bf3f82f2 100644 --- a/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature +++ b/fineract-e2e-tests-runner/src/test/resources/features/LoanRepayment.feature @@ -3350,6 +3350,7 @@ Feature: LoanRepayment | 13 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 22 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 22 August 2024 | Repayment | 38.24 | 35.44 | 0.0 | 0.0 | 2.8 | 0.0 | false | + | 22 August 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false | | 23 August 2024 | Repayment | 10.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false | | 24 August 2024 | Accrual | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | 0.0 | false | Then Loan Repayment schedule has 2 periods, with the following data for periods: @@ -3369,9 +3370,10 @@ Feature: LoanRepayment | 13 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 22 August 2024 | Repayment | 35.44 | 35.44 | 0.0 | 0.0 | 0.0 | 0.0 | true | | 22 August 2024 | Repayment | 38.24 | 35.44 | 0.0 | 0.0 | 2.8 | 0.0 | false | + | 22 August 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false | | 23 August 2024 | Repayment | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 | 0.0 | false | | 24 August 2024 | Accrual | 2.8 | 0.0 | 0.0 | 0.0 | 2.8 | 0.0 | false | - | 25 August 2024 | Accrual | 10.0 | 0.0 | 0.0 | 0.0 | 10.0 | 0.0 | false | + | 25 August 2024 | Accrual | 5.0 | 0.0 | 0.0 | 0.0 | 5.0 | 0.0 | false | Then Loan Repayment schedule has 2 periods, with the following data for periods: | Nr | Days | Date | Paid date | Balance of loan | Principal due | Interest | Fees | Penalties | Due | Paid | In advance | Late | Outstanding | | | | 23 July 2024 | | 111.92 | | | 0.0 | | 0.0 | 0.0 | | | | diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java index a8c6b0feecf..7a462c00458 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTermVariationType.java @@ -26,9 +26,11 @@ public enum LoanTermVariationType { PRINCIPAL_AMOUNT(3, "loanTermType.principalAmount"), // DUE_DATE(4, "loanTermType.dueDate"), // INSERT_INSTALLMENT(5, "loanTermType.insertInstallment"), // - DELETE_INSTALLMENT(6, "loanTermType.deleteInstallment"), GRACE_ON_INTEREST(7, "loanTermType.graceOnInterest"), GRACE_ON_PRINCIPAL(8, - "loanTermType.graceOnPrincipal"), EXTEND_REPAYMENT_PERIOD(9, - "loanTermType.extendRepaymentPeriod"), INTEREST_RATE_FROM_INSTALLMENT(10, "loanTermType.interestRateFromInstallment"); + DELETE_INSTALLMENT(6, "loanTermType.deleteInstallment"), // + GRACE_ON_INTEREST(7, "loanTermType.graceOnInterest"), // + GRACE_ON_PRINCIPAL(8, "loanTermType.graceOnPrincipal"), // + EXTEND_REPAYMENT_PERIOD(9, "loanTermType.extendRepaymentPeriod"), // + INTEREST_RATE_FROM_INSTALLMENT(10, "loanTermType.interestRateFromInstallment"); // private final Integer value; private final String code; diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 582362a8ee4..3b40c7cb3a9 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -473,7 +473,7 @@ private boolean isObligationsMetOnDisbursementDate(LocalDate disbursementDate, && disbursementDate.equals(loanRepaymentScheduleInstallment.getObligationsMetOnDate()); } - private boolean isNotObligationsMet(LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment) { + protected boolean isNotObligationsMet(LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment) { return !loanRepaymentScheduleInstallment.isObligationsMet() && loanRepaymentScheduleInstallment.getObligationsMetOnDate() == null; } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java index 02847c4e3a8..d8c12da8312 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java @@ -1,3 +1,21 @@ +/** + * 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.service; import java.util.Map; diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 95b9fce13c7..a487c6621ac 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -64,6 +64,8 @@ import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsDataWrapper; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; @@ -72,6 +74,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; @@ -175,8 +178,6 @@ public Pair repr currentInstallment.updateObligationsMet(currency, disbursementDate); } - List chargeOrTransactions = createSortedChargesAndTransactionsList(loanTransactions, charges); - MoneyHolder overpaymentHolder = new MoneyHolder(Money.zero(currency)); final Loan loan = loanTransactions.get(0).getLoan(); final Integer installmentAmountInMultiplesOf = loan.getLoanProduct().getInstallmentAmountInMultiplesOf(); @@ -186,17 +187,26 @@ public Pair repr ProgressiveTransactionCtx ctx = new ProgressiveTransactionCtx(currency, installments, charges, overpaymentHolder, changedTransactionDetail, scheduleModel); + LoanTermVariationsDataWrapper loanTermVariations = Optional + .ofNullable(loan.getActiveLoanTermVariations()).map(loanTermVariationsSet -> loanTermVariationsSet.stream() + .map(LoanTermVariations::toData).collect(Collectors.toCollection(ArrayList::new))) + .map(LoanTermVariationsDataWrapper::new).orElse(null); + List changeOperations = createSortedChangeList(loanTermVariations, loanTransactions, charges); + List overpaidTransactions = new ArrayList<>(); - for (final ChargeOrTransaction chargeOrTransaction : chargeOrTransactions) { - if (chargeOrTransaction.isTransaction()) { - LoanTransaction transaction = chargeOrTransaction.getLoanTransaction().get(); + for (final ChangeOperation changeOperation : changeOperations) { + if (changeOperation.isInterestRateChange()) { + final LoanTermVariationsData interestRateChange = changeOperation.getInterestRateChange().get(); + processInterestRateChange(installments, interestRateChange, scheduleModel); + } else if (changeOperation.isTransaction()) { + LoanTransaction transaction = changeOperation.getLoanTransaction().get(); processSingleTransaction(transaction, ctx); transaction = getProcessedTransaction(changedTransactionDetail, transaction); if (transaction.isOverPaid() && transaction.isRepaymentLikeType()) { // TODO CREDIT, DEBIT overpaidTransactions.add(transaction); } } else { - LoanCharge loanCharge = chargeOrTransaction.getLoanCharge().get(); + LoanCharge loanCharge = changeOperation.getLoanCharge().get(); processSingleCharge(loanCharge, currency, installments, disbursementDate); if (!loanCharge.isFullyPaid() && !overpaidTransactions.isEmpty()) { overpaidTransactions = processOverpaidTransactions(overpaidTransactions, ctx); @@ -210,13 +220,19 @@ public Pair repr createNewTransaction(oldTransaction, newTransaction, ctx); } recalculateInterestForDate(ThreadLocalContextUtil.getBusinessDate(), ctx); - List txs = chargeOrTransactions.stream() // - .filter(ChargeOrTransaction::isTransaction) // + List txs = changeOperations.stream() // + .filter(ChangeOperation::isTransaction) // .map(e -> e.getLoanTransaction().get()).toList(); reprocessInstallments(disbursementDate, txs, installments, currency); return Pair.of(changedTransactionDetail, scheduleModel); } + @Override + public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, + MonetaryCurrency currency, List installments, Set charges) { + return reprocessProgressiveLoanTransactions(disbursementDate, loanTransactions, currency, installments, charges).getLeft(); + } + @Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true) public ProgressiveLoanInterestScheduleModel calculateInterestScheduleModel(@NotNull Long loanId) { Loan loan = loanAssembler.assembleFrom(loanId); @@ -236,18 +252,36 @@ public ProgressiveLoanInterestScheduleModel calculateInterestScheduleModel(Local return reprocessProgressiveLoanTransactions(disbursementDate, transactions, currency, installments, charges).getRight(); } - @Override - public ChangedTransactionDetail reprocessLoanTransactions(LocalDate disbursementDate, List loanTransactions, - MonetaryCurrency currency, List installments, Set charges) { - return reprocessProgressiveLoanTransactions(disbursementDate, loanTransactions, currency, installments, charges).getLeft(); - } - @NotNull private static LoanTransaction getProcessedTransaction(ChangedTransactionDetail changedTransactionDetail, LoanTransaction transaction) { LoanTransaction newTransaction = changedTransactionDetail.getNewTransactionMappings().get(transaction.getId()); return newTransaction == null ? transaction : newTransaction; } + private void processInterestRateChange(final List installments, + final LoanTermVariationsData interestRateChange, final ProgressiveLoanInterestScheduleModel scheduleModel) { + final LocalDate interestRateChangeSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom(); + final BigDecimal newInterestRate = interestRateChange.getDecimalValue(); + emiCalculator.changeInterestRate(scheduleModel, interestRateChangeSubmittedOnDate, newInterestRate); + processInterestRateChangeOnInstallments(scheduleModel, interestRateChangeSubmittedOnDate, installments); + } + + private void processInterestRateChangeOnInstallments(final ProgressiveLoanInterestScheduleModel scheduleModel, + final LocalDate interestRateChangeSubmittedOnDate, final List installments) { + installments.stream() // + .filter(installment -> isNotObligationsMet(installment) + && !interestRateChangeSubmittedOnDate.isAfter(installment.getDueDate())) + .forEach(installment -> updateInstallmentIfInterestPeriodPresent(scheduleModel, installment)); // + } + + private void updateInstallmentIfInterestPeriodPresent(final ProgressiveLoanInterestScheduleModel scheduleModel, + final LoanRepaymentScheduleInstallment installment) { + emiCalculator.findRepaymentPeriod(scheduleModel, installment.getDueDate()).ifPresent(interestRepaymentPeriod -> { + installment.updateInterestCharged(interestRepaymentPeriod.getDueInterest().getAmount()); + installment.updatePrincipal(interestRepaymentPeriod.getDuePrincipal().getAmount()); + }); + } + @Override public void processLatestTransaction(LoanTransaction loanTransaction, TransactionCtx ctx) { switch (loanTransaction.getTypeOf()) { @@ -762,17 +796,29 @@ private void processSingleCharge(LoanCharge loanCharge, MonetaryCurrency currenc } @NotNull - private List createSortedChargesAndTransactionsList(List loanTransactions, - Set charges) { - List chargeOrTransactions = new ArrayList<>(); + private List createSortedChangeList(final LoanTermVariationsDataWrapper loanTermVariations, + final List loanTransactions, final Set charges) { + List changeOperations = new ArrayList<>(); + if (loanTermVariations != null && !loanTermVariations.getInterestRateFromInstallment().isEmpty()) { + changeOperations.addAll(loanTermVariations.getInterestRateFromInstallment().stream().map(ChangeOperation::new).toList()); + } if (charges != null) { - chargeOrTransactions.addAll(charges.stream().map(ChargeOrTransaction::new).toList()); + changeOperations.addAll(charges.stream().map(ChangeOperation::new).toList()); } if (loanTransactions != null) { - chargeOrTransactions.addAll(loanTransactions.stream().map(ChargeOrTransaction::new).toList()); + changeOperations.addAll(loanTransactions.stream().map(ChangeOperation::new).toList()); + } + Collections.sort(changeOperations); + return changeOperations; + } + + private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { + // TODO: Fix this and enhance EMICalculator to support reamortization and reaging + if (disbursementTransaction.getLoan().isInterestBearing()) { + handleDisbursementWithEMICalculator(disbursementTransaction, transactionCtx); + } else { + handleDisbursementWithoutEMICalculator(disbursementTransaction, transactionCtx); } - Collections.sort(chargeOrTransactions); - return chargeOrTransactions; } private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { @@ -820,15 +866,6 @@ private void handleDisbursementWithEMICalculator(LoanTransaction disbursementTra allocateOverpayment(disbursementTransaction, transactionCtx); } - private void handleDisbursement(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { - // TODO: Fix this and enhance EMICalculator to support reamortization and reaging - if (disbursementTransaction.getLoan().isInterestBearing()) { - handleDisbursementWithEMICalculator(disbursementTransaction, transactionCtx); - } else { - handleDisbursementWithoutEMICalculator(disbursementTransaction, transactionCtx); - } - } - private void handleDisbursementWithoutEMICalculator(LoanTransaction disbursementTransaction, TransactionCtx transactionCtx) { disbursementTransaction.resetDerivedComponents(); final MathContext mc = MoneyHelper.getMathContext(); @@ -923,7 +960,6 @@ private void recalculateInterestForDate(LocalDate currentDate, ProgressiveTransa if (overdueInstallmentsSortedByInstallmentNumber.isEmpty()) { return; } - ProgressiveLoanInterestScheduleModel model = ctx.getModel(); MonetaryCurrency currency = ctx.getCurrency(); Money overDuePrincipal = Money.zero(currency); Money aggregatedOverDuePrincipal = Money.zero(currency); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChangeOperation.java similarity index 73% rename from fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java rename to fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChangeOperation.java index 504719b6712..16469f8a0d6 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransaction.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChangeOperation.java @@ -24,26 +24,40 @@ import java.util.Optional; import lombok.Getter; import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.jetbrains.annotations.NotNull; @Getter -public class ChargeOrTransaction implements Comparable { +public class ChangeOperation implements Comparable { + private final Optional interestRateChange; private final Optional loanCharge; private final Optional loanTransaction; - public ChargeOrTransaction(LoanCharge loanCharge) { + public ChangeOperation(LoanCharge loanCharge) { + this.interestRateChange = Optional.empty(); this.loanCharge = Optional.of(loanCharge); this.loanTransaction = Optional.empty(); } - public ChargeOrTransaction(LoanTransaction loanTransaction) { + public ChangeOperation(LoanTransaction loanTransaction) { + this.interestRateChange = Optional.empty(); this.loanTransaction = Optional.of(loanTransaction); this.loanCharge = Optional.empty(); } + public ChangeOperation(LoanTermVariationsData interestRateChange) { + this.interestRateChange = Optional.of(interestRateChange); + this.loanTransaction = Optional.empty(); + this.loanCharge = Optional.empty(); + } + + public boolean isInterestRateChange() { + return interestRateChange.isPresent(); + } + public boolean isTransaction() { return loanTransaction.isPresent(); } @@ -52,8 +66,18 @@ public boolean isCharge() { return loanCharge.isPresent(); } + private boolean isAccrualActivity() { + return isTransaction() && loanTransaction.get().isAccrualActivity(); + } + + private boolean isBackdatedCharge() { + return isCharge() && DateUtils.isBefore(loanCharge.get().getDueDate(), loanCharge.get().getSubmittedOnDate()); + } + private LocalDate getEffectiveDate() { - if (loanCharge.isPresent()) { + if (interestRateChange.isPresent()) { + return getSubmittedOnDate(); + } else if (loanCharge.isPresent()) { if (isBackdatedCharge()) { return loanCharge.get().getDueDate(); } else { @@ -66,16 +90,10 @@ private LocalDate getEffectiveDate() { } } - private boolean isAccrualActivity() { - return isTransaction() && loanTransaction.get().isAccrualActivity(); - } - - private boolean isBackdatedCharge() { - return isCharge() && DateUtils.isBefore(loanCharge.get().getDueDate(), loanCharge.get().getSubmittedOnDate()); - } - private LocalDate getSubmittedOnDate() { - if (loanCharge.isPresent()) { + if (interestRateChange.isPresent()) { + return interestRateChange.get().getTermVariationApplicableFrom(); + } else if (loanCharge.isPresent()) { return loanCharge.get().getSubmittedOnDate(); } else if (loanTransaction.isPresent()) { return loanTransaction.get().getSubmittedOnDate(); @@ -85,7 +103,9 @@ private LocalDate getSubmittedOnDate() { } private OffsetDateTime getCreatedDateTime() { - if (loanCharge.isPresent() && loanCharge.get().getCreatedDate().isPresent()) { + if (interestRateChange.isPresent()) { + return DateUtils.getOffsetDateTimeOfTenantFromLocalDate(getSubmittedOnDate()); + } else if (loanCharge.isPresent() && loanCharge.get().getCreatedDate().isPresent()) { return loanCharge.get().getCreatedDate().get(); } else if (loanTransaction.isPresent()) { return loanTransaction.get().getCreatedDateTime(); @@ -96,16 +116,16 @@ private OffsetDateTime getCreatedDateTime() { @Override @SuppressFBWarnings(value = "EQ_COMPARETO_USE_OBJECT_EQUALS", justification = "TODO: fix this! See: https://stackoverflow.com/questions/2609037/findbugs-how-to-solve-eq-compareto-use-object-equals") - public int compareTo(@NotNull ChargeOrTransaction o) { + public int compareTo(@NotNull ChangeOperation o) { int datePortion = DateUtils.compare(this.getEffectiveDate(), o.getEffectiveDate()); if (datePortion == 0) { - boolean isAccrual = isAccrualActivity(); + final boolean isAccrual = isAccrualActivity(); if (isAccrual != o.isAccrualActivity()) { return isAccrual ? 1 : -1; } int submittedDate = DateUtils.compare(getSubmittedOnDate(), o.getSubmittedOnDate()); if (submittedDate == 0) { - return DateUtils.compare(getCreatedDateTime(), o.getCreatedDateTime()); + return DateUtils.compare(getCreatedDateTime(), o.getCreatedDateTime(), null); } return submittedDate; } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestRate.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestRate.java index bf5fe3474ce..50e78716aac 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestRate.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/InterestRate.java @@ -22,9 +22,9 @@ import java.time.LocalDate; import org.jetbrains.annotations.NotNull; -public record InterestRate(LocalDate effectiveFrom, // - LocalDate validFrom, // - BigDecimal interestRate // +public record InterestRate(// + LocalDate effectiveFrom, // + BigDecimal interestRate// ) implements Comparable { @Override diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java index 9dbce58214b..c20d0908fe5 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/ProgressiveLoanInterestScheduleModel.java @@ -22,8 +22,10 @@ import java.math.MathContext; import java.time.LocalDate; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Optional; +import java.util.TreeSet; import java.util.function.Consumer; import lombok.Data; import lombok.experimental.Accessors; @@ -37,7 +39,7 @@ public class ProgressiveLoanInterestScheduleModel { private final List repaymentPeriods; - private final List interestRates; + private final TreeSet interestRates; private final LoanProductRelatedDetail loanProductRelatedDetail; private final Integer installmentAmountInMultiplesOf; private MathContext mc; @@ -45,17 +47,17 @@ public class ProgressiveLoanInterestScheduleModel { public ProgressiveLoanInterestScheduleModel(List repaymentPeriods, LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc) { this.repaymentPeriods = repaymentPeriods; - this.interestRates = new ArrayList<>(); + this.interestRates = new TreeSet<>(Collections.reverseOrder()); this.loanProductRelatedDetail = loanProductRelatedDetail; this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf; this.mc = mc; } - private ProgressiveLoanInterestScheduleModel(List repaymentPeriods, final List interestRates, + private ProgressiveLoanInterestScheduleModel(List repaymentPeriods, final TreeSet interestRates, LoanProductRelatedDetail loanProductRelatedDetail, Integer installmentAmountInMultiplesOf, MathContext mc) { this.mc = mc; this.repaymentPeriods = copyRepaymentPeriods(repaymentPeriods); - this.interestRates = new ArrayList<>(interestRates); + this.interestRates = new TreeSet<>(interestRates); this.loanProductRelatedDetail = loanProductRelatedDetail; this.installmentAmountInMultiplesOf = installmentAmountInMultiplesOf; } @@ -81,8 +83,15 @@ public BigDecimal getInterestRate(final LocalDate effectiveDate) { } private BigDecimal findInterestRate(final LocalDate effectiveDate) { - return interestRates.stream().filter(ir -> !DateUtils.isAfter(ir.effectiveFrom(), effectiveDate)).map(InterestRate::interestRate) - .findFirst().orElse(loanProductRelatedDetail.getAnnualNominalInterestRate()); + return interestRates.stream() // + .filter(ir -> !DateUtils.isAfter(ir.effectiveFrom(), effectiveDate)) // + .map(InterestRate::interestRate) // + .findFirst() // + .orElse(loanProductRelatedDetail.getAnnualNominalInterestRate()); // + } + + public void addInterestRate(final LocalDate newInterestEffectiveDate, final BigDecimal newInterestRate) { + interestRates.add(new InterestRate(newInterestEffectiveDate, newInterestRate)); } public Optional findRepaymentPeriod(final LocalDate repaymentPeriodDueDate) { @@ -177,11 +186,11 @@ void insertInterestPeriod(final RepaymentPeriod repaymentPeriod, final LocalDate previousInterestPeriod.addDisbursementAmount(disbursedAmount); previousInterestPeriod.addBalanceCorrectionAmount(correctionAmount); final InterestPeriod interestPeriod = new InterestPeriod(repaymentPeriod, previousInterestPeriod.getDueDate(), originalDueDate, - BigDecimal.ZERO, getZero(mc), getZero(mc), getZero(mc), mc); + BigDecimal.ZERO, getZero(), getZero(), getZero(), mc); repaymentPeriod.getInterestPeriods().add(interestPeriod); } - private Money getZero(MathContext mc) { + public Money getZero() { return Money.zero(loanProductRelatedDetail.getCurrency(), mc); } } diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java index 6de2f31dab1..8caf5473895 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/RepaymentPeriod.java @@ -137,18 +137,18 @@ public Money getDuePrincipal() { return MathUtil.max(getEmi().minus(getDueInterest(), mc), getPaidPrincipal(), false); } + public Money getTotalPaidAmount() { + return getPaidPrincipal().plus(getPaidInterest()); + } + public boolean isFullyPaid() { - return getEmi().isEqualTo(getPaidPrincipal().plus(getPaidInterest())); + return getEmi().isEqualTo(getTotalPaidAmount()); } public Money getUnrecognizedInterest() { return getCalculatedDueInterest().minus(getDueInterest(), mc); } - public Money getOutstandingPrincipal() { - return MathUtil.minus(getDuePrincipal(), getPaidPrincipal()); - } - public Money getOutstandingLoanBalance() { if (outstandingBalanceCalculation == null) { outstandingBalanceCalculation = Memo.of(() -> { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java index 2c68173626e..8a5cb7ae2b6 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/ProgressiveLoanScheduleGenerator.java @@ -38,7 +38,6 @@ import org.apache.fineract.portfolio.loanaccount.data.DisbursementData; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleAccrualData; -import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; @@ -65,27 +64,19 @@ public class ProgressiveLoanScheduleGenerator implements LoanScheduleGenerator { private final ScheduledDateGenerator scheduledDateGenerator; private final EMICalculator emiCalculator; + public LoanSchedulePlan generate(final MathContext mc, final LoanRepaymentScheduleModelData modelData) { + LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(modelData, mc); + return LoanSchedulePlan.from(generate(mc, loanApplicationTerms, null, null)); + } + @Override public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTerms loanApplicationTerms, final Set loanCharges, final HolidayDetailDTO holidayDetailDTO) { final ApplicationCurrency applicationCurrency = loanApplicationTerms.getApplicationCurrency(); - // generate list of proposed schedule due dates - LocalDate loanEndDate = scheduledDateGenerator.getLastRepaymentDate(loanApplicationTerms, holidayDetailDTO); - if (loanApplicationTerms.getLoanTermVariations() != null) { - LoanTermVariationsData lastDueDateVariation = loanApplicationTerms.getLoanTermVariations() - .fetchLoanTermDueDateVariationsData(loanEndDate); - if (lastDueDateVariation != null) { - loanEndDate = lastDueDateVariation.getDateValue(); - } - } - // determine the total charges due at time of disbursement final BigDecimal chargesDueAtTimeOfDisbursement = deriveTotalChargesDueAtTimeOfDisbursement(loanCharges); - // setup variables for tracking important facts required for loan - // schedule generation. - final MonetaryCurrency currency = loanApplicationTerms.getCurrency(); LocalDate periodStartDate = RepaymentStartDateType.DISBURSEMENT_DATE.equals(loanApplicationTerms.getRepaymentStartDateType()) ? loanApplicationTerms.getExpectedDisbursementDate() @@ -98,6 +89,7 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer // set and handled separately after all installments generated final Set nonCompoundingCharges = separateTotalCompoundingPercentageCharges(loanCharges); + // generate list of proposed schedule due dates final List expectedRepaymentPeriods = scheduledDateGenerator.generateRepaymentPeriods(mc, periodStartDate, loanApplicationTerms, holidayDetailDTO); final ProgressiveLoanInterestScheduleModel interestScheduleModel = emiCalculator.generatePeriodInterestScheduleModel( @@ -106,29 +98,17 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer final List periods = new ArrayList<>(expectedRepaymentPeriods.size()); prepareDisbursementsOnLoanApplicationTerms(loanApplicationTerms); - - final ArrayList disbursementDataList = new ArrayList<>(loanApplicationTerms.getDisbursementDatas()); - disbursementDataList.sort(Comparator.comparing(DisbursementData::disbursementDate)); + final List disbursementDataList = getSortedDisbursementList(loanApplicationTerms); for (LoanScheduleModelRepaymentPeriod repaymentPeriod : expectedRepaymentPeriods) { scheduleParams.setPeriodStartDate(repaymentPeriod.getFromDate()); scheduleParams.setActualRepaymentDate(repaymentPeriod.getDueDate()); - + // in same repayment period the logic firstly applies interest rate changes and just after the disbursements + applyInterestRateChangesOnPeriod(loanApplicationTerms, repaymentPeriod, interestScheduleModel); processDisbursements(loanApplicationTerms, disbursementDataList, scheduleParams, interestScheduleModel, periods, chargesDueAtTimeOfDisbursement, false, mc); repaymentPeriod.setPeriodNumber(scheduleParams.getInstalmentNumber()); - if (loanApplicationTerms.getLoanTermVariations() != null) { - for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) { - final LocalDate interestRateSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom(); - final BigDecimal newInterestRate = interestRateChange.getDecimalValue(); - if (interestRateSubmittedOnDate.isAfter(repaymentPeriod.getFromDate()) - && !interestRateSubmittedOnDate.isAfter(repaymentPeriod.getDueDate())) { - emiCalculator.changeInterestRate(interestScheduleModel, interestRateSubmittedOnDate, newInterestRate); - } - } - } - emiCalculator.findRepaymentPeriod(interestScheduleModel, repaymentPeriod.getDueDate()).ifPresent(interestRepaymentPeriod -> { final Money principalDue = interestRepaymentPeriod.getDuePrincipal(); final Money interestDue = interestRepaymentPeriod.getDueInterest(); @@ -170,10 +150,24 @@ public LoanScheduleModel generate(final MathContext mc, final LoanApplicationTer scheduleParams.getTotalRepaymentExpected().getAmount(), totalOutstanding); } - public LoanSchedulePlan generate(final MathContext mc, final LoanRepaymentScheduleModelData modelData) { + private List getSortedDisbursementList(LoanApplicationTerms loanApplicationTerms) { + final List disbursementDataList = new ArrayList<>(loanApplicationTerms.getDisbursementDatas()); + disbursementDataList.sort(Comparator.comparing(DisbursementData::disbursementDate)); + return disbursementDataList; + } - LoanApplicationTerms loanApplicationTerms = LoanApplicationTerms.assembleFrom(modelData, mc); - return LoanSchedulePlan.from(generate(mc, loanApplicationTerms, null, null)); + private void applyInterestRateChangesOnPeriod(final LoanApplicationTerms loanApplicationTerms, + final LoanScheduleModelRepaymentPeriod repaymentPeriod, final ProgressiveLoanInterestScheduleModel interestScheduleModel) { + if (loanApplicationTerms.getLoanTermVariations() != null) { + for (var interestRateChange : loanApplicationTerms.getLoanTermVariations().getInterestRateFromInstallment()) { + final LocalDate interestRateSubmittedOnDate = interestRateChange.getTermVariationApplicableFrom(); + final BigDecimal newInterestRate = interestRateChange.getDecimalValue(); + if (interestRateSubmittedOnDate.isAfter(repaymentPeriod.getFromDate()) + && !interestRateSubmittedOnDate.isAfter(repaymentPeriod.getDueDate())) { + emiCalculator.changeInterestRate(interestScheduleModel, interestRateSubmittedOnDate, newInterestRate); + } + } + } } private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTerms loanApplicationTerms) { @@ -185,10 +179,10 @@ private void prepareDisbursementsOnLoanApplicationTerms(final LoanApplicationTer } } - private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, - final ArrayList disbursementDataList, final LoanScheduleParams scheduleParams, - final ProgressiveLoanInterestScheduleModel interestScheduleModel, final List periods, - final BigDecimal chargesDueAtTimeOfDisbursement, final boolean includeDisbursementsAfterMaturityDate, final MathContext mc) { + private void processDisbursements(final LoanApplicationTerms loanApplicationTerms, final List disbursementDataList, + final LoanScheduleParams scheduleParams, final ProgressiveLoanInterestScheduleModel interestScheduleModel, + final List periods, final BigDecimal chargesDueAtTimeOfDisbursement, + final boolean includeDisbursementsAfterMaturityDate, final MathContext mc) { for (DisbursementData disbursementData : disbursementDataList) { final LocalDate disbursementDate = disbursementData.disbursementDate(); @@ -280,6 +274,9 @@ public OutstandingAmountsDTO calculatePrepaymentAmount(MonetaryCurrency currency .principal(result.getOutstandingBalance()) // .interest(result.getPayableInterest()); + installments.stream().filter(installment -> installment.getDueDate().isBefore(onDate)) + .forEach(installment -> amounts.plusInterest(installment.getInterestOutstanding(currency))); + installments.forEach(installment -> amounts // .plusFeeCharges(installment.getFeeChargesOutstanding(currency)) .plusPenaltyCharges(installment.getPenaltyChargesOutstanding(currency))); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java index c3f1c888ccf..db141c79bda 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculator.java @@ -95,16 +95,14 @@ public Optional findRepaymentPeriod(final ProgressiveLoanIntere @Override public void addDisbursement(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate disbursementDueDate, final Money disbursedAmount) { - scheduleModel - .changeOutstandingBalanceAndUpdateInterestPeriods(disbursementDueDate, disbursedAmount, - Money.zero(disbursedAmount.getCurrency(), scheduleModel.mc())) + scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(disbursementDueDate, disbursedAmount, scheduleModel.getZero()) .ifPresent((repaymentPeriod) -> calculateEMIValueAndRateFactors( getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, disbursementDueDate), scheduleModel)); } private LocalDate getEffectiveRepaymentDueDate(final ProgressiveLoanInterestScheduleModel scheduleModel, - final RepaymentPeriod changedRepaymentPeriod, final LocalDate disbursementDueDate) { - final boolean isRelatedToNextRepaymentPeriod = changedRepaymentPeriod.getDueDate().isEqual(disbursementDueDate); + final RepaymentPeriod changedRepaymentPeriod, final LocalDate operationDueDate) { + final boolean isRelatedToNextRepaymentPeriod = changedRepaymentPeriod.getDueDate().isEqual(operationDueDate); if (isRelatedToNextRepaymentPeriod) { final Optional nextRepaymentPeriod = scheduleModel.repaymentPeriods().stream() .filter(repaymentPeriod -> changedRepaymentPeriod.equals(repaymentPeriod.getPrevious().orElse(null))).findFirst(); @@ -120,14 +118,20 @@ private LocalDate getEffectiveRepaymentDueDate(final ProgressiveLoanInterestSche @Override public void changeInterestRate(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate newInterestSubmittedOnDate, final BigDecimal newInterestRate) { - // TODO: impl + final LocalDate interestRateChangeEffectiveDate = newInterestSubmittedOnDate.minusDays(1); + scheduleModel.addInterestRate(interestRateChangeEffectiveDate, newInterestRate); + scheduleModel + .changeOutstandingBalanceAndUpdateInterestPeriods(interestRateChangeEffectiveDate, scheduleModel.getZero(), + scheduleModel.getZero()) + .ifPresent(repaymentPeriod -> calculateEMIValueAndRateFactors( + getEffectiveRepaymentDueDate(scheduleModel, repaymentPeriod, interestRateChangeEffectiveDate), scheduleModel)); } @Override public void addBalanceCorrection(ProgressiveLoanInterestScheduleModel scheduleModel, LocalDate balanceCorrectionDate, Money balanceCorrectionAmount) { - final Money zeroAmount = Money.zero(balanceCorrectionAmount.getCurrency()); - scheduleModel.changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, zeroAmount, balanceCorrectionAmount) + scheduleModel + .changeOutstandingBalanceAndUpdateInterestPeriods(balanceCorrectionDate, scheduleModel.getZero(), balanceCorrectionAmount) .ifPresent(repaymentPeriod -> { calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel); calculateOutstandingBalance(scheduleModel); @@ -163,8 +167,10 @@ private static LocalDate calcBalanceCorrectionDate(LocalDate repaymentPeriodDueD public PayableDetails getPayableDetails(final ProgressiveLoanInterestScheduleModel scheduleModel, final LocalDate repaymentPeriodDueDate, final LocalDate targetDate) { MathContext mc = scheduleModel.mc(); - RepaymentPeriod repaymentPeriod = scheduleModel.deepCopy(mc).repaymentPeriods().stream() + ProgressiveLoanInterestScheduleModel scheduleModelCopy = scheduleModel.deepCopy(mc); + RepaymentPeriod repaymentPeriod = scheduleModelCopy.repaymentPeriods().stream() .filter(rp -> rp.getDueDate().equals(repaymentPeriodDueDate)).findFirst().orElseThrow(); + LocalDate adjustedTargetDate = targetDate; InterestPeriod interestPeriod; if (!targetDate.isAfter(repaymentPeriod.getFromDate())) { @@ -180,22 +186,12 @@ public PayableDetails getPayableDetails(final ProgressiveLoanInterestScheduleMod interestPeriod.setDueDate(adjustedTargetDate); int index = repaymentPeriod.getInterestPeriods().indexOf(interestPeriod); repaymentPeriod.getInterestPeriods().subList(index + 1, repaymentPeriod.getInterestPeriods().size()).clear(); - calculateRateFactorForRepaymentPeriod(repaymentPeriod, scheduleModel); - - // TODO: gather all the unrecognized interest from previous periods based on target date - Money payableInterest = targetDate.isBefore(repaymentPeriod.getFromDate()) - ? Money.zero(scheduleModel.loanProductRelatedDetail().getCurrency(), mc) - : repaymentPeriod.getDueInterest(); - Money outstandingLoanBalance = interestPeriod.getOutstandingLoanBalance().add(interestPeriod.getDisbursementAmount()); - - Money calculatedEmi = outstandingLoanBalance.plus(payableInterest, mc); - if (calculatedEmi.isLessThan(repaymentPeriod.getEmi())) { - // Review this logic - repaymentPeriod.setEmi(outstandingLoanBalance.plus(payableInterest).plus(repaymentPeriod.getPaidInterest(), mc) - .plus(repaymentPeriod.getPaidPrincipal(), mc)); - } - Money payablePrincipal = repaymentPeriod.getEmi().minus(payableInterest, mc); - return new PayableDetails(repaymentPeriod.getEmi(), payablePrincipal, payableInterest, + scheduleModelCopy.repaymentPeriods().forEach(rp -> rp.getInterestPeriods().removeIf(ip -> ip.getDueDate().isAfter(targetDate))); + calculateRateFactorForPeriods(scheduleModelCopy.repaymentPeriods(), scheduleModelCopy); + calculateOutstandingBalance(scheduleModelCopy); + calculateLastUnpaidRepaymentPeriodEMI(scheduleModelCopy); + + return new PayableDetails(repaymentPeriod.getEmi(), repaymentPeriod.getDuePrincipal(), repaymentPeriod.getDueInterest(), interestPeriod.getOutstandingLoanBalance().add(interestPeriod.getDisbursementAmount(), mc)); } @@ -212,10 +208,8 @@ void calculateEMIValueAndRateFactors(final LocalDate calculateFromRepaymentPerio final ProgressiveLoanInterestScheduleModel scheduleModel) { final List relatedRepaymentPeriods = scheduleModel.getRelatedRepaymentPeriods(calculateFromRepaymentPeriodDueDate); calculateRateFactorForPeriods(relatedRepaymentPeriods, scheduleModel); - // TODO: optimalize calculateOutstandingBalance(scheduleModel); calculateEMIOnPeriods(relatedRepaymentPeriods, scheduleModel); - // TODO: optimalize calculateOutstandingBalance(scheduleModel); calculateLastUnpaidRepaymentPeriodEMI(scheduleModel); checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(scheduleModel, relatedRepaymentPeriods); @@ -255,7 +249,9 @@ private void checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(final Progressiv .isGreaterThan(Money.of(originalEmi.getCurrency(), BigDecimal.valueOf(lowerHalfOfRelatedPeriods), mc)); if (shouldBeAdjusted) { - Money adjustment = emiDifference.dividedBy(numberOfRelatedPeriods, mc); + long uncountablePeriods = relatedRepaymentPeriods.stream().filter(rp -> originalEmi.isLessThan(rp.getTotalPaidAmount())) + .count(); + Money adjustment = emiDifference.dividedBy(Math.max(1, numberOfRelatedPeriods - uncountablePeriods), mc); Money adjustedEqualMonthlyInstallmentValue = applyInstallmentAmountInMultiplesOf(scheduleModel, originalEmi.plus(adjustment, mc)); if (adjustedEqualMonthlyInstallmentValue.isEqualTo(originalEmi)) { @@ -264,7 +260,8 @@ private void checkAndAdjustEmiIfNeededOnRelatedRepaymentPeriods(final Progressiv final LocalDate relatedPeriodsFirstDueDate = relatedRepaymentPeriods.get(0).getDueDate(); final ProgressiveLoanInterestScheduleModel newScheduleModel = scheduleModel.deepCopy(mc); newScheduleModel.repaymentPeriods().forEach(period -> { - if (!period.getDueDate().isBefore(relatedPeriodsFirstDueDate)) { + if (!period.getDueDate().isBefore(relatedPeriodsFirstDueDate) + && !adjustedEqualMonthlyInstallmentValue.isLessThan(period.getTotalPaidAmount())) { period.setEmi(adjustedEqualMonthlyInstallmentValue); } }); @@ -419,7 +416,11 @@ void calculateEMIOnPeriods(final List repaymentPeriods, final P calculateEMIValue(rateFactorN, outstandingBalance.getAmount(), fnResult, mc), mc); final Money finalEqualMonthlyInstallment = applyInstallmentAmountInMultiplesOf(scheduleModel, equalMonthlyInstallment); - repaymentPeriods.forEach(period -> period.setEmi(finalEqualMonthlyInstallment)); + repaymentPeriods.forEach(period -> { + if (!finalEqualMonthlyInstallment.isLessThan(period.getTotalPaidAmount())) { + period.setEmi(finalEqualMonthlyInstallment); + } + }); } Money applyInstallmentAmountInMultiplesOf(final ProgressiveLoanInterestScheduleModel scheduleModel, @@ -597,7 +598,7 @@ BigDecimal rateFactorByRepaymentPeriod(final BigDecimal interestRate, final BigD return interestRate// .multiply(interestFractionPerPeriod, mc)// .multiply(actualDaysInPeriod, mc)// - .divide(calculatedDaysInPeriod, mc);// + .divide(calculatedDaysInPeriod, mc).setScale(mc.getPrecision(), mc.getRoundingMode());// } /** @@ -611,7 +612,7 @@ BigDecimal rateFactorByRepaymentPartialPeriod(final BigDecimal interestRate, fin return interestRate// .multiply(interestFractionPerPeriod, mc)// .multiply(actualDaysInPeriod, mc)// - .divide(calculatedDaysInPeriod, mc);// + .divide(calculatedDaysInPeriod, mc).setScale(mc.getPrecision(), mc.getRoundingMode());// } /** diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChangeOperationTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChangeOperationTest.java new file mode 100644 index 00000000000..4269573884f --- /dev/null +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChangeOperationTest.java @@ -0,0 +1,159 @@ +/** + * 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 com.google.common.collect.Collections2; +import java.time.LocalDate; +import java.time.OffsetDateTime; +import java.util.Collection; +import java.util.List; +import java.util.Optional; +import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant; +import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; +import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.mockito.MockedStatic; +import org.mockito.Mockito; + +public class ChangeOperationTest { + + // DateUtils.getOffsetDateTimeOfTenantFromLocalDate + private static final MockedStatic threadLocalContextUtil = Mockito.mockStatic(ThreadLocalContextUtil.class); + + @BeforeAll + public static void init() { + threadLocalContextUtil.when(ThreadLocalContextUtil::getTenant) + .thenReturn(new FineractPlatformTenant(null, null, null, "Europe/Budapest", null)); + } + + @AfterAll + public static void tearDown() { + threadLocalContextUtil.close(); + } + + @Test + public void testCompareToEqual() { + ChangeOperation interestChange = createInterestRateChange("2023-10-17"); + ChangeOperation charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + ChangeOperation transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + Assertions.assertTrue(interestChange.compareTo(transaction) < 0); + Assertions.assertTrue(interestChange.compareTo(charge) < 0); + Assertions.assertTrue(charge.compareTo(transaction) == 0); + Assertions.assertTrue(transaction.compareTo(charge) == 0); + } + + @Test + public void testCompareToEqualBackdatedCharge() { + ChangeOperation interestChange = createInterestRateChange("2023-10-17"); + ChangeOperation charge = createCharge("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00"); + ChangeOperation transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00"); + Assertions.assertTrue(interestChange.compareTo(transaction) > 0); + Assertions.assertTrue(interestChange.compareTo(charge) > 0); + Assertions.assertTrue(charge.compareTo(transaction) == 0); + Assertions.assertTrue(transaction.compareTo(charge) == 0); + } + + @Test + public void testCompareToCreatedDateTime() { + ChangeOperation charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:31+01:00"); + ChangeOperation transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + Assertions.assertTrue(charge.compareTo(transaction) > 0); + Assertions.assertTrue(transaction.compareTo(charge) < 0); + } + + @Test + public void testCompareToSubmittedOnDate() { + ChangeOperation charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + ChangeOperation transaction = createTransaction("2023-10-17", "2023-10-16", "2023-10-17T10:15:30+01:00"); + Assertions.assertTrue(charge.compareTo(transaction) > 0); + Assertions.assertTrue(transaction.compareTo(charge) < 0); + } + + @Test + public void testComparatorEffectiveDate() { + ChangeOperation charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + ChangeOperation transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00"); + Assertions.assertTrue(charge.compareTo(transaction) > 0); + Assertions.assertTrue(transaction.compareTo(charge) < 0); + } + + @Test + public void testComparatorOnDifferentSubmittedDay() { + ChangeOperation cot1 = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); + ChangeOperation cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:16:30+01:00"); + ChangeOperation cot3 = createCharge("2023-10-17", "2023-10-18", "2023-10-18T10:14:30+01:00"); + Collection> permutations = Collections2.permutations(List.of(cot1, cot2, cot3)); + List expected = List.of(cot1, cot3, cot2); + for (List permutation : permutations) { + Assertions.assertEquals(expected, permutation.stream().sorted().toList()); + } + } + + @Test + public void testComparatorOnSameDayBackdatedCharge() { + ChangeOperation cot1 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:31+01:00"); + ChangeOperation cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:15:33+01:00"); + ChangeOperation cot3 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:32+01:00"); + Collection> permutations = Collections2.permutations(List.of(cot1, cot2, cot3)); + List expected = List.of(cot1, cot3, cot2); + for (List permutation : permutations) { + Assertions.assertEquals(expected, permutation.stream().sorted().toList()); + } + } + + @Test + public void testComparatorOnSameDay() { + ChangeOperation cot1 = createCharge("2023-10-24", "2023-10-19", "2023-10-19T10:15:31+01:00"); + ChangeOperation cot2 = createTransaction("2023-10-19", "2023-10-19", "2023-10-19T10:15:33+01:00"); + ChangeOperation cot3 = createCharge("2023-10-24", "2023-10-19", "2023-10-19T10:15:32+01:00"); + Collection> permutations = Collections2.permutations(List.of(cot1, cot2, cot3)); + List expected = List.of(cot1, cot3, cot2); + for (List permutation : permutations) { + Assertions.assertEquals(expected, permutation.stream().sorted().toList()); + } + } + + private ChangeOperation createInterestRateChange(String submittedDate) { + LoanTermVariationsData interestRateChange = Mockito.mock(LoanTermVariationsData.class); + Mockito.when(interestRateChange.getTermVariationApplicableFrom()).thenReturn(LocalDate.parse(submittedDate)); + return new ChangeOperation(interestRateChange); + } + + private ChangeOperation createCharge(String effectiveDate, String submittedDate, String creationDateTime) { + LoanCharge charge = Mockito.mock(LoanCharge.class); + Mockito.when(charge.getDueDate()).thenReturn(LocalDate.parse(effectiveDate)); + Mockito.when(charge.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate)); + Mockito.when(charge.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.parse(creationDateTime))); + return new ChangeOperation(charge); + } + + private ChangeOperation createTransaction(String transactionDate, String submittedDate, String creationDateTime) { + LoanTransaction transaction = Mockito.mock(LoanTransaction.class); + Mockito.when(transaction.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate)); + Mockito.when(transaction.getTransactionDate()).thenReturn(LocalDate.parse(transactionDate)); + Mockito.when(transaction.getCreatedDateTime()).thenReturn(OffsetDateTime.parse(creationDateTime)); + return new ChangeOperation(transaction); + } + +} diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransactionTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransactionTest.java deleted file mode 100644 index 20ac2238b4e..00000000000 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/ChargeOrTransactionTest.java +++ /dev/null @@ -1,127 +0,0 @@ -/** - * 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 com.google.common.collect.Collections2; -import java.time.LocalDate; -import java.time.OffsetDateTime; -import java.util.Collection; -import java.util.List; -import java.util.Optional; -import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.Test; -import org.mockito.Mockito; - -public class ChargeOrTransactionTest { - - @Test - public void testCompareToEqual() { - ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); - ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); - Assertions.assertTrue(charge.compareTo(transaction) == 0); - Assertions.assertTrue(transaction.compareTo(charge) == 0); - } - - @Test - public void testCompareToEqualBackdatedCharge() { - ChargeOrTransaction charge = createCharge("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00"); - ChargeOrTransaction transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00"); - Assertions.assertTrue(charge.compareTo(transaction) == 0); - Assertions.assertTrue(transaction.compareTo(charge) == 0); - } - - @Test - public void testCompareToCreatedDateTime() { - ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:31+01:00"); - ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); - Assertions.assertTrue(charge.compareTo(transaction) > 0); - Assertions.assertTrue(transaction.compareTo(charge) < 0); - } - - @Test - public void testCompareToSubmittedOnDate() { - ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); - ChargeOrTransaction transaction = createTransaction("2023-10-17", "2023-10-16", "2023-10-17T10:15:30+01:00"); - Assertions.assertTrue(charge.compareTo(transaction) > 0); - Assertions.assertTrue(transaction.compareTo(charge) < 0); - } - - @Test - public void testComparatorEffectiveDate() { - ChargeOrTransaction charge = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); - ChargeOrTransaction transaction = createTransaction("2023-10-16", "2023-10-17", "2023-10-17T10:15:30+01:00"); - Assertions.assertTrue(charge.compareTo(transaction) > 0); - Assertions.assertTrue(transaction.compareTo(charge) < 0); - } - - @Test - public void testComparatorOnDifferentSubmittedDay() { - ChargeOrTransaction cot1 = createCharge("2023-10-17", "2023-10-17", "2023-10-17T10:15:30+01:00"); - ChargeOrTransaction cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:16:30+01:00"); - ChargeOrTransaction cot3 = createCharge("2023-10-17", "2023-10-18", "2023-10-18T10:14:30+01:00"); - Collection> permutations = Collections2.permutations(List.of(cot1, cot2, cot3)); - List expected = List.of(cot1, cot3, cot2); - for (List permutation : permutations) { - Assertions.assertEquals(expected, permutation.stream().sorted().toList()); - } - } - - @Test - public void testComparatorOnSameDayBackdatedCharge() { - ChargeOrTransaction cot1 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:31+01:00"); - ChargeOrTransaction cot2 = createTransaction("2023-10-17", "2023-10-19", "2023-10-19T10:15:33+01:00"); - ChargeOrTransaction cot3 = createCharge("2023-10-17", "2023-10-19", "2023-10-19T10:15:32+01:00"); - Collection> permutations = Collections2.permutations(List.of(cot1, cot2, cot3)); - List expected = List.of(cot1, cot3, cot2); - for (List permutation : permutations) { - Assertions.assertEquals(expected, permutation.stream().sorted().toList()); - } - } - - @Test - public void testComparatorOnSameDay() { - ChargeOrTransaction cot1 = createCharge("2023-10-24", "2023-10-19", "2023-10-19T10:15:31+01:00"); - ChargeOrTransaction cot2 = createTransaction("2023-10-19", "2023-10-19", "2023-10-19T10:15:33+01:00"); - ChargeOrTransaction cot3 = createCharge("2023-10-24", "2023-10-19", "2023-10-19T10:15:32+01:00"); - Collection> permutations = Collections2.permutations(List.of(cot1, cot2, cot3)); - List expected = List.of(cot1, cot3, cot2); - for (List permutation : permutations) { - Assertions.assertEquals(expected, permutation.stream().sorted().toList()); - } - } - - private ChargeOrTransaction createCharge(String effectiveDate, String submittedDate, String creationDateTime) { - LoanCharge charge = Mockito.mock(LoanCharge.class); - Mockito.when(charge.getDueDate()).thenReturn(LocalDate.parse(effectiveDate)); - Mockito.when(charge.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate)); - Mockito.when(charge.getCreatedDate()).thenReturn(Optional.of(OffsetDateTime.parse(creationDateTime))); - return new ChargeOrTransaction(charge); - } - - private ChargeOrTransaction createTransaction(String transactionDate, String submittedDate, String creationDateTime) { - LoanTransaction transaction = Mockito.mock(LoanTransaction.class); - Mockito.when(transaction.getSubmittedOnDate()).thenReturn(LocalDate.parse(submittedDate)); - Mockito.when(transaction.getTransactionDate()).thenReturn(LocalDate.parse(transactionDate)); - Mockito.when(transaction.getCreatedDateTime()).thenReturn(OffsetDateTime.parse(creationDateTime)); - return new ChargeOrTransaction(transaction); - } - -} diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrepaymentCalculationTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrepaymentCalculationTest.java new file mode 100644 index 00000000000..1fec1bb9262 --- /dev/null +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/PrepaymentCalculationTest.java @@ -0,0 +1,146 @@ +/** + * 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.loanschedule.domain; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.mockito.Mockito.when; + +import java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.time.LocalDate; +import java.util.List; +import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; +import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.PayableDetails; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.RepaymentPeriod; +import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator; +import org.apache.fineract.portfolio.loanproduct.domain.LoanPreClosureInterestCalculationStrategy; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.MockedStatic; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +public class PrepaymentCalculationTest { + + private static final MockedStatic moneyHelper = Mockito.mockStatic(MoneyHelper.class); + private static final MathContext mc = new MathContext(12, RoundingMode.HALF_EVEN); + private static final MonetaryCurrency monetaryCurrency = MonetaryCurrency + .fromApplicationCurrency(new ApplicationCurrency("USD", "USD", 2, 1, "USD", "$")); + + @Mock + private LoanProductRelatedDetail loanProductRelatedDetail; + + @Mock + private LoanApplicationTerms loanApplicationTerms; + + @Mock + private AdvancedPaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor; + + @Mock + private EMICalculator emiCalculator; + + @InjectMocks + private ProgressiveLoanScheduleGenerator progressiveLoanScheduleGenerator; + + private Loan loan; + + @BeforeEach + public void setUp() { + MockitoAnnotations.openMocks(this); + + moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP)); + moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP); + + loan = Mockito.mock(Loan.class); + when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE); + when(loan.getDisbursementDate()).thenReturn(LocalDate.of(2022, 9, 7)); + when(loan.getRepaymentScheduleInstallments()).thenReturn(createRepaymentScheduleInstallments()); + + when(loanApplicationTerms.getPreClosureInterestCalculationStrategy()) + .thenReturn(LoanPreClosureInterestCalculationStrategy.TILL_PRE_CLOSURE_DATE); + + when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(BigDecimal.valueOf(7.0)); + + List repaymentPeriods = createMockRepaymentPeriods(); + ProgressiveLoanInterestScheduleModel scheduleModel = new ProgressiveLoanInterestScheduleModel(repaymentPeriods, + loanProductRelatedDetail, 100, mc); + + when(loanRepaymentScheduleTransactionProcessor.reprocessProgressiveLoanTransactions(Mockito.any(), Mockito.anyList(), Mockito.any(), + Mockito.anyList(), Mockito.anySet())).thenReturn(org.apache.commons.lang3.tuple.Pair.of(null, scheduleModel)); + + PayableDetails payableDetails = new PayableDetails(Money.of(monetaryCurrency, BigDecimal.valueOf(200)), + Money.of(monetaryCurrency, BigDecimal.valueOf(500)), Money.of(monetaryCurrency, BigDecimal.valueOf(0)), + Money.of(monetaryCurrency, BigDecimal.valueOf(1000))); + + when(emiCalculator.getPayableDetails(Mockito.any(ProgressiveLoanInterestScheduleModel.class), Mockito.any(LocalDate.class), + Mockito.any(LocalDate.class))).thenReturn(payableDetails); + } + + @AfterAll + public static void tearDown() { + moneyHelper.close(); + } + + @Test + public void testCalculatePrepaymentAmount() { + LocalDate prepaymentDate = LocalDate.of(2023, 6, 1); + + OutstandingAmountsDTO result = progressiveLoanScheduleGenerator.calculatePrepaymentAmount(monetaryCurrency, prepaymentDate, + loanApplicationTerms, mc, loan, null, loanRepaymentScheduleTransactionProcessor); + + assertEquals("1000.00", result.principal().getAmount().toString()); + assertEquals("15.00", result.interest().getAmount().toString()); + } + + private List createRepaymentScheduleInstallments() { + LoanRepaymentScheduleInstallment installment1 = new LoanRepaymentScheduleInstallment(loan, 1, LocalDate.of(2022, 10, 1), + LocalDate.of(2022, 11, 1), BigDecimal.valueOf(500), BigDecimal.valueOf(10), BigDecimal.ZERO, BigDecimal.ZERO, false, null); + + LoanRepaymentScheduleInstallment installment2 = new LoanRepaymentScheduleInstallment(loan, 2, LocalDate.of(2022, 11, 2), + LocalDate.of(2022, 12, 1), BigDecimal.valueOf(500), BigDecimal.valueOf(5), BigDecimal.ZERO, BigDecimal.ZERO, false, null); + + return List.of(installment1, installment2); + } + + private List createMockRepaymentPeriods() { + RepaymentPeriod period1 = Mockito.mock(RepaymentPeriod.class); + when(period1.getFromDate()).thenReturn(LocalDate.of(2022, 10, 1)); + when(period1.getDueDate()).thenReturn(LocalDate.of(2022, 11, 1)); + + RepaymentPeriod period2 = Mockito.mock(RepaymentPeriod.class); + when(period2.getFromDate()).thenReturn(LocalDate.of(2022, 11, 2)); + when(period2.getDueDate()).thenReturn(LocalDate.of(2022, 12, 1)); + + return List.of(period1, period2); + } +} diff --git a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java index 2ae49b1a5ca..10b8740c26a 100644 --- a/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java +++ b/fineract-progressive-loan/src/test/java/org/apache/fineract/portfolio/loanproduct/calc/ProgressiveEMICalculatorTest.java @@ -44,7 +44,6 @@ import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Disabled; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Timeout; import org.junit.jupiter.api.extension.ExtendWith; @@ -95,13 +94,12 @@ private BigDecimal getRateFactorsByMonth(final DaysInYearType daysInYearType, fi final BigDecimal daysInPeriod = BigDecimal.valueOf(DateUtils.getDifferenceInDays(period.getFromDate(), period.getDueDate())); final BigDecimal daysInYear = BigDecimal.valueOf(daysInYearType.getNumberOfDays(period.getFromDate())); final BigDecimal daysInMonth = BigDecimal.valueOf(daysInMonthType.getNumberOfDays(period.getFromDate())); - final BigDecimal rateFactor = emiCalculator.rateFactorByRepaymentEveryMonth(interestRate, BigDecimal.ONE, daysInMonth, daysInYear, - daysInPeriod, daysInPeriod, mc); - return rateFactor.setScale(12, MoneyHelper.getRoundingMode()); + return emiCalculator.rateFactorByRepaymentEveryMonth(interestRate, BigDecimal.ONE, daysInMonth, daysInYear, daysInPeriod, + daysInPeriod, mc); } @Test - public void testRateFactorByRepaymentEveryMonthMethod_DayInYear365_DaysInMonthActual() { + public void test_rateFactorByRepaymentEveryMonthMethod_DayInYear365_DaysInMonthActual() { // Given final DaysInYearType daysInYearType = DaysInYearType.DAYS_365; final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL; @@ -116,7 +114,7 @@ public void testRateFactorByRepaymentEveryMonthMethod_DayInYear365_DaysInMonthAc } @Test - public void testRateFactorByRepaymentEveryMonthMethod_DayInYearActual_DaysInMonthActual() { + public void test_rateFactorByRepaymentEveryMonthMethod_DayInYearActual_DaysInMonthActual() { // Given final DaysInYearType daysInYearType = DaysInYearType.ACTUAL; final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL; @@ -132,7 +130,7 @@ public void testRateFactorByRepaymentEveryMonthMethod_DayInYearActual_DaysInMont } @Test - public void testFnValueFunction_RepayEvery1Month_DayInYear365_DaysInMonthActual() { + public void test_fnValueFunction_RepayEvery1Month_DayInYear365_DaysInMonthActual() { // Given final DaysInYearType daysInYearType = DaysInYearType.DAYS_365; final DaysInMonthType daysInMonthType = DaysInMonthType.ACTUAL; @@ -159,7 +157,7 @@ public void testFnValueFunction_RepayEvery1Month_DayInYear365_DaysInMonthActual( } @Test - public void testEMICalculator_generateInterestScheduleModel() { + public void test_generateInterestScheduleModel() { final List expectedRepaymentPeriods = new ArrayList<>(); final Integer installmentAmountInMultiplesOf = null; @@ -184,7 +182,7 @@ public void testEMICalculator_generateInterestScheduleModel() { @Test @Timeout(1) // seconds - public void testEMICalculation_performance() { + public void test_emi_calculator_performance() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -223,19 +221,19 @@ public void testEMICalculation_performance() { List repaymentPeriods = interestSchedule.repaymentPeriods(); for (int i = 0; i < repaymentPeriods.size(); i++) { final RepaymentPeriod repaymentPeriod = repaymentPeriods.get(i); - Assertions.assertTrue(0 < toDouble(repaymentPeriod.getDuePrincipal().getAmount())); - Assertions.assertTrue(0 < toDouble(repaymentPeriod.getDueInterest().getAmount())); + Assertions.assertTrue(0 < toDouble(repaymentPeriod.getDuePrincipal())); + Assertions.assertTrue(0 < toDouble(repaymentPeriod.getDueInterest())); if (i == repaymentPeriods.size() - 1) { - Assertions.assertEquals(0.0, toDouble(repaymentPeriod.getOutstandingLoanBalance().getAmount())); + Assertions.assertEquals(0.0, toDouble(repaymentPeriod.getOutstandingLoanBalance())); } else { - Assertions.assertEquals(8.65, toDouble(repaymentPeriod.getEmi().getAmount())); - Assertions.assertTrue(0 < toDouble(repaymentPeriod.getOutstandingLoanBalance().getAmount())); + Assertions.assertEquals(8.65, toDouble(repaymentPeriod.getEmi())); + Assertions.assertTrue(0 < toDouble(repaymentPeriod.getOutstandingLoanBalance())); } } } @Test - public void testEMICalculation_CheckEmiButNewEmiNotBetterThanOriginal() { + public void test_emiAdjustment_newCalculatedEmiNotBetterThanOriginal() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -272,7 +270,7 @@ public void testEMICalculation_CheckEmiButNewEmiNotBetterThanOriginal() { } @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { + public void test_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -283,7 +281,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -296,7 +294,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66); @@ -309,7 +307,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay } @Test - public void testEMICalculation_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_repayEvery1Month() { + public void test_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -356,7 +354,7 @@ public void testEMICalculation_multi_disbursedAmt200_2ndOnDueDate_dayInYears360_ } @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_reschedule() { + public void test_reschedule_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -367,7 +365,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 15), LocalDate.of(2024, 6, 15))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 15), LocalDate.of(2024, 7, 15))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -380,7 +378,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66); @@ -392,9 +390,8 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay checkPeriod(interestSchedule, 5, 0, 17.13, 0.007901833333, 0.13, 17.00, 0.0); } - @Disabled("till interest rate change got implemented") @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_reschedule_interest_on0201_4per() { + public void test_reschedule_interest_on0201_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -405,7 +402,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("7"); + final BigDecimal interestRate = BigDecimal.valueOf(7.0); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -420,10 +417,10 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); - final BigDecimal interestRateNewValue = new BigDecimal("4"); + final BigDecimal interestRateNewValue = BigDecimal.valueOf(4.0); final LocalDate interestChangeDate = LocalDate.of(2024, 2, 2); emiCalculator.changeInterestRate(interestSchedule, interestChangeDate, interestRateNewValue); @@ -436,9 +433,55 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay checkPeriod(interestSchedule, 5, 0, 16.89, 0.003333333333, 0.06, 16.83, 0.0); } - @Disabled("till interest rate change got implemented") @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_reschedule_interest_on0215_4per() { + public void test_reschedule_interest_on0201_2nd_EMI_not_changeable_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { + + final List expectedRepaymentPeriods = new ArrayList<>(); + + expectedRepaymentPeriods.add(repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1))); + expectedRepaymentPeriods.add(repayment(2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1))); + expectedRepaymentPeriods.add(repayment(3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1))); + expectedRepaymentPeriods.add(repayment(4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1))); + expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); + expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); + + final BigDecimal interestRate = BigDecimal.valueOf(7.0); + final Integer installmentAmountInMultiplesOf = null; + + Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); + Mockito.when(loanProductRelatedDetail.getDaysInYearType()).thenReturn(DaysInYearType.DAYS_360.getValue()); + Mockito.when(loanProductRelatedDetail.getDaysInMonthType()).thenReturn(DaysInMonthType.DAYS_30.getValue()); + Mockito.when(loanProductRelatedDetail.getRepaymentPeriodFrequencyType()).thenReturn(PeriodFrequencyType.MONTHS); + Mockito.when(loanProductRelatedDetail.getRepayEvery()).thenReturn(1); + Mockito.when(loanProductRelatedDetail.getCurrency()).thenReturn(monetaryCurrency); + + threadLocalContextUtil.when(ThreadLocalContextUtil::getBusinessDate).thenReturn(LocalDate.of(2024, 2, 14)); + + final ProgressiveLoanInterestScheduleModel interestModel = emiCalculator.generatePeriodInterestScheduleModel( + expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); + + final Money disbursedAmount = toMoney(100.0); + emiCalculator.addDisbursement(interestModel, LocalDate.of(2024, 1, 1), disbursedAmount); + emiCalculator.payInterest(interestModel, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(0.58)); + emiCalculator.payPrincipal(interestModel, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1), toMoney(16.43)); + + emiCalculator.payPrincipal(interestModel, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 1), toMoney(16.90)); + + final BigDecimal interestRateNewValue = BigDecimal.valueOf(4.0); + final LocalDate interestChangeDate = LocalDate.of(2024, 2, 2); + emiCalculator.changeInterestRate(interestModel, interestChangeDate, interestRateNewValue); + + checkPeriod(interestModel, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 66.67); + checkPeriod(interestModel, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 66.67); + checkPeriod(interestModel, 1, 0, 17.01, 0.003333333333, 0.22, 0.0, 17.01, 66.56); + checkPeriod(interestModel, 2, 0, 16.85, 0.003333333333, 0.22, 0.44, 16.41, 50.15); + checkPeriod(interestModel, 3, 0, 16.85, 0.003333333333, 0.17, 16.68, 33.47); + checkPeriod(interestModel, 4, 0, 16.85, 0.003333333333, 0.11, 16.74, 16.73); + checkPeriod(interestModel, 5, 0, 16.79, 0.003333333333, 0.06, 16.73, 0.0); + } + + @Test + public void test_reschedule_interest_on0215_4per_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -449,7 +492,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("7"); + final BigDecimal interestRate = BigDecimal.valueOf(7.0); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -464,14 +507,15 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); - final BigDecimal interestRateNewValue = new BigDecimal("4"); + final BigDecimal interestRateNewValue = BigDecimal.valueOf(4.0); final LocalDate interestChangeDate = LocalDate.of(2024, 2, 15); emiCalculator.changeInterestRate(interestSchedule, interestChangeDate, interestRateNewValue); - checkPeriod(interestSchedule, 0, 0, 17.01, 0.005833333333, 0.58, 16.43, 83.57); + checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.58, 16.43, 83.57); + checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 1, 0, 16.90, 0.002614942529, 0.22, 0.37, 16.53, 67.04); checkPeriod(interestSchedule, 1, 1, 16.90, 0.001839080460, 0.15, 0.37, 16.53, 67.04); checkPeriod(interestSchedule, 2, 0, 16.90, 0.003333333333, 0.22, 16.68, 50.36); @@ -484,7 +528,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay * This test case tests a period early and late repayment with balance correction */ @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_add_balance_correction_on0215() { + public void test_balance_correction_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -495,7 +539,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("7"); + final BigDecimal interestRate = BigDecimal.valueOf(7.0); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -510,37 +554,36 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); // schedule 1st period 1st day PayableDetails payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 1)); - Assertions.assertEquals(100, toDouble(payableDetails.getOutstandingBalance().getAmount())); - Assertions.assertEquals(17.01, toDouble(payableDetails.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.0, toDouble(payableDetails.getPayableInterest().getAmount())); + Assertions.assertEquals(100, toDouble(payableDetails.getOutstandingBalance())); + Assertions.assertEquals(17.01, toDouble(payableDetails.getPayablePrincipal())); + Assertions.assertEquals(0.0, toDouble(payableDetails.getPayableInterest())); // schedule 2nd period last day payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); - Assertions.assertEquals(83.57, toDouble(payableDetails.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.52, toDouble(payableDetails.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.49, toDouble(payableDetails.getPayableInterest().getAmount())); + Assertions.assertEquals(83.57, toDouble(payableDetails.getOutstandingBalance())); + Assertions.assertEquals(16.52, toDouble(payableDetails.getPayablePrincipal())); + Assertions.assertEquals(0.49, toDouble(payableDetails.getPayableInterest())); // pay off a period with balance correction final LocalDate op1stCorrectionPeriodDueDate = LocalDate.of(2024, 3, 1); final LocalDate op1stCorrectionDate = LocalDate.of(2024, 2, 15); - final Money op1stCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(16.77)); + final Money op1stCorrectionAmount = toMoney(16.77); // get remaining balance and dues for a date final PayableDetails repaymentDetails1st = emiCalculator.getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate); - Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest().getAmount())); + Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance())); + Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal())); + Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest())); emiCalculator.payPrincipal(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, op1stCorrectionAmount); - emiCalculator.payInterest(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, - Money.of(monetaryCurrency, BigDecimal.valueOf(0.24))); + emiCalculator.payInterest(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate, toMoney(0.24)); checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 1, 0, 17.01, 0.002816091954, 0.24, 16.77, 66.80); @@ -553,18 +596,17 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay // totally pay off another period with balance correction final LocalDate op2ndCorrectionPeriodDueDate = LocalDate.of(2024, 4, 1); final LocalDate op2ndCorrectionDate = LocalDate.of(2024, 3, 1); - final Money op2ndCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(16.42)); + final Money op2ndCorrectionAmount = toMoney(16.42); // get remaining balance and dues for a date final PayableDetails repaymentDetails2st = emiCalculator.getPayableDetails(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate); - Assertions.assertEquals(66.80, toDouble(repaymentDetails2st.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.81, toDouble(repaymentDetails2st.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.20, toDouble(repaymentDetails2st.getPayableInterest().getAmount())); + Assertions.assertEquals(66.80, toDouble(repaymentDetails2st.getOutstandingBalance())); + Assertions.assertEquals(16.81, toDouble(repaymentDetails2st.getPayablePrincipal())); + Assertions.assertEquals(0.20, toDouble(repaymentDetails2st.getPayableInterest())); emiCalculator.payPrincipal(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, op2ndCorrectionAmount); - emiCalculator.payInterest(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, - Money.of(monetaryCurrency, BigDecimal.valueOf(0.49))); + emiCalculator.payInterest(interestSchedule, op2ndCorrectionPeriodDueDate, op2ndCorrectionDate, toMoney(0.49)); checkPeriod(interestSchedule, 0, 1, 17.01, 0.005833333333, 0.58, 16.43, 83.57); checkPeriod(interestSchedule, 1, 0, 17.01, 0.002816091954, 0.24, 16.77, 50.38); @@ -578,17 +620,17 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay LocalDate periodDueDate = LocalDate.of(2024, 7, 1); LocalDate payDate = LocalDate.of(2024, 7, 1); final PayableDetails repaymentDetails3rd = emiCalculator.getPayableDetails(interestSchedule, periodDueDate, payDate); - Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.1, toDouble(repaymentDetails3rd.getPayableInterest().getAmount())); + Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getOutstandingBalance())); + Assertions.assertEquals(16.75, toDouble(repaymentDetails3rd.getPayablePrincipal())); + Assertions.assertEquals(0.1, toDouble(repaymentDetails3rd.getPayableInterest())); // check numbers after the last period due date periodDueDate = LocalDate.of(2024, 7, 1); payDate = LocalDate.of(2024, 7, 15); final PayableDetails repaymentDetails4th = emiCalculator.getPayableDetails(interestSchedule, periodDueDate, payDate); - Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.1, toDouble(repaymentDetails4th.getPayableInterest().getAmount())); + Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getOutstandingBalance())); + Assertions.assertEquals(16.75, toDouble(repaymentDetails4th.getPayablePrincipal())); + Assertions.assertEquals(0.1, toDouble(repaymentDetails4th.getPayableInterest())); // balance update on the last period, check the right interest interval split emiCalculator.addBalanceCorrection(interestSchedule, LocalDate.of(2024, 6, 10), Money.of(monetaryCurrency, BigDecimal.ZERO)); @@ -601,7 +643,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay } @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_payoff_on0215() { + public void test_payoff_on0215_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -612,7 +654,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("7"); + final BigDecimal interestRate = BigDecimal.valueOf(7.0); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -627,51 +669,43 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); // partially pay off a period with balance correction final LocalDate op1stCorrectionPeriodDueDate = LocalDate.of(2024, 3, 1); final LocalDate op1stCorrectionDate = LocalDate.of(2024, 2, 15); - final Money op1stCorrectionAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(15.0)); + final Money op1stCorrectionAmount = toMoney(15.0); // get remaining balance and dues for a date final PayableDetails repaymentDetails1st = emiCalculator.getPayableDetails(interestSchedule, op1stCorrectionPeriodDueDate, op1stCorrectionDate); - Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest().getAmount())); + Assertions.assertEquals(83.57, toDouble(repaymentDetails1st.getOutstandingBalance())); + Assertions.assertEquals(16.77, toDouble(repaymentDetails1st.getPayablePrincipal())); + Assertions.assertEquals(0.24, toDouble(repaymentDetails1st.getPayableInterest())); PayableDetails details = null; // check getPayableDetails forcast details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); - Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.49, toDouble(details.getPayableInterest().getAmount())); + Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance())); + Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal())); + Assertions.assertEquals(0.49, toDouble(details.getPayableInterest())); // apply balance change and check again emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), op1stCorrectionDate, op1stCorrectionAmount); details = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 1)); - Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance().getAmount())); - Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.49, toDouble(details.getPayableInterest().getAmount())); - - emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(1.43))); - emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(0.58))); - emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(16.77))); - emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(0.24))); - emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(17.01))); - emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(17.01))); - emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(17.01))); - emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 2, 15), - Money.of(monetaryCurrency, BigDecimal.valueOf(15.77))); + Assertions.assertEquals(83.57, toDouble(details.getOutstandingBalance())); + Assertions.assertEquals(16.52, toDouble(details.getPayablePrincipal())); + Assertions.assertEquals(0.49, toDouble(details.getPayableInterest())); + + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), toMoney(1.43)); + emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 15), toMoney(0.58)); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 15), toMoney(16.77)); + emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 2, 15), toMoney(0.24)); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 2, 15), toMoney(17.01)); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 2, 15), toMoney(17.01)); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 2, 15), toMoney(17.01)); + emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 2, 15), toMoney(15.77)); // check periods in model @@ -685,7 +719,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay } @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month_payoff_on0115() { + public void test_payoff_on0115_disbursedAmt100_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -696,7 +730,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("7"); + final BigDecimal interestRate = BigDecimal.valueOf(7.0); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -717,13 +751,13 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay // get remaining balance and dues on due date PayableDetails payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 2, 1)); - Assertions.assertEquals(16.43, toDouble(payableDetails.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.58, toDouble(payableDetails.getPayableInterest().getAmount())); + Assertions.assertEquals(16.43, toDouble(payableDetails.getPayablePrincipal())); + Assertions.assertEquals(0.58, toDouble(payableDetails.getPayableInterest())); // check numbers on payoff date payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15)); - Assertions.assertEquals(16.75, toDouble(payableDetails.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.26, toDouble(payableDetails.getPayableInterest().getAmount())); + Assertions.assertEquals(16.75, toDouble(payableDetails.getPayablePrincipal())); + Assertions.assertEquals(0.26, toDouble(payableDetails.getPayableInterest())); emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15), toMoney(16.75)); emiCalculator.payInterest(interestSchedule, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 1, 15), toMoney(0.26)); @@ -738,8 +772,8 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay emiCalculator.payPrincipal(interestSchedule, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 1, 15), toMoney(17.01)); payableDetails = emiCalculator.getPayableDetails(interestSchedule, LocalDate.of(2024, 7, 1), LocalDate.of(2024, 7, 1)); - Assertions.assertEquals(15.21, toDouble(payableDetails.getPayablePrincipal().getAmount())); - Assertions.assertEquals(0.5, toDouble(payableDetails.getPayableInterest().getAmount())); + Assertions.assertEquals(15.21, toDouble(payableDetails.getPayablePrincipal())); + Assertions.assertEquals(0.5, toDouble(payableDetails.getPayableInterest())); // check periods in model checkPeriod(interestSchedule, 0, 0, 17.01, 0.0, 0.0, 0.26, 16.75, 15.21); @@ -765,7 +799,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonth30_repay } @Test - public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_daysInMonth30_repayEvery1Month() { + public void test_multiDisbursedAmt300InSamePeriod_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -776,7 +810,7 @@ public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_da expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -789,7 +823,7 @@ public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_da final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + Money disbursedAmount = toMoney(100); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66); @@ -800,7 +834,7 @@ public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_da checkPeriod(interestSchedule, 4, 0, 17.13, 0.007901833333, 0.27, 16.86, 17.0); checkPeriod(interestSchedule, 5, 0, 17.13, 0.007901833333, 0.13, 17.00, 0.0); - disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(200)); + disbursedAmount = toMoney(200.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 51.33, 0.0, 0.0, 2.02, 49.31, 250.69); @@ -814,7 +848,7 @@ public void testEMICalculation_multiDisbursedAmt300InSamePeriod_dayInYears360_da } @Test - public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears360_daysInMonth30_repayEvery1Month() { + public void test_multiDisbursedAmt200InDifferentPeriod_dayInYears360_daysInMonth30_repayEvery1Month() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -825,7 +859,7 @@ public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears3 expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -838,7 +872,7 @@ public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears3 final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66); @@ -849,7 +883,7 @@ public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears3 checkPeriod(interestSchedule, 4, 0, 17.13, 0.007901833333, 0.27, 16.86, 17.0); checkPeriod(interestSchedule, 5, 0, 17.13, 0.007901833333, 0.13, 17.00, 0.0); - disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 2, 15), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.79, 16.34, 83.66); @@ -863,7 +897,7 @@ public void testEMICalculation_multiDisbursedAmt200InDifferentPeriod_dayInYears3 } @Test - public void testEMICalculation_multiDisbursedAmt150InSamePeriod_dayInYears360_daysInMonth30_repayEvery1Month_backdated_disbursement() { + public void test_multiDisbursedAmt150InSamePeriod_dayInYears360_daysInMonth30_repayEvery1Month_backdated_disbursement() { final List expectedRepaymentPeriods = new ArrayList<>(); @@ -874,7 +908,7 @@ public void testEMICalculation_multiDisbursedAmt150InSamePeriod_dayInYears360_da expectedRepaymentPeriods.add(repayment(5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1))); expectedRepaymentPeriods.add(repayment(6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -887,14 +921,14 @@ public void testEMICalculation_multiDisbursedAmt150InSamePeriod_dayInYears360_da final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 5), disbursedAmount); - disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(50)); + disbursedAmount = toMoney(50.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount); // add disbursement on same date - disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(25)); + disbursedAmount = toMoney(25.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 8), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 29.94, 0.001019591398, 0.00, 1.15, 28.79, 146.21); @@ -908,7 +942,7 @@ public void testEMICalculation_multiDisbursedAmt150InSamePeriod_dayInYears360_da } @Test - public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1Month() { + public void test_disbursedAmt100_dayInYearsActual_daysInMonthActual_repayEvery1Month() { final List expectedRepaymentPeriods = List.of( repayment(1, LocalDate.of(2023, 12, 12), LocalDate.of(2024, 1, 12)), @@ -918,7 +952,7 @@ public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActua repayment(5, LocalDate.of(2024, 4, 12), LocalDate.of(2024, 5, 1)), repayment(6, LocalDate.of(2024, 5, 12), LocalDate.of(2024, 6, 1))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -931,7 +965,7 @@ public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActua final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2023, 12, 12), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 17.13, 0.0, 0.0, 0.80, 16.33, 83.67); @@ -944,7 +978,7 @@ public void testEMICalculation_disbursedAmt100_dayInYearsActual_daysInMonthActua } @Test - public void testEMICalculation_disbursedAmt1000_NoInterest_repayEvery1Month() { + public void test_disbursedAmt1000_NoInterest_repayEvery1Month() { final List expectedRepaymentPeriods = List.of( repayment(2, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1)), @@ -952,7 +986,7 @@ public void testEMICalculation_disbursedAmt1000_NoInterest_repayEvery1Month() { repayment(4, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1)), repayment(5, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1))); - final BigDecimal interestRate = new BigDecimal("0"); + final BigDecimal interestRate = BigDecimal.ZERO; final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -965,7 +999,7 @@ public void testEMICalculation_disbursedAmt1000_NoInterest_repayEvery1Month() { final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(1000)); + final Money disbursedAmount = toMoney(1000.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 250.0, 0.0, 0.0, 250.0, 750.0); @@ -975,7 +1009,7 @@ public void testEMICalculation_disbursedAmt1000_NoInterest_repayEvery1Month() { } @Test - public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery1Week() { + public void test_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery1Week() { final List expectedRepaymentPeriods = List.of( repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 8)), @@ -985,7 +1019,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_r repayment(5, LocalDate.of(2024, 1, 29), LocalDate.of(2024, 2, 5)), repayment(6, LocalDate.of(2024, 2, 5), LocalDate.of(2024, 2, 12))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -998,7 +1032,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_r final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 16.77, 0.0, 0.0, 0.18, 16.59, 83.41); @@ -1011,14 +1045,14 @@ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_r } @Test - public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery2Week() { + public void test_disbursedAmt100_dayInYears364_daysInMonthActual_repayEvery2Week() { final List expectedRepaymentPeriods = List.of( repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 15)), repayment(2, LocalDate.of(2024, 1, 15), LocalDate.of(2024, 1, 29)), repayment(3, LocalDate.of(2024, 1, 29), LocalDate.of(2024, 2, 12))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -1031,7 +1065,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_r final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 33.57, 0.0, 0.0, 0.36, 33.21, 66.79); @@ -1041,7 +1075,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears364_daysInMonthActual_r } @Test - public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days() { + public void test_disbursedAmt100_dayInYears360_daysInMonthDoesntMatter_repayEvery15Days() { final List expectedRepaymentPeriods = List.of( repayment(1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 16)), @@ -1051,7 +1085,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonthDoesntMa repayment(5, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 3, 16)), repayment(6, LocalDate.of(2024, 3, 16), LocalDate.of(2024, 3, 31))); - final BigDecimal interestRate = new BigDecimal("9.4822"); + final BigDecimal interestRate = BigDecimal.valueOf(9.4822); final Integer installmentAmountInMultiplesOf = null; Mockito.when(loanProductRelatedDetail.getAnnualNominalInterestRate()).thenReturn(interestRate); @@ -1064,7 +1098,7 @@ public void testEMICalculation_disbursedAmt100_dayInYears360_daysInMonthDoesntMa final ProgressiveLoanInterestScheduleModel interestSchedule = emiCalculator.generatePeriodInterestScheduleModel( expectedRepaymentPeriods, loanProductRelatedDetail, installmentAmountInMultiplesOf, mc); - final Money disbursedAmount = Money.of(monetaryCurrency, BigDecimal.valueOf(100)); + final Money disbursedAmount = toMoney(100.0); emiCalculator.addDisbursement(interestSchedule, LocalDate.of(2024, 1, 1), disbursedAmount); checkPeriod(interestSchedule, 0, 0, 16.90, 0.0, 0.0, 0.40, 16.50, 83.50); @@ -1105,16 +1139,20 @@ private static void checkPeriod(final ProgressiveLoanInterestScheduleModel inter final RepaymentPeriod repaymentPeriod = interestScheduleModel.repaymentPeriods().get(repaymentIdx); final InterestPeriod interestPeriod = repaymentPeriod.getInterestPeriods().get(interestIdx); - Assertions.assertEquals(emiValue, toDouble(repaymentPeriod.getEmi().getAmount())); + Assertions.assertEquals(emiValue, toDouble(repaymentPeriod.getEmi())); Assertions.assertEquals(rateFactor, toDouble(applyMathContext(interestPeriod.getRateFactor()))); - Assertions.assertEquals(interestDue, toDouble(interestPeriod.getCalculatedDueInterest().getAmount())); - Assertions.assertEquals(interestDueCumulated, toDouble(repaymentPeriod.getDueInterest().getAmount())); - Assertions.assertEquals(principalDue, toDouble(repaymentPeriod.getDuePrincipal().getAmount())); - Assertions.assertEquals(remaingBalance, toDouble(repaymentPeriod.getOutstandingLoanBalance().getAmount())); + Assertions.assertEquals(interestDue, toDouble(interestPeriod.getCalculatedDueInterest())); + Assertions.assertEquals(interestDueCumulated, toDouble(repaymentPeriod.getDueInterest())); + Assertions.assertEquals(principalDue, toDouble(repaymentPeriod.getDuePrincipal())); + Assertions.assertEquals(remaingBalance, toDouble(repaymentPeriod.getOutstandingLoanBalance())); + } + + private static double toDouble(final Money value) { + return value == null ? 0.0 : toDouble(value.getAmount()); } private static double toDouble(final BigDecimal value) { - return value == null ? 0 : value.doubleValue(); + return value == null ? 0.0 : value.doubleValue(); } private static BigDecimal applyMathContext(final BigDecimal value) { diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java index 9bfffb582da..65073621afe 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilter.java @@ -32,6 +32,7 @@ import org.apache.fineract.useradministration.exception.UnAuthenticatedUserException; import org.apache.http.HttpStatus; import org.springframework.context.annotation.Conditional; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; import org.springframework.web.filter.OncePerRequestFilter; @RequiredArgsConstructor @@ -84,7 +85,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse } } } catch (UnAuthenticatedUserException e) { - Reject.reject(null, HttpStatus.SC_UNAUTHORIZED).toServletResponse(response); + throw new AuthenticationCredentialsNotFoundException("Not Authenticated", e); } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/SpringSecurityPlatformSecurityContext.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/SpringSecurityPlatformSecurityContext.java index 4a8628d3241..3775c2eacc5 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/SpringSecurityPlatformSecurityContext.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/security/service/SpringSecurityPlatformSecurityContext.java @@ -63,7 +63,10 @@ public AppUser authenticatedUser() { if (context != null) { final Authentication auth = context.getAuthentication(); if (auth != null) { - currentUser = (AppUser) auth.getPrincipal(); + Object principal = auth.getPrincipal(); + if (principal instanceof AppUser appUser) { + currentUser = appUser; + } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index daacb6b03e5..ce5332e858d 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -860,7 +860,6 @@ public Pair makeRefund(final Loan loan, final transactionDate, txnExternalId); final boolean isTransactionChronologicallyLatest = loan.isChronologicallyLatestRepaymentOrWaiver(refundTransaction); - loan.getLoanTransactions().add(refundTransaction); boolean shouldCreateInterestRefundTransaction = loan.getLoanProductRelatedDetail().getSupportedInterestRefundTypes().stream() .map(LoanSupportedInterestRefundTypes::getTransactionType) @@ -869,9 +868,6 @@ public Pair makeRefund(final Loan loan, final if (shouldCreateInterestRefundTransaction) { interestRefundTransaction = createInterestRefundLoanTransaction(loan, transactionDate, transactionAmount); - if (interestRefundTransaction != null) { - loan.addLoanTransaction(interestRefundTransaction); - } } final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = transactionProcessorFactory @@ -887,13 +883,24 @@ public Pair makeRefund(final Loan loan, final || !currentInstallment.getTotalOutstanding(loan.getCurrency()).isEqualTo(refundTransaction.getAmount(loan.getCurrency())); // if (!reprocess) { + loan.getLoanTransactions().add(refundTransaction); loanRepaymentScheduleTransactionProcessor.processLatestTransaction(refundTransaction, new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); + if (interestRefundTransaction != null) { + loan.addLoanTransaction(interestRefundTransaction); + loanRepaymentScheduleTransactionProcessor.processLatestTransaction(interestRefundTransaction, + new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), + new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); + } } else { if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); } + loan.getLoanTransactions().add(refundTransaction); + if (interestRefundTransaction != null) { + loan.addLoanTransaction(interestRefundTransaction); + } final List allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( loan.getDisbursementDate(), allNonContraTransactionsPostDisbursement, loan.getCurrency(), diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java index 047fb2e35a0..71acf9ebebf 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java @@ -1154,6 +1154,7 @@ private void processAccrualTransactionsOnLoanClosure(Loan loan) { updateLoanChargesAndInstallmentChargesPaidBy(loan, accrualTransaction); // TODO check if this is required // saveLoanTransactionWithDataIntegrityViolationChecks(accrualTransaction); + accrualTransaction = loanTransactionRepository.saveAndFlush(accrualTransaction); loan.addLoanTransaction(accrualTransaction); businessEventNotifierService.notifyPostBusinessEvent(new LoanAccrualTransactionCreatedBusinessEvent(accrualTransaction)); @@ -1246,11 +1247,17 @@ private void determineReceivableIncomeDetailsForLoanClosure(Loan loan, Map dataV } private boolean transactionHappenedAfterOther(LoanTransaction transaction, LoanTransaction otherTransaction) { - return new ChargeOrTransaction(transaction).compareTo(new ChargeOrTransaction(otherTransaction)) > 0; + return new ChangeOperation(transaction).compareTo(new ChangeOperation(otherTransaction)) > 0; } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java index 28087f74a41..58ad2fb7c00 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationValidator.java @@ -34,7 +34,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor; -import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ChargeOrTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.ChangeOperation; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.springframework.stereotype.Component; @@ -130,6 +130,6 @@ private void throwExceptionIfValidationErrorsExist(List dataV } private boolean transactionHappenedAfterOther(LoanTransaction transaction, LoanTransaction otherTransaction) { - return new ChargeOrTransaction(transaction).compareTo(new ChargeOrTransaction(otherTransaction)) > 0; + return new ChangeOperation(transaction).compareTo(new ChangeOperation(otherTransaction)) > 0; } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java index ff78d092dca..37452d3bfbb 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/jobs/filter/LoanCOBApiFilterTest.java @@ -20,18 +20,24 @@ import static org.apache.fineract.infrastructure.jobs.filter.LoanCOBFilterHelper.LOAN_GLIMACCOUNT_PATH_PATTERN; import static org.apache.fineract.infrastructure.jobs.filter.LoanCOBFilterHelper.LOAN_PATH_PATTERN; +import static org.junit.jupiter.api.Assertions.assertThrows; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyList; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.doThrow; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import com.sun.research.ws.wadl.HTTPMethods; import jakarta.servlet.FilterChain; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.PrintWriter; @@ -58,6 +64,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequestRepository; import org.apache.fineract.useradministration.domain.AppUser; +import org.apache.fineract.useradministration.exception.UnAuthenticatedUserException; import org.apache.http.HttpStatus; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.Assertions; @@ -71,6 +78,7 @@ import org.mockito.quality.Strictness; import org.springframework.mock.web.MockHttpServletRequest; import org.springframework.mock.web.MockHttpServletResponse; +import org.springframework.security.authentication.AuthenticationCredentialsNotFoundException; @ExtendWith(MockitoExtension.class) @MockitoSettings(strictness = Strictness.LENIENT) @@ -445,4 +453,41 @@ void shouldRejectWhenGlimLoanIsHardLocked() throws ServletException, IOException testObj.doFilterInternal(request, response, filterChain); verify(response, times(1)).setStatus(HttpStatus.SC_CONFLICT); } + + @Test + void shouldThrowAuthenticationCredentialsNotFoundException_WhenUnAuthenticatedUserExceptionIsThrown() throws IOException { + LoanCOBFilterHelper spyHelper = spy(helper); + testObj = new LoanCOBApiFilter(spyHelper); + MockHttpServletRequest request = mock(MockHttpServletRequest.class); + MockHttpServletResponse response = mock(MockHttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + final byte[] cachedBody = new byte[0]; + given(request.getInputStream()) + .willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new ByteArrayInputStream(cachedBody))); + doReturn(true).when(spyHelper).isOnApiList(any(BodyCachingHttpServletRequestWrapper.class)); + doThrow(new UnAuthenticatedUserException()).when(spyHelper).isBypassUser(); + + assertThrows(AuthenticationCredentialsNotFoundException.class, () -> testObj.doFilterInternal(request, response, filterChain)); + verifyNoInteractions(filterChain); + } + + @Test + void shouldProceed_WhenAuthenticatedUser() throws Exception { + LoanCOBFilterHelper spyHelper = spy(helper); + testObj = new LoanCOBApiFilter(spyHelper); + MockHttpServletRequest request = mock(MockHttpServletRequest.class); + MockHttpServletResponse response = mock(MockHttpServletResponse.class); + FilterChain filterChain = mock(FilterChain.class); + + final byte[] cachedBody = new byte[0]; + given(request.getInputStream()) + .willReturn(new BodyCachingHttpServletRequestWrapper.CachedBodyServletInputStream(new ByteArrayInputStream(cachedBody))); + doReturn(true).when(spyHelper).isOnApiList(any(BodyCachingHttpServletRequestWrapper.class)); + doReturn(true).when(spyHelper).isBypassUser(); + + testObj.doFilterInternal(request, response, filterChain); + + verify(filterChain, times(1)).doFilter(any(HttpServletRequest.class), any(HttpServletResponse.class)); + } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/infrastructure/security/service/SpringSecurityPlatformSecurityContextTest.java b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/security/service/SpringSecurityPlatformSecurityContextTest.java new file mode 100644 index 00000000000..d9347c6d99d --- /dev/null +++ b/fineract-provider/src/test/java/org/apache/fineract/infrastructure/security/service/SpringSecurityPlatformSecurityContextTest.java @@ -0,0 +1,114 @@ +/** + * 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.infrastructure.security.service; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertThrows; +import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.spy; +import static org.mockito.Mockito.when; + +import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; +import org.apache.fineract.infrastructure.security.exception.ResetPasswordException; +import org.apache.fineract.useradministration.domain.AppUser; +import org.apache.fineract.useradministration.exception.UnAuthenticatedUserException; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; + +@ExtendWith(MockitoExtension.class) +class SpringSecurityPlatformSecurityContextTest { + + @Mock + private SecurityContext securityContext; + + @Mock + private Authentication authentication; + + @Mock + private AppUser appUser; + + @Mock + private ConfigurationDomainService configurationDomainService; + + @InjectMocks + private SpringSecurityPlatformSecurityContext securityContextProvider; + + @BeforeEach + void setUp() { + SecurityContextHolder.setContext(securityContext); + } + + @AfterEach + void tearDown() { + SecurityContextHolder.clearContext(); + } + + @Test + void shouldReturnAppUserWhenPrincipalIsAppUser() { + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(appUser); + when(configurationDomainService.isPasswordForcedResetEnable()).thenReturn(false); + + AppUser result = securityContextProvider.authenticatedUser(); + + assertEquals(appUser, result, "authenticatedUser() should return AppUser"); + } + + @Test + void shouldThrowUnAuthenticatedUserExceptionWhenPrincipalIsNotAppUser() { + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn("anonymousUser"); + + assertThrows(UnAuthenticatedUserException.class, + () -> securityContextProvider.authenticatedUser(), + "authenticatedUser() should throw UnAuthenticatedUserException when " + + "Principal is not AppUser"); + } + + @Test + void shouldThrowUnAuthenticatedUserExceptionWhenAuthenticationIsNull() { + when(securityContext.getAuthentication()).thenReturn(null); + + assertThrows(UnAuthenticatedUserException.class, + () -> securityContextProvider.authenticatedUser(), + "authenticatedUser() should throw UnAuthenticatedUserException when " + + "Authentication is null"); + } + + @Test + void shouldThrowResetPasswordExceptionWhenPasswordMustBeReset() { + when(securityContext.getAuthentication()).thenReturn(authentication); + when(authentication.getPrincipal()).thenReturn(appUser); + SpringSecurityPlatformSecurityContext spyContextProvider = spy(securityContextProvider); + doReturn(true).when(spyContextProvider).doesPasswordHasToBeRenewed(appUser); + + assertThrows(ResetPasswordException.class, + spyContextProvider::authenticatedUser, + "authenticatedUser() should throw ResetPasswordException when password needs" + + " to be reset"); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index b91140e8e0c..c01171f4750 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -48,6 +48,7 @@ import org.apache.fineract.client.models.GetLoansLoanIdLoanChargeData; import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod; import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdTransactions; import org.apache.fineract.client.models.LoanProduct; import org.apache.fineract.client.models.PaymentAllocationOrder; import org.apache.fineract.client.models.PostClientsResponse; @@ -55,6 +56,7 @@ import org.apache.fineract.client.models.PostCreateRescheduleLoansResponse; import org.apache.fineract.client.models.PostLoanProductsRequest; import org.apache.fineract.client.models.PostLoanProductsResponse; +import org.apache.fineract.client.models.PostLoansLoanIdChargesChargeIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest; @@ -81,6 +83,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; +import org.apache.fineract.portfolio.loanproduct.domain.LoanRescheduleStrategyMethod; import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.BeforeAll; @@ -5211,6 +5214,124 @@ public void uc147c() { }); } + // UC14-: Advanced payment allocation with Interest, Interest Recalculation Enabled + // Interest Calculation Period Type: daily + // Use original EMI and apply disbursement and change Interest Rate + // ADVANCED_PAYMENT_ALLOCATION_STRATEGY + // 1. Create a Loan product with Adv. Payment. Alloc. and with 7% Interest, 360/30, 1 repayment per month + // 2. Submit Loan and approve + // 3. Disburse + // 4. Validate Repayment Schedule + // 5. Reschedule Interest Rate to 4% + // 6. Validate Repayment Schedule + @Test + public void uc147d() { + final String operationDate = "1 January 2024"; + runAt(operationDate, () -> { + BigDecimal interestRatePerPeriod = BigDecimal.valueOf(7.0); + + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .interestRatePerPeriod(interestRatePerPeriod.doubleValue()).interestRateFrequencyType(YEARS)// + .daysInMonthType(DaysInMonthType.DAYS_30)// + .daysInYearType(DaysInYearType.DAYS_360)// + .numberOfRepayments(6)// + .repaymentEvery(1)// + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())// + .repaymentStartDateType(LoanProduct.RepaymentStartDateTypeEnum.SUBMITTED_ON_DATE.ordinal())// + .enableDownPayment(false)// + .allowPartialPeriodInterestCalcualtion(null)// + .enableAutoRepaymentForDownPayment(null)// + .isInterestRecalculationEnabled(true)// + .interestRecalculationCompoundingMethod(0)// + .interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)// + .multiDisburseLoan(true)// + .disallowExpectedDisbursements(true)// + .allowApprovedDisbursedAmountsOverApplied(true)// + .overAppliedCalculationType("flat")// + .overAppliedNumber(10000)// + .maxTrancheCount(10)// + .outstandingLoanBalance(10000.0)// + .installmentAmountInMultiplesOf(null)// + .rescheduleStrategyMethod(LoanRescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD.getValue())// + .recalculationRestFrequencyType(1);// + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 100.0, 6); + + applicationRequest = applicationRequest.numberOfRepayments(6)// + .loanTermFrequency(6)// + .loanTermFrequencyType(2)// + .interestRatePerPeriod(interestRatePerPeriod)// + .interestCalculationPeriodType(DAYS)// + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)// + .repaymentEvery(1)// + .repaymentFrequencyType(2)// + .maxOutstandingLoanBalance(BigDecimal.valueOf(10000.0))// + ;// + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()// + .approvedLoanAmount(BigDecimal.valueOf(100))// + .approvedOnDate(operationDate).dateFormat(DATETIME_PATTERN).locale("en"));// + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest()// + .transactionAmount(BigDecimal.valueOf(100.0))// + .actualDisbursementDate(operationDate).dateFormat(DATETIME_PATTERN).locale("en"));// + + GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 102.05, 0.0, 100.0, 0.0, null); + + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 16.43, 0.0, 16.43, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.58, 0.0, + 0.58, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 16.52, 0.0, 16.52, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.49, 0.0, + 0.49, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 4, 1), 16.62, 0.0, 16.62, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.39, 0.0, + 0.39, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 5, 1), 16.72, 0.0, 16.72, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.29, 0.0, + 0.29, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 6, 1), 16.81, 0.0, 16.81, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.20, 0.0, + 0.20, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 6, LocalDate.of(2024, 7, 1), 16.90, 0.0, 16.90, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.10, 0.0, + 0.10, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + assertEquals(loanDetails.getNumberOfRepayments(), 6); + + updateBusinessDate("1 February 2024"); + loanTransactionHelper.makeRepayment("1 February 2024", 17.01f, loanResponse.getLoanId().intValue()); + + updateBusinessDate("14 February 2024"); + PostCreateRescheduleLoansResponse rescheduleLoansResponse = loanRescheduleRequestHelper// + .createLoanRescheduleRequest(new PostCreateRescheduleLoansRequest()// + .loanId(loanDetails.getId())// + .rescheduleReasonId(1L)// + .rescheduleFromDate("15 February 2024").dateFormat(DATETIME_PATTERN).locale("en")// + .submittedOnDate("14 February 2024")// + .newInterestRate(BigDecimal.valueOf(4.0)));// + + loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleLoansResponse.getResourceId(), // + new PostUpdateRescheduleLoansRequest()// + .approvedOnDate("14 February 2024").locale("en").dateFormat(DATETIME_PATTERN));// + + loanDetails = loanTransactionHelper.getLoanDetails(loanResponse.getLoanId()); + validateLoanSummaryBalances(loanDetails, 84.5, 17.01, 83.57, 16.43, null); + + validateRepaymentPeriod(loanDetails, 1, LocalDate.of(2024, 2, 1), 16.43, 16.43, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.58, 0.58, + 0.0, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 2, LocalDate.of(2024, 3, 1), 16.53, 0.0, 16.53, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.37, 0.0, + 0.37, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 3, LocalDate.of(2024, 4, 1), 16.68, 0.0, 16.68, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.22, 0.0, + 0.22, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 4, LocalDate.of(2024, 5, 1), 16.73, 0.0, 16.73, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.17, 0.0, + 0.17, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 5, LocalDate.of(2024, 6, 1), 16.79, 0.0, 16.79, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.11, 0.0, + 0.11, 0.0, 0.0); + validateRepaymentPeriod(loanDetails, 6, LocalDate.of(2024, 7, 1), 16.84, 0.0, 16.84, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.06, 0.0, + 0.06, 0.0, 0.0); + assertTrue(loanDetails.getStatus().getActive()); + }); + } + // uc148a: Advanced payment allocation, with Interest Recalculation in Loan Product and Adjust last, unpaid period // ADVANCED_PAYMENT_ALLOCATION_STRATEGY // 1. Create a Loan product with Adv. Pment. Alloc. with Interest Recalculation enabled and Adjust last, unpaid @@ -5274,6 +5395,84 @@ public void uc148c() { }); } + // UC149: Validate TotalUnpaidAccruedDueInterest in Zero when Interest paid is higher than Accrued Interest + // 1. Create a Loan product with Adv. Pment. Alloc. and with Declining Balance, Accrual accounting and Daily Accrual + // Activity + // 2. Submit Loan, approve and Disburse + // 3. Add a Loan Specific Due date charge + // 4. Add a Repayment higher than accrued interest to validate TotalUnpaidAccruedDueInterest equal to Zero + @Test + public void uc149() { + String operationDate = "22 April 2024"; + AtomicLong createdLoanId = new AtomicLong(); + AtomicLong createdLoanChargeId = new AtomicLong(); + AtomicLong createdLoanAccrualId = new AtomicLong(); + runAt(operationDate, () -> { + Long clientId = client.getClientId(); + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .interestRateFrequencyType(YEARS).numberOfRepayments(4)// + .maxInterestRatePerPeriod((double) 0)// + .repaymentEvery(1)// + .repaymentFrequencyType(1L)// + .allowPartialPeriodInterestCalcualtion(false)// + .multiDisburseLoan(false)// + .disallowExpectedDisbursements(null)// + .allowApprovedDisbursedAmountsOverApplied(null)// + .overAppliedCalculationType(null)// + .overAppliedNumber(null)// + .installmentAmountInMultiplesOf(null)// + .loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) // + ;// + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), operationDate, 400.0, 6); + + applicationRequest = applicationRequest.interestCalculationPeriodType(DAYS).interestRatePerPeriod(BigDecimal.ZERO) + .transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY); + + PostLoansResponse loanResponse = loanTransactionHelper.applyLoan(applicationRequest); + createdLoanId.set(loanResponse.getLoanId()); + + loanTransactionHelper.approveLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest() + .approvedLoanAmount(BigDecimal.valueOf(400.0)).dateFormat(DATETIME_PATTERN).approvedOnDate(operationDate).locale("en")); + + loanTransactionHelper.disburseLoan(loanResponse.getLoanId(), new PostLoansLoanIdRequest().actualDisbursementDate(operationDate) + .dateFormat(DATETIME_PATTERN).transactionAmount(BigDecimal.valueOf(400.0)).locale("en")); + + addRepaymentForLoan(createdLoanId.get(), 600.00, operationDate); + + createdLoanChargeId.set(addCharge(createdLoanId.get(), true, 30, operationDate)); + + executeInlineCOB(createdLoanId.get()); + final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); + // Validate Loan Accrual transaction + final GetLoansLoanIdTransactions loanTransaction = loanDetails.getTransactions().stream() + .filter(t -> Boolean.TRUE.equals(t.getType().getAccrual())).toList().get(0); + assertNotNull(loanTransaction); + createdLoanAccrualId.set(loanTransaction.getId()); + }); + + runAt("10 October 2024", () -> { + PostLoansLoanIdChargesChargeIdRequest request = new PostLoansLoanIdChargesChargeIdRequest().amount(15.0).locale("en"); + loanTransactionHelper.chargeAdjustment(createdLoanId.get(), createdLoanChargeId.get(), request); + + loanTransactionHelper.makeGoodwillCredit(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate(operationDate).locale("en").transactionAmount(15.0)); + + final GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoanDetails(createdLoanId.get()); + // Get the Repayment transaction + GetLoansLoanIdTransactions loanTransaction = loanDetails.getTransactions().stream() + .filter(t -> Boolean.TRUE.equals(t.getType().getRepayment())).toList().get(0); + loanTransactionHelper.reverseRepayment(Math.toIntExact(createdLoanId.get()), Math.toIntExact(loanTransaction.getId()), + operationDate); + + // Validate Loan Accrual transaction + loanTransaction = loanDetails.getTransactions().stream().filter(t -> Boolean.TRUE.equals(t.getType().getAccrual())).toList() + .get(0); + assertNotNull(loanTransaction); + assertEquals(loanTransaction.getId(), createdLoanAccrualId.get()); + }); + } + private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId, Integer numberOfRepayments, String loanDisbursementDate, double amount) { LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------"); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AuthenticationIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AuthenticationIntegrationTest.java new file mode 100644 index 00000000000..33b40791855 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AuthenticationIntegrationTest.java @@ -0,0 +1,127 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests; + +import static org.junit.jupiter.api.Assertions.assertEquals; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.util.Collections; +import java.util.HashMap; +import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.integrationtests.common.ClientHelper; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.accounting.AccountHelper; +import org.apache.fineract.integrationtests.common.loans.LoanApplicationTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder; +import org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension; +import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper; +import org.apache.fineract.integrationtests.common.organisation.StaffHelper; +import org.apache.fineract.integrationtests.useradministration.users.UserHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +@Slf4j +@ExtendWith(LoanTestLifecycleExtension.class) +public class AuthenticationIntegrationTest { + + private static final String LOAN_DATE = "11 July 2022"; + private static final String APPROVE_COMMAND = "approve"; + private ResponseSpecification responseSpec; + private RequestSpecification requestSpec; + private LoanTransactionHelper loanTransactionHelper; + private Integer loanID; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + setupAuthenticatedRequestSpec(); + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); + + AccountHelper accountHelper = new AccountHelper(this.requestSpec, this.responseSpec); + Integer staffId = StaffHelper.createStaff(this.requestSpec, this.responseSpec); + String username = Utils.uniqueRandomStringGenerator("user", 8); + UserHelper.createUser(this.requestSpec, this.responseSpec, 1, staffId, username, "P4ssw0rd", "resourceId"); + Integer clientID = ClientHelper.createClient(requestSpec, responseSpec); + + Integer loanProductID = setupLoanProduct(accountHelper); + this.loanID = loanTransactionHelper.applyForLoanApplicationWithPaymentStrategyAndPastMonth(clientID, loanProductID, + Collections.emptyList(), null, "10000", LoanApplicationTestBuilder.DEFAULT_STRATEGY, "10 July 2022", LOAN_DATE); + } + + @Test + public void shouldAllowAccessForAuthenticatedUser() { + setupAuthenticatedRequestSpec(); + String loanApprovalCommand = createLoanApprovalCommand(); + String loanApprovalRequest = createLoanApprovalRequest(); + + HashMap response = Utils.performServerPost(this.requestSpec, this.responseSpec, loanApprovalCommand, loanApprovalRequest, + "changes"); + HashMap status = (HashMap) response.get("status"); + + assertEquals(200, (Integer) status.get("id")); + } + + @Test + public void shouldReturnUnauthorizedForUnauthenticatedAccess() throws JsonProcessingException { + setupUnauthenticatedRequestSpec(); + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(401).build(); + + String loanApprovalCommand = createLoanApprovalCommand(); + String loanApprovalRequest = createLoanApprovalRequest(); + + String rawResponse = Utils.performServerPost(this.requestSpec, this.responseSpec, loanApprovalCommand, loanApprovalRequest, null); + + ObjectMapper objectMapper = new ObjectMapper(); + HashMap response = objectMapper.readValue(rawResponse, HashMap.class); + + assertEquals(401, (Integer) response.get("status")); + assertEquals("Unauthorized", response.get("error")); + } + + private Integer setupLoanProduct(AccountHelper accountHelper) { + return this.loanTransactionHelper.createLoanProduct("0", "0", LoanProductTestBuilder.DEFAULT_STRATEGY, "2", + accountHelper.createAssetAccount(), accountHelper.createIncomeAccount(), accountHelper.createExpenseAccount(), + accountHelper.createLiabilityAccount()); + } + + private void setupAuthenticatedRequestSpec() { + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + } + + private void setupUnauthenticatedRequestSpec() { + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + } + + private String createLoanApprovalRequest() { + return this.loanTransactionHelper.getApproveLoanAsJSON(LOAN_DATE); + } + + private String createLoanApprovalCommand() { + return this.loanTransactionHelper.createLoanOperationURL(APPROVE_COMMAND, loanID); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java index df133d68849..a7490ce99f8 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ClientLoanIntegrationTest.java @@ -3118,6 +3118,94 @@ public void testRBIPaymentStrategy() { } + @Test + public void testLoanPrePaymentWithMultiplePayments() { + final Integer clientID = ClientHelper.createClient(REQUEST_SPEC, RESPONSE_SPEC); + ClientHelper.verifyClientCreatedOnServer(REQUEST_SPEC, RESPONSE_SPEC, clientID); + + // Create a loan product + Integer loanProductId = createLoanProduct(false, NONE); + Integer collateralId = CollateralManagementHelper.createCollateralProduct(REQUEST_SPEC, RESPONSE_SPEC); + Integer clientCollateralId = CollateralManagementHelper.createClientCollateral(REQUEST_SPEC, RESPONSE_SPEC, + String.valueOf(clientID), collateralId); + List collaterals = List.of(collaterals(clientCollateralId, BigDecimal.ONE)); + + // Apply for a loan + final String disbursementDate = "1 May 2023"; + final String approvalDate = "1 April 2023"; + final String submissionDate = "1 March 2023"; + final String interestRate = "7"; + final Integer loanID = applyForLoanApplication(clientID, loanProductId, disbursementDate, submissionDate, interestRate, null, null, + "1000", collaterals); + Assertions.assertNotNull(loanID); + + // Check loan status + HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(REQUEST_SPEC, RESPONSE_SPEC, loanID); + LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); + + // Approve the loan + LOG.info("-----------------------------------APPROVE LOAN-----------------------------------------"); + loanStatusHashMap = LOAN_TRANSACTION_HELPER.approveLoan(approvalDate, loanID); + LoanStatusChecker.verifyLoanIsApproved(loanStatusHashMap); + LoanStatusChecker.verifyLoanIsWaitingForDisbursal(loanStatusHashMap); + + // Disburse the loan + LOG.info("-------------------------------DISBURSE LOAN-------------------------------------------"); + String loanDetails = LOAN_TRANSACTION_HELPER.getLoanDetails(REQUEST_SPEC, RESPONSE_SPEC, loanID); + loanStatusHashMap = LOAN_TRANSACTION_HELPER.disburseLoanWithNetDisbursalAmount(disbursementDate, loanID, + JsonPath.from(loanDetails).get("netDisbursalAmount").toString()); + LoanStatusChecker.verifyLoanIsActive(loanStatusHashMap); + + // Make the first partial repayment + LOG.info("------------------------MAKE FIRST PARTIAL REPAYMENT-----------------------------------"); + Float firstRepaymentAmount = 500.0f; // First partial repayment + String firstRepaymentDate = "1 June 2023"; + LOAN_TRANSACTION_HELPER.makeRepayment(firstRepaymentDate, firstRepaymentAmount, loanID); + + // Verify the prepayment amount after the first partial repayment + LOG.info("------------------------GET PREPAYMENT AMOUNT AFTER FIRST PAYMENT-----------------------"); + HashMap prepayAmount = loanTransactionHelper.getPrepayAmount(REQUEST_SPEC, RESPONSE_SPEC, loanID); + Assertions.assertNotNull(prepayAmount); + + // Extract the principal and interest portions + Float totalPrepayAmount = (Float) prepayAmount.get("amount"); + Float principalAmount = (Float) prepayAmount.get("principalPortion"); + Float interestAmount = (Float) prepayAmount.get("interestPortion"); + + // Expected values after the first partial repayment + Float expectedTotalPrepayAmount = 606.18f; + Float expectedPrincipal = 570.0f; + Float expectedInterest = 36.18f; + + // Validate calculations + validateNumberForEqual(String.valueOf(expectedTotalPrepayAmount), String.valueOf(totalPrepayAmount)); + validateNumberForEqual(String.valueOf(expectedPrincipal), String.valueOf(principalAmount)); + validateNumberForEqual(String.valueOf(expectedInterest), String.valueOf(interestAmount)); + + // Make the second partial repayment + LOG.info("------------------------MAKE SECOND PARTIAL REPAYMENT----------------------------------"); + Float secondRepaymentAmount = 606.18f; + String secondRepaymentDate = "1 July 2023"; + LOAN_TRANSACTION_HELPER.makeRepayment(secondRepaymentDate, secondRepaymentAmount, loanID); + + // Recheck the prepayment amount + LOG.info("------------------------RECHECK PREPAYMENT AMOUNT AFTER FULL REPAYMENT------------------"); + HashMap postPrepayAmount = loanTransactionHelper.getPrepayAmount(REQUEST_SPEC, RESPONSE_SPEC, loanID); + Assertions.assertNotNull(postPrepayAmount); + + // Verify that the principal and interest portions are zero + Float postPrincipalAmount = (Float) postPrepayAmount.get("principalPortion"); + Float postInterestAmount = (Float) postPrepayAmount.get("interestPortion"); + + validateNumberForEqual("0.0", String.valueOf(postPrincipalAmount)); + validateNumberForEqual("0.0", String.valueOf(postInterestAmount)); + + // Check the loan status after repayment + LOG.info("------------------------CHECK LOAN STATUS---------------------------------------------"); + loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(REQUEST_SPEC, RESPONSE_SPEC, loanID); + LoanStatusChecker.verifyLoanAccountIsClosed(loanStatusHashMap); + } + @Test public void testLoanScheduleWithInterestRecalculation_WITH_REST_SAME_AS_REPAYMENT_INTEREST_COMPOUND_NONE_STRATEGY_REDUCE_EMI() { @@ -7217,6 +7305,19 @@ private Integer applyForLoanApplication(final Integer clientID, final Integer lo return LOAN_TRANSACTION_HELPER.getLoanId(loanApplicationJSON); } + private Integer applyForLoanApplication(final Integer clientID, final Integer loanProductID, String disbursementDate, + String submissionDate, String interestRate, List charges, final String savingsId, String principal, + List collaterals) { + LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); + final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principal).withLoanTermFrequency("2") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("2").withRepaymentEveryAfter("1") + .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod(interestRate).withAmortizationTypeAsEqualInstallments() + .withInterestTypeAsDecliningBalance().withInterestCalculationPeriodTypeSameAsRepaymentPeriod() + .withExpectedDisbursementDate(disbursementDate).withSubmittedOnDate(submissionDate).withCollaterals(collaterals) + .withCharges(charges).build(clientID.toString(), loanProductID.toString(), savingsId); + return LOAN_TRANSACTION_HELPER.getLoanId(loanApplicationJSON); + } + private Integer applyForLoanApplicationWithExternalId(RequestSpecification requestSpecification, ResponseSpecification responseSpecification, final Integer clientID, final Integer loanProductID, String principal, final String externalId) { diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java index 6aee674af0d..7d8b137e44a 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/ExternalBusinessEventTest.java @@ -183,7 +183,7 @@ public void verifyInterestRefundPostBusinessEventCreatedForMerchantIssuedRefundW Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId()); verifyBusinessEvents(new LoanTransactionBusinessEvent("LoanTransactionInterestRefundPostBusinessEvent", "22 January 2021", 5.75, - 994.25, 5.75, 0.0, 0.0, 0.0)); + 0.0, 5.75, 0.0, 0.0, 0.0)); }); enableLoanInterestRefundPstBusinessEvent(false); } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAuditingIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAuditingIntegrationTest.java index 1ff92e392b2..5498d72075d 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAuditingIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanAuditingIntegrationTest.java @@ -35,7 +35,6 @@ import java.time.temporal.ChronoUnit; import java.util.Collections; import java.util.HashMap; -import java.util.List; import java.util.Map; import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.integrationtests.common.ClientHelper; @@ -93,12 +92,12 @@ public void checkAuditDates() throws InterruptedException { final Account expenseAccount = this.accountHelper.createExpenseAccount(); final Account overpaymentAccount = this.accountHelper.createLiabilityAccount(); - final Integer loanProductID = createLoanProduct("0", "0", LoanProductTestBuilder.DEFAULT_STRATEGY, "2", assetAccount, incomeAccount, - expenseAccount, overpaymentAccount); + final Integer loanProductID = this.loanTransactionHelper.createLoanProduct("0", "0", LoanProductTestBuilder.DEFAULT_STRATEGY, "2", + assetAccount, incomeAccount, expenseAccount, overpaymentAccount); OffsetDateTime now = Utils.getAuditDateTimeToCompare(); - final Integer loanID = applyForLoanApplicationWithPaymentStrategyAndPastMonth(clientID, loanProductID, Collections.emptyList(), - null, "10000", LoanApplicationTestBuilder.DEFAULT_STRATEGY, "10 July 2022", "11 July 2022"); + final Integer loanID = this.loanTransactionHelper.applyForLoanApplicationWithPaymentStrategyAndPastMonth(clientID, loanProductID, + Collections.emptyList(), null, "10000", LoanApplicationTestBuilder.DEFAULT_STRATEGY, "10 July 2022", "11 July 2022"); Assertions.assertNotNull(loanID); HashMap loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); LoanStatusChecker.verifyLoanIsPending(loanStatusHashMap); @@ -141,45 +140,4 @@ public void checkAuditDates() throws InterruptedException { assertEquals(userId, auditFieldsResponse.get(LAST_MODIFIED_BY)); assertTrue(DateUtils.isEqual(now2, lastModifiedDate, ChronoUnit.MINUTES)); } - - private Integer applyForLoanApplicationWithPaymentStrategyAndPastMonth(final Integer clientID, final Integer loanProductID, - List charges, final String savingsId, String principal, final String repaymentStrategy, final String submittedOnDate, - final String disbursementDate) { - LOG.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); - - final String loanApplicationJSON = new LoanApplicationTestBuilder() // - .withPrincipal(principal) // - .withLoanTermFrequency("6") // - .withLoanTermFrequencyAsMonths() // - .withNumberOfRepayments("6") // - .withRepaymentEveryAfter("1") // - .withRepaymentFrequencyTypeAsMonths() // - .withInterestRatePerPeriod("2") // - .withAmortizationTypeAsEqualInstallments() // - .withInterestTypeAsFlatBalance() // - .withInterestCalculationPeriodTypeSameAsRepaymentPeriod() // - .withExpectedDisbursementDate(disbursementDate) // - .withSubmittedOnDate(submittedOnDate) // - .withRepaymentStrategy(repaymentStrategy) // - .withCharges(charges).build(clientID.toString(), loanProductID.toString(), savingsId); - return this.loanTransactionHelper.getLoanId(loanApplicationJSON); - } - - private Integer createLoanProduct(final String inMultiplesOf, final String digitsAfterDecimal, final String repaymentStrategy, - final String accountingRule, final Account... accounts) { - LOG.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); - final String loanProductJSON = new LoanProductTestBuilder() // - .withPrincipal("10000000.00") // - .withNumberOfRepayments("24") // - .withRepaymentAfterEvery("1") // - .withRepaymentTypeAsMonth() // - .withinterestRatePerPeriod("2") // - .withInterestRateFrequencyTypeAsMonths() // - .withRepaymentStrategy(repaymentStrategy) // - .withAmortizationTypeAsEqualPrincipalPayment() // - .withInterestTypeAsDecliningBalance() // - .currencyDetails(digitsAfterDecimal, inMultiplesOf).withAccounting(accountingRule, accounts).build(null); - return this.loanTransactionHelper.getLoanProductId(loanProductJSON); - } - } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java index 858f334ef7d..2cf9cc0d776 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanInterestRefundTest.java @@ -18,6 +18,8 @@ */ package org.apache.fineract.integrationtests; +import static org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum.REPLAYED; + import io.restassured.builder.RequestSpecBuilder; import io.restassured.builder.ResponseSpecBuilder; import io.restassured.http.ContentType; @@ -28,6 +30,7 @@ import java.util.concurrent.atomic.AtomicReference; import lombok.extern.slf4j.Slf4j; import org.apache.fineract.client.models.GetLoansLoanIdResponse; +import org.apache.fineract.client.models.GetLoansLoanIdTransactionsTransactionIdResponse; import org.apache.fineract.client.models.PostClientsResponse; import org.apache.fineract.client.models.PostLoanProductsResponse; import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse; @@ -142,12 +145,39 @@ public void verifyInterestRefundCreatedForPayoutRefund() { Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId()); logTransactions(loanId); - verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"), - transaction(1000.0, "Payout Refund", "22 January 2021"), transaction(5.75, "Accrual", "22 January 2021"), - transaction(5.75, "Interest Refund", "22 January 2021")); + verifyTransactions(loanId, // + transaction(1000.0, "Disbursement", "01 January 2021"), // + transaction(1000.0, "Payout Refund", "22 January 2021"), // + transaction(5.75, "Interest Refund", "22 January 2021"), // + transaction(5.75, "Accrual", "22 January 2021")); // + + checkTransactionWasNotReverseReplayed(postLoansLoanIdTransactionsResponse.getLoanId(), + postLoansLoanIdTransactionsResponse.getResourceId()); + checkTransactionWasNotReverseReplayed(postLoansLoanIdTransactionsResponse.getLoanId(), + postLoansLoanIdTransactionsResponse.getSubResourceId()); + + verifyTRJournalEntries(postLoansLoanIdTransactionsResponse.getResourceId(), journalEntry(1000, fundSource, "DEBIT"), // + journalEntry(5.75, interestReceivableAccount, "CREDIT"), // + journalEntry(994.25, loansReceivableAccount, "CREDIT")); + + verifyTRJournalEntries(postLoansLoanIdTransactionsResponse.getSubResourceId(), + journalEntry(5.75, interestIncomeAccount, "DEBIT"), // + journalEntry(5.75, loansReceivableAccount, "CREDIT")); // }); } + private void checkTransactionWasNotReverseReplayed(Long loanId, Long transactionId) { + GetLoansLoanIdTransactionsTransactionIdResponse loanTransactionDetails = loanTransactionHelper.getLoanTransactionDetails(loanId, + transactionId); + if (loanTransactionDetails.getTransactionRelations() != null) { + loanTransactionDetails.getTransactionRelations().forEach(transactionRelation -> { + if (REPLAYED.name().equals(transactionRelation.getRelationType())) { + Assertions.fail("Transaction was replayed!"); + } + }); + } + } + @Test public void verifyInterestRefundCreatedForMerchantIssuedRefund() { AtomicReference loanIdRef = new AtomicReference<>(); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java index 58f04ad234e..128573f6666 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java @@ -91,6 +91,7 @@ import org.apache.fineract.integrationtests.common.CommonConstants; import org.apache.fineract.integrationtests.common.PaymentTypeHelper; import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.accounting.Account; import org.apache.fineract.portfolio.delinquency.domain.DelinquencyAction; import org.apache.poi.hssf.usermodel.HSSFWorkbook; import org.apache.poi.ss.usermodel.Workbook; @@ -1133,7 +1134,7 @@ private String getDisburseLoanWithRepaymentRescheduleAsJSON(final String actualD return new Gson().toJson(map); } - private String getApproveLoanAsJSON(final String approvalDate) { + public String getApproveLoanAsJSON(final String approvalDate) { return getApproveLoanAsJSON(approvalDate, null, null, null); } @@ -1397,7 +1398,7 @@ public String getLoanCalculationBodyAsJSON(final String productID) { return new Gson().toJson(map); } - private String createLoanOperationURL(final String command, final Integer loanID) { + public String createLoanOperationURL(final String command, final Integer loanID) { return "/fineract-provider/api/v1/loans/" + loanID + "?command=" + command + "&" + Utils.TENANT_IDENTIFIER; } @@ -2045,4 +2046,29 @@ public PostLoansResponse calculateRepaymentScheduleForApplyLoan(PostLoansRequest return ok(fineract().loans.calculateLoanScheduleOrSubmitLoanApplication(request, command)); } + public Integer createLoanProduct(final String inMultiplesOf, final String digitsAfterDecimal, final String repaymentStrategy, + final String accountingRule, final Account... accounts) { + log.info("------------------------------CREATING NEW LOAN PRODUCT ---------------------------------------"); + final String loanProductJSON = new LoanProductTestBuilder().withPrincipal("10000000.00").withNumberOfRepayments("24") + .withRepaymentAfterEvery("1").withRepaymentTypeAsMonth().withinterestRatePerPeriod("2") + .withInterestRateFrequencyTypeAsMonths().withRepaymentStrategy(repaymentStrategy) + .withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsDecliningBalance() + .currencyDetails(digitsAfterDecimal, inMultiplesOf).withAccounting(accountingRule, accounts).build(null); + return getLoanProductId(loanProductJSON); + } + + public Integer applyForLoanApplicationWithPaymentStrategyAndPastMonth(final Integer clientID, final Integer loanProductID, + List charges, final String savingsId, String principal, final String repaymentStrategy, final String submittedOnDate, + final String disbursementDate) { + log.info("--------------------------------APPLYING FOR LOAN APPLICATION--------------------------------"); + + final String loanApplicationJSON = new LoanApplicationTestBuilder().withPrincipal(principal).withLoanTermFrequency("6") + .withLoanTermFrequencyAsMonths().withNumberOfRepayments("6").withRepaymentEveryAfter("1") + .withRepaymentFrequencyTypeAsMonths().withInterestRatePerPeriod("2").withAmortizationTypeAsEqualInstallments() + .withInterestTypeAsFlatBalance().withInterestCalculationPeriodTypeSameAsRepaymentPeriod() + .withExpectedDisbursementDate(disbursementDate).withSubmittedOnDate(submittedOnDate) + .withRepaymentStrategy(repaymentStrategy).withCharges(charges) + .build(clientID.toString(), loanProductID.toString(), savingsId); + return getLoanId(loanApplicationJSON); + } } diff --git a/kubernetes/fineractmysql-deployment.yml b/kubernetes/fineractmysql-deployment.yml index 582e3d2002a..a0b4b35f724 100644 --- a/kubernetes/fineractmysql-deployment.yml +++ b/kubernetes/fineractmysql-deployment.yml @@ -86,7 +86,7 @@ spec: tier: fineractmysql spec: containers: - - image: mariadb:11.2 + - image: mariadb:11.4 name: mysql resources: requests: