diff --git a/.github/workflows/build-embeddable-progressive-loan-jar.yml b/.github/workflows/build-embeddable-progressive-loan-jar.yml index 1b16e4606..cfc6605b3 100644 --- a/.github/workflows/build-embeddable-progressive-loan-jar.yml +++ b/.github/workflows/build-embeddable-progressive-loan-jar.yml @@ -27,6 +27,8 @@ jobs: run: | EMBEDDABLE_JAR_FILE=(`ls fineract-progressive-loan-embeddable-schedule-generator/build/libs/*-all.jar | head -n 1`) echo "EMBEDDABLE_JAR_FILE=$EMBEDDABLE_JAR_FILE" >> $GITHUB_ENV + - name: Run unit tests + run: ./gradlew --no-daemon --console=plain :fineract-progressive-loan-embeddable-schedule-generator:test - name: Build Sample Application run: | mkdir sample-app diff --git a/fineract-progressive-loan-embeddable-schedule-generator/dependencies.gradle b/fineract-progressive-loan-embeddable-schedule-generator/dependencies.gradle index 066464554..c808eace4 100644 --- a/fineract-progressive-loan-embeddable-schedule-generator/dependencies.gradle +++ b/fineract-progressive-loan-embeddable-schedule-generator/dependencies.gradle @@ -20,6 +20,7 @@ dependencies { implementation(project(path: ':fineract-progressive-loan')) implementation(project(path: ':fineract-loan')) + testImplementation(project(path: ':fineract-core')) annotationProcessor 'org.projectlombok:lombok' annotationProcessor 'org.mapstruct:mapstruct-processor' diff --git a/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java b/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java index eef822b2c..1cca7c5a9 100644 --- a/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java +++ b/fineract-progressive-loan-embeddable-schedule-generator/misc/Main.java @@ -75,9 +75,9 @@ public class Main { if (period instanceof LoanSchedulePlanDisbursementPeriod dp) { System.out.printf(" Disbursement - Date: %s, Amount: %s%n", dp.periodDueDate(), dp.getPrincipalAmount()); } if (period instanceof LoanSchedulePlanDownPaymentPeriod rp) { - System.out.printf(" Down payment Period: #%d, Due Date: %s, Balance: %s, Principal: %s, Total: %s%n", rp.periodNumber(), rp.periodDueDate(), rp.getOutstandingLoanBalance(), rp.getPrincipalAmount(), rp.getTotalDueAmount()); + System.out.printf(" Down payment Period: #%d, Due Date: %s, Balance: %s, Principal: %s, Total: %s, Total Outstanding Balance: %s%n", rp.periodNumber(), rp.periodDueDate(), rp.getOutstandingLoanBalance(), rp.getPrincipalAmount(), rp.getTotalDueAmount(), rp.getTotalOutstandingLoanBalance()); } if (period instanceof LoanSchedulePlanRepaymentPeriod rp) { - System.out.printf(" Repayment Period: #%d, Due Date: %s, Balance: %s, Principal: %s, Interest: %s, Total: %s%n", rp.periodNumber(), rp.periodDueDate(), rp.getOutstandingLoanBalance(), rp.getPrincipalAmount(), rp.getInterestAmount(), rp.getTotalDueAmount()); + System.out.printf(" Repayment Period: #%d, Due Date: %s, Balance: %s, Principal: %s, Interest: %s, Total: %s, Total Outstanding Balance: %s%n", rp.periodNumber(), rp.periodDueDate(), rp.getOutstandingLoanBalance(), rp.getPrincipalAmount(), rp.getInterestAmount(), rp.getTotalDueAmount(), rp.getTotalOutstandingLoanBalance()); } } } diff --git a/fineract-progressive-loan-embeddable-schedule-generator/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGeneratorTest.java b/fineract-progressive-loan-embeddable-schedule-generator/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGeneratorTest.java new file mode 100644 index 000000000..69a9803e3 --- /dev/null +++ b/fineract-progressive-loan-embeddable-schedule-generator/src/test/java/org/apache/fineract/portfolio/loanaccount/loanschedule/domain/EmbeddableProgressiveLoanScheduleGeneratorTest.java @@ -0,0 +1,113 @@ +/** + * 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 java.math.BigDecimal; +import java.math.MathContext; +import java.math.RoundingMode; +import java.time.LocalDate; +import org.apache.fineract.organisation.monetary.data.CurrencyData; +import org.apache.fineract.portfolio.common.domain.DaysInMonthType; +import org.apache.fineract.portfolio.common.domain.DaysInYearType; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlan; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanDisbursementPeriod; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanPeriod; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePlanRepaymentPeriod; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; + +class EmbeddableProgressiveLoanScheduleGeneratorTest { + + @Test + void testGenerate() { + MathContext mc = new MathContext(12, RoundingMode.HALF_UP); + EmbeddableProgressiveLoanScheduleGenerator calculator = new EmbeddableProgressiveLoanScheduleGenerator(); + + final CurrencyData currency = new CurrencyData("usd", "US Dollar", 2, null, "usd", "$"); + final LocalDate startDate = LocalDate.of(2024, 1, 1); + final LocalDate disbursementDate = LocalDate.of(2024, 1, 1); + final BigDecimal disbursedAmount = BigDecimal.valueOf(100); + + final int noRepayments = 6; + final int repaymentFrequency = 1; + final String repaymentFrequencyType = "MONTHS"; + final BigDecimal downPaymentPercentage = BigDecimal.ZERO; + final boolean isDownPaymentEnabled = BigDecimal.ZERO.compareTo(downPaymentPercentage) != 0; + final BigDecimal annualNominalInterestRate = BigDecimal.valueOf(7.0); + final DaysInMonthType daysInMonthType = DaysInMonthType.DAYS_30; + final DaysInYearType daysInYearType = DaysInYearType.DAYS_360; + final Integer installmentAmountInMultiplesOf = null; + final Integer fixedLength = null; + + var config = new LoanRepaymentScheduleModelData(startDate, currency, disbursedAmount, disbursementDate, noRepayments, + repaymentFrequency, repaymentFrequencyType, annualNominalInterestRate, isDownPaymentEnabled, daysInMonthType, + daysInYearType, downPaymentPercentage, installmentAmountInMultiplesOf, fixedLength); + + final LoanSchedulePlan plan = calculator.generate(mc, config); + + Assertions.assertEquals(182, plan.getLoanTermInDays()); + Assertions.assertEquals(100.00, toDouble(plan.getTotalDisbursedAmount())); + Assertions.assertEquals(2.05, toDouble(plan.getTotalInterestAmount())); + Assertions.assertEquals(102.05, toDouble(plan.getTotalRepaymentAmount())); + + Assertions.assertEquals(7, plan.getPeriods().size()); + checkPeriod(plan.getPeriods().get(0), LocalDate.of(2024, 1, 1), LocalDate.of(2024, 1, 1), 100.0, 100.0); + checkPeriod(plan.getPeriods().get(1), 1, LocalDate.of(2024, 1, 1), LocalDate.of(2024, 2, 1), 16.43, 0.58, 0.0, 0.0, 17.01, 83.57, + 85.04); + checkPeriod(plan.getPeriods().get(2), 2, LocalDate.of(2024, 2, 1), LocalDate.of(2024, 3, 1), 16.52, 0.49, 0.0, 0.0, 17.01, 67.05, + 68.03); + checkPeriod(plan.getPeriods().get(3), 3, LocalDate.of(2024, 3, 1), LocalDate.of(2024, 4, 1), 16.62, 0.39, 0.0, 0.0, 17.01, 50.43, + 51.02); + checkPeriod(plan.getPeriods().get(4), 4, LocalDate.of(2024, 4, 1), LocalDate.of(2024, 5, 1), 16.72, 0.29, 0.0, 0.0, 17.01, 33.71, + 34.01); + checkPeriod(plan.getPeriods().get(5), 5, LocalDate.of(2024, 5, 1), LocalDate.of(2024, 6, 1), 16.81, 0.20, 0.0, 0.0, 17.01, 16.90, + 17.00); + checkPeriod(plan.getPeriods().get(6), 6, LocalDate.of(2024, 6, 1), LocalDate.of(2024, 7, 1), 16.90, 0.10, 0.0, 0.0, 17.00, 0.0, + 0.0); + } + + private static void checkPeriod(LoanSchedulePlanPeriod period, LocalDate fromDate, LocalDate dueDate, double principal, + double outstandingBalance) { + checkPeriod(period, null, fromDate, dueDate, principal, 0.0, 0.0, 0.0, 0.0, outstandingBalance, 0.0); + } + + private static void checkPeriod(LoanSchedulePlanPeriod period, Integer periodNumber, LocalDate fromDate, LocalDate dueDate, + double principal, double interest, double fee, double penalty, double totalDue, double outstandingBalance, + double totalOutstandingBalance) { + Assertions.assertEquals(periodNumber, period.periodNumber()); + Assertions.assertEquals(fromDate, period.periodFromDate()); + Assertions.assertEquals(dueDate, period.periodDueDate()); + if (period instanceof LoanSchedulePlanDisbursementPeriod disbursementPeriod) { + Assertions.assertEquals(principal, toDouble(disbursementPeriod.getPrincipalAmount())); + Assertions.assertEquals(outstandingBalance, toDouble(disbursementPeriod.getOutstandingLoanBalance())); + } else if (period instanceof LoanSchedulePlanRepaymentPeriod repaymentPeriod) { + Assertions.assertEquals(principal, toDouble(repaymentPeriod.getPrincipalAmount())); + Assertions.assertEquals(interest, toDouble(repaymentPeriod.getInterestAmount())); + Assertions.assertEquals(fee, toDouble(repaymentPeriod.getFeeAmount())); + Assertions.assertEquals(penalty, toDouble(repaymentPeriod.getPenaltyAmount())); + Assertions.assertEquals(totalDue, toDouble(repaymentPeriod.getTotalDueAmount())); + Assertions.assertEquals(outstandingBalance, toDouble(repaymentPeriod.getOutstandingLoanBalance())); + Assertions.assertEquals(totalOutstandingBalance, toDouble(repaymentPeriod.getTotalOutstandingLoanBalance())); + } + } + + private static double toDouble(final BigDecimal value) { + return value == null ? 0.0 : value.doubleValue(); + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePlan.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePlan.java index cdffe2c7d..fbd32fef2 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePlan.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePlan.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.loanschedule.data; import java.math.BigDecimal; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.atomic.AtomicReference; import lombok.Data; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; @@ -44,6 +45,8 @@ public class LoanSchedulePlan { public static LoanSchedulePlan from(LoanScheduleModel model) { List periods = new ArrayList<>(); + BigDecimal remainingTotalOutstanding = model.getTotalRepaymentExpected(); + AtomicReference remainingTotalOutstandingRef = new AtomicReference<>(remainingTotalOutstanding); model.getPeriods().forEach(periodModel -> { LoanSchedulePlanPeriod periodPlan = null; if (periodModel instanceof LoanScheduleModelDisbursementPeriod disbursementPeriod) { @@ -52,13 +55,17 @@ public class LoanSchedulePlan { disbursementPeriod.getPrincipalDisbursed().getAmount(), // disbursementPeriod.getPrincipalDisbursed().getAmount());// } else if (periodModel instanceof LoanScheduleModelDownPaymentPeriod downPaymentPeriod) { + remainingTotalOutstandingRef + .set(remainingTotalOutstandingRef.get().subtract(downPaymentPeriod.getPrincipalDue().getAmount())); periodPlan = new LoanSchedulePlanDownPaymentPeriod(downPaymentPeriod.getPeriodNumber(), // downPaymentPeriod.getPeriodDate(), // downPaymentPeriod.getPeriodDate(), // downPaymentPeriod.getPrincipalDue().getAmount(), // downPaymentPeriod.getPrincipalDue().getAmount(), // - downPaymentPeriod.getOutstandingLoanBalance().getAmount());// + downPaymentPeriod.getOutstandingLoanBalance().getAmount(), // + remainingTotalOutstandingRef.get());// } else if (periodModel instanceof LoanScheduleModelRepaymentPeriod repaymentPeriod) { + remainingTotalOutstandingRef.set(remainingTotalOutstandingRef.get().subtract(repaymentPeriod.getTotalDue().getAmount())); periodPlan = new LoanSchedulePlanRepaymentPeriod(repaymentPeriod.getPeriodNumber(), // repaymentPeriod.getFromDate(), // repaymentPeriod.getDueDate(), // @@ -67,7 +74,8 @@ public class LoanSchedulePlan { repaymentPeriod.getFeeChargesDue().getAmount(), // repaymentPeriod.getPenaltyChargesDue().getAmount(), // repaymentPeriod.getTotalDue().getAmount(), // - repaymentPeriod.getOutstandingLoanBalance().getAmount());// + repaymentPeriod.getOutstandingLoanBalance().getAmount(), // + remainingTotalOutstandingRef.get());// } if (periodPlan != null) { periods.add(periodPlan); diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePlanDownPaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePlanDownPaymentPeriod.java index 2d7929a43..15e46fb46 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePlanDownPaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePlanDownPaymentPeriod.java @@ -31,6 +31,7 @@ public final class LoanSchedulePlanDownPaymentPeriod implements LoanSchedulePlan private final BigDecimal principalAmount; private final BigDecimal totalDueAmount; private final BigDecimal outstandingLoanBalance; + private final BigDecimal totalOutstandingLoanBalance; @Override public Integer periodNumber() { diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePlanRepaymentPeriod.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePlanRepaymentPeriod.java index 03a11d250..3f40ebf53 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePlanRepaymentPeriod.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/data/LoanSchedulePlanRepaymentPeriod.java @@ -34,6 +34,7 @@ public final class LoanSchedulePlanRepaymentPeriod implements LoanSchedulePlanPe private final BigDecimal penaltyAmount; private final BigDecimal totalDueAmount; private final BigDecimal outstandingLoanBalance; + private final BigDecimal totalOutstandingLoanBalance; @Override public Integer periodNumber() {