Skip to content

Commit

Permalink
FINERACT-2081: EmbeddableProgressiveLoanScheduleGenerator add totalOu…
Browse files Browse the repository at this point in the history
…tstandingLoanBalance field
  • Loading branch information
magyari-adam authored and adamsaghy committed Feb 7, 2025
1 parent 486ecb1 commit 9fa64ec
Show file tree
Hide file tree
Showing 7 changed files with 130 additions and 4 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/build-embeddable-progressive-loan-jar.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -75,9 +75,9 @@ static void printPlan(final LoanSchedulePlan plan) throws InterruptedException {
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());
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
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;
Expand All @@ -44,6 +45,8 @@ public class LoanSchedulePlan {
public static LoanSchedulePlan from(LoanScheduleModel model) {
List<LoanSchedulePlanPeriod> periods = new ArrayList<>();

BigDecimal remainingTotalOutstanding = model.getTotalRepaymentExpected();
AtomicReference<BigDecimal> remainingTotalOutstandingRef = new AtomicReference<>(remainingTotalOutstanding);
model.getPeriods().forEach(periodModel -> {
LoanSchedulePlanPeriod periodPlan = null;
if (periodModel instanceof LoanScheduleModelDisbursementPeriod disbursementPeriod) {
Expand All @@ -52,13 +55,17 @@ public static LoanSchedulePlan from(LoanScheduleModel model) {
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(), //
Expand All @@ -67,7 +74,8 @@ public static LoanSchedulePlan from(LoanScheduleModel model) {
repaymentPeriod.getFeeChargesDue().getAmount(), //
repaymentPeriod.getPenaltyChargesDue().getAmount(), //
repaymentPeriod.getTotalDue().getAmount(), //
repaymentPeriod.getOutstandingLoanBalance().getAmount());//
repaymentPeriod.getOutstandingLoanBalance().getAmount(), //
remainingTotalOutstandingRef.get());//
}
if (periodPlan != null) {
periods.add(periodPlan);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down

0 comments on commit 9fa64ec

Please sign in to comment.