diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/arrears/LoanArrearsData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/arrears/LoanArrearsData.java new file mode 100644 index 000000000..879174204 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/arrears/LoanArrearsData.java @@ -0,0 +1,37 @@ +/** + * 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.arrears; + +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.Data; + +@Data +public class LoanArrearsData { + + private BigDecimal principalOverdue; + private BigDecimal interestOverdue; + private BigDecimal feeOverdue; + private BigDecimal penaltyOverdue; + private BigDecimal totalOverdue; + + private LocalDate overDueSince; + + private boolean isOverdue; +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanArrearsAgingService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanArrearsAgingService.java index a401ceca1..0625fdaf5 100755 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanArrearsAgingService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanArrearsAgingService.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.service; import java.util.List; import java.util.Map; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.arrears.LoanArrearsData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; public interface LoanArrearsAgingService { @@ -35,4 +36,6 @@ public interface LoanArrearsAgingService { boolean isInsertStatement); void updateScheduleWithPaidDetail(Map> scheduleDate, List> loanSummary); + + LoanArrearsData calculateArrearsForLoan(Loan loan); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanArrearsAgingServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanArrearsAgingServiceImpl.java index 2526b5ba7..7e126b3bb 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanArrearsAgingServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanArrearsAgingServiceImpl.java @@ -55,6 +55,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.arrears.LoanArrearsData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; import org.springframework.dao.DataAccessException; import org.springframework.jdbc.core.JdbcTemplate; @@ -138,8 +139,8 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService { } } - private String constructUpdateStatement(final Loan loan, boolean isInsertStatement) { - String updateSql = null; + @Override + public LoanArrearsData calculateArrearsForLoan(Loan loan) { List installments = loan.getRepaymentScheduleInstallments(); BigDecimal principalOverdue = BigDecimal.ZERO; BigDecimal interestOverdue = BigDecimal.ZERO; @@ -158,9 +159,33 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService { } } } - BigDecimal totalOverDue = principalOverdue.add(interestOverdue).add(feeOverdue).add(penaltyOverdue); - if (totalOverDue.compareTo(BigDecimal.ZERO) > 0) { + boolean isOverdue = totalOverDue.compareTo(BigDecimal.ZERO) > 0; + if (!isOverdue) { + overDueSince = null; + } + + LoanArrearsData result = new LoanArrearsData(); + result.setPrincipalOverdue(principalOverdue); + result.setInterestOverdue(interestOverdue); + result.setFeeOverdue(feeOverdue); + result.setPenaltyOverdue(penaltyOverdue); + result.setTotalOverdue(totalOverDue); + result.setOverDueSince(overDueSince); + result.setOverdue(isOverdue); + return result; + } + + private String constructUpdateStatement(final Loan loan, boolean isInsertStatement) { + String updateSql = null; + LoanArrearsData arrearsData = calculateArrearsForLoan(loan); + BigDecimal principalOverdue = arrearsData.getPrincipalOverdue(); + BigDecimal interestOverdue = arrearsData.getInterestOverdue(); + BigDecimal feeOverdue = arrearsData.getFeeOverdue(); + BigDecimal penaltyOverdue = arrearsData.getPenaltyOverdue(); + LocalDate overDueSince = arrearsData.getOverDueSince(); + + if (arrearsData.isOverdue()) { if (isInsertStatement) { updateSql = constructInsertStatement(loan.getId(), principalOverdue, interestOverdue, feeOverdue, penaltyOverdue, overDueSince); @@ -201,6 +226,7 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService { BigDecimal penaltyOverdue = BigDecimal.ZERO; LocalDate overDueSince = DateUtils.getBusinessLocalDate(); + // TODO: this needs to be refactored to use the calculateArrearsForLoan method. for (LoanSchedulePeriodData loanSchedulePeriodData : entry.getValue()) { if (!loanSchedulePeriodData.getComplete()) { principalOverdue = principalOverdue diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanPointInTimeData.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanPointInTimeData.java index 08fd930df..f615edbe1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanPointInTimeData.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanPointInTimeData.java @@ -23,6 +23,7 @@ import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.organisation.monetary.mapper.CurrencyMapper; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.arrears.LoanArrearsData; import org.mapstruct.Mapping; @Data @@ -51,6 +52,9 @@ public class LoanPointInTimeData { private Long loanProductId; private String loanProductName; + // Arrears data + private LoanArrearsData arrears; + @org.mapstruct.Mapper(config = MapstructMapperConfig.class, uses = { LoanStatusEnumData.Mapper.class, CurrencyMapper.class, LoanPrincipalData.Mapper.class, LoanInterestData.Mapper.class, LoanFeeData.Mapper.class, LoanPenaltyData.Mapper.class, LoanTotalAmountData.Mapper.class }) @@ -70,6 +74,7 @@ public class LoanPointInTimeData { @Mapping(source = "summary", target = "total") @Mapping(source = "loanProduct.id", target = "loanProductId") @Mapping(source = "loanProduct.name", target = "loanProductName") + @Mapping(target = "arrears", ignore = true) LoanPointInTimeData map(Loan source); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanPointInTimeServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanPointInTimeServiceImpl.java index 345c8e1e7..278c2a9a7 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanPointInTimeServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanPointInTimeServiceImpl.java @@ -35,6 +35,7 @@ import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil; import org.apache.fineract.portfolio.loanaccount.data.LoanPointInTimeData; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.arrears.LoanArrearsData; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.interceptor.TransactionInterceptor; @@ -49,6 +50,7 @@ public class LoanPointInTimeServiceImpl implements LoanPointInTimeService { private final LoanAssembler loanAssembler; private final LoanPointInTimeData.Mapper dataMapper; private final EntityManager entityManager; + private final LoanArrearsAgingService arrearsAgingService; @Override public LoanPointInTimeData retrieveAt(Long loanId, LocalDate date) { @@ -69,7 +71,11 @@ public class LoanPointInTimeServiceImpl implements LoanPointInTimeService { ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null, null); loanScheduleService.recalculateSchedule(loan, scheduleGeneratorDTO); - return dataMapper.map(loan); + LoanArrearsData arrearsData = arrearsAgingService.calculateArrearsForLoan(loan); + + LoanPointInTimeData result = dataMapper.map(loan); + result.setArrears(arrearsData); + return result; } finally { entityManager.clear(); TransactionInterceptor.currentTransactionStatus().setRollbackOnly(); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java index 46ce4b44b..dfe969ca4 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java @@ -700,6 +700,15 @@ public abstract class BaseLoanIntegrationTest extends IntegrationTest { } } + protected void verifyArreals(LoanPointInTimeData pointInTimeData, boolean isOverDue, String overdueSince) { + assertThat(Objects.requireNonNull(pointInTimeData.getArrears()).getOverdue()).isEqualTo(isOverDue); + if (isOverDue) { + assertThat(Objects.requireNonNull(pointInTimeData.getArrears().getOverDueSince()).toString()).isEqualTo(overdueSince); + } else { + assertThat(pointInTimeData.getArrears().getOverDueSince()).isNull(); + } + } + protected void placeHardLockOnLoan(Long loanId) { loanAccountLockHelper.placeSoftLockOnLoanAccount(loanId.intValue(), "LOAN_COB_CHUNK_PROCESSING"); } diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/pointintime/LoanPointInTimeTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/pointintime/LoanPointInTimeTest.java index 71d42240f..969a87032 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/pointintime/LoanPointInTimeTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/loan/pointintime/LoanPointInTimeTest.java @@ -656,4 +656,102 @@ public class LoanPointInTimeTest extends BaseLoanIntegrationTest { ); }); } + + @Test + public void test_LoanPointInTimeDataWorks_ForArrealDataCalculation() { + AtomicReference aLoanId = new AtomicReference<>(); + + runAt("01 January 2023", () -> { + // Create Client + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + + int numberOfRepayments = 3; + int repaymentEvery = 1; + + // Create charges + double charge1Amount = 1.0; + double charge2Amount = 1.5; + Long charge1Id = createDisbursementPercentageCharge(charge1Amount); + Long charge2Id = createDisbursementPercentageCharge(charge2Amount); + + // Create Loan Product + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct() // + .numberOfRepayments(numberOfRepayments) // + .repaymentEvery(repaymentEvery) // + .installmentAmountInMultiplesOf(null) // + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue()) // + .interestType(InterestType.DECLINING_BALANCE)// + .interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)// + .interestRecalculationCompoundingMethod(InterestRecalculationCompoundingMethod.NONE)// + .rescheduleStrategyMethod(RescheduleStrategyMethod.ADJUST_LAST_UNPAID_PERIOD)// + .isInterestRecalculationEnabled(true)// + .recalculationRestFrequencyInterval(1)// + .recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY)// + .rescheduleStrategyMethod(RescheduleStrategyMethod.REDUCE_EMI_AMOUNT)// + .allowPartialPeriodInterestCalcualtion(false)// + .disallowExpectedDisbursements(false)// + .allowApprovedDisbursedAmountsOverApplied(false)// + .overAppliedNumber(null)// + .overAppliedCalculationType(null)// + .multiDisburseLoan(null)// + .charges(List.of(new LoanProductChargeData().id(charge1Id), new LoanProductChargeData().id(charge2Id)));// + + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + Long loanProductId = loanProductResponse.getResourceId(); + + // Apply and Approve Loan + double amount = 5000.0; + + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductId, "01 January 2023", amount, numberOfRepayments)// + .repaymentEvery(repaymentEvery)// + .loanTermFrequency(numberOfRepayments)// + .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)// + .loanTermFrequencyType(RepaymentFrequencyType.MONTHS)// + .interestType(InterestType.DECLINING_BALANCE)// + .interestCalculationPeriodType(InterestCalculationPeriodType.DAILY)// + .charges(List.of(// + new PostLoansRequestChargeData().chargeId(charge1Id).amount(BigDecimal.valueOf(charge1Amount)), // + new PostLoansRequestChargeData().chargeId(charge2Id).amount(BigDecimal.valueOf(charge2Amount))// + ));// + + PostLoansResponse postLoansResponse = loanTransactionHelper.applyLoan(applicationRequest); + + PostLoansLoanIdResponse approvedLoanResult = loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(), + approveLoanRequest(amount, "01 January 2023")); + + aLoanId.getAndSet(approvedLoanResult.getLoanId()); + Long loanId = aLoanId.get(); + + // disburse Loan + disburseLoan(loanId, BigDecimal.valueOf(5000.0), "01 January 2023"); + + // verify transactions + verifyTransactions(loanId, // + transaction(5000.0, "Disbursement", "01 January 2023"), // + transaction(125.0, "Repayment (at time of disbursement)", "01 January 2023") // + ); + }); + + runAt("05 February 2023", () -> { + Long loanId = aLoanId.get(); + + LoanPointInTimeData pointInTimeData = getPointInTimeData(loanId, "10 February 2023"); + verifyOutstanding(pointInTimeData, outstanding(5000.0, 0.0, 0.0, 0.0, 5000.0)); + verifyArreals(pointInTimeData, true, "2023-02-01"); + + // repay 500 + addRepaymentForLoan(loanId, 2500.0, "01 February 2023"); + + LoanPointInTimeData pointInTimeDataAfterRepay = getPointInTimeData(loanId, "10 February 2023"); + verifyOutstanding(pointInTimeDataAfterRepay, outstanding(2500.0, 0.0, 0.0, 0.0, 2500.0)); + verifyArreals(pointInTimeDataAfterRepay, false, null); + + // verify transactions + verifyTransactions(loanId, // + transaction(5000.0, "Disbursement", "01 January 2023"), // + transaction(125.0, "Repayment (at time of disbursement)", "01 January 2023"), // + transaction(2500.0, "Repayment", "01 February 2023") // + ); + }); + } }