FINERACT-2321: Adding arrears days to loan/at-date api
This commit is contained in:
+37
@@ -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;
|
||||||
|
}
|
||||||
+3
@@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.service;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
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;
|
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
|
||||||
|
|
||||||
public interface LoanArrearsAgingService {
|
public interface LoanArrearsAgingService {
|
||||||
@@ -35,4 +36,6 @@ public interface LoanArrearsAgingService {
|
|||||||
boolean isInsertStatement);
|
boolean isInsertStatement);
|
||||||
|
|
||||||
void updateScheduleWithPaidDetail(Map<Long, List<LoanSchedulePeriodData>> scheduleDate, List<Map<String, Object>> loanSummary);
|
void updateScheduleWithPaidDetail(Map<Long, List<LoanSchedulePeriodData>> scheduleDate, List<Map<String, Object>> loanSummary);
|
||||||
|
|
||||||
|
LoanArrearsData calculateArrearsForLoan(Loan loan);
|
||||||
}
|
}
|
||||||
|
|||||||
+30
-4
@@ -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.LoanRepaymentScheduleInstallment;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
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.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
|
||||||
import org.springframework.dao.DataAccessException;
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.jdbc.core.JdbcTemplate;
|
import org.springframework.jdbc.core.JdbcTemplate;
|
||||||
@@ -138,8 +139,8 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private String constructUpdateStatement(final Loan loan, boolean isInsertStatement) {
|
@Override
|
||||||
String updateSql = null;
|
public LoanArrearsData calculateArrearsForLoan(Loan loan) {
|
||||||
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
|
||||||
BigDecimal principalOverdue = BigDecimal.ZERO;
|
BigDecimal principalOverdue = BigDecimal.ZERO;
|
||||||
BigDecimal interestOverdue = BigDecimal.ZERO;
|
BigDecimal interestOverdue = BigDecimal.ZERO;
|
||||||
@@ -158,9 +159,33 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
BigDecimal totalOverDue = principalOverdue.add(interestOverdue).add(feeOverdue).add(penaltyOverdue);
|
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) {
|
if (isInsertStatement) {
|
||||||
updateSql = constructInsertStatement(loan.getId(), principalOverdue, interestOverdue, feeOverdue, penaltyOverdue,
|
updateSql = constructInsertStatement(loan.getId(), principalOverdue, interestOverdue, feeOverdue, penaltyOverdue,
|
||||||
overDueSince);
|
overDueSince);
|
||||||
@@ -201,6 +226,7 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService {
|
|||||||
BigDecimal penaltyOverdue = BigDecimal.ZERO;
|
BigDecimal penaltyOverdue = BigDecimal.ZERO;
|
||||||
LocalDate overDueSince = DateUtils.getBusinessLocalDate();
|
LocalDate overDueSince = DateUtils.getBusinessLocalDate();
|
||||||
|
|
||||||
|
// TODO: this needs to be refactored to use the calculateArrearsForLoan method.
|
||||||
for (LoanSchedulePeriodData loanSchedulePeriodData : entry.getValue()) {
|
for (LoanSchedulePeriodData loanSchedulePeriodData : entry.getValue()) {
|
||||||
if (!loanSchedulePeriodData.getComplete()) {
|
if (!loanSchedulePeriodData.getComplete()) {
|
||||||
principalOverdue = principalOverdue
|
principalOverdue = principalOverdue
|
||||||
|
|||||||
+5
@@ -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.data.CurrencyData;
|
||||||
import org.apache.fineract.organisation.monetary.mapper.CurrencyMapper;
|
import org.apache.fineract.organisation.monetary.mapper.CurrencyMapper;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.arrears.LoanArrearsData;
|
||||||
import org.mapstruct.Mapping;
|
import org.mapstruct.Mapping;
|
||||||
|
|
||||||
@Data
|
@Data
|
||||||
@@ -51,6 +52,9 @@ public class LoanPointInTimeData {
|
|||||||
private Long loanProductId;
|
private Long loanProductId;
|
||||||
private String loanProductName;
|
private String loanProductName;
|
||||||
|
|
||||||
|
// Arrears data
|
||||||
|
private LoanArrearsData arrears;
|
||||||
|
|
||||||
@org.mapstruct.Mapper(config = MapstructMapperConfig.class, uses = { LoanStatusEnumData.Mapper.class, CurrencyMapper.class,
|
@org.mapstruct.Mapper(config = MapstructMapperConfig.class, uses = { LoanStatusEnumData.Mapper.class, CurrencyMapper.class,
|
||||||
LoanPrincipalData.Mapper.class, LoanInterestData.Mapper.class, LoanFeeData.Mapper.class, LoanPenaltyData.Mapper.class,
|
LoanPrincipalData.Mapper.class, LoanInterestData.Mapper.class, LoanFeeData.Mapper.class, LoanPenaltyData.Mapper.class,
|
||||||
LoanTotalAmountData.Mapper.class })
|
LoanTotalAmountData.Mapper.class })
|
||||||
@@ -70,6 +74,7 @@ public class LoanPointInTimeData {
|
|||||||
@Mapping(source = "summary", target = "total")
|
@Mapping(source = "summary", target = "total")
|
||||||
@Mapping(source = "loanProduct.id", target = "loanProductId")
|
@Mapping(source = "loanProduct.id", target = "loanProductId")
|
||||||
@Mapping(source = "loanProduct.name", target = "loanProductName")
|
@Mapping(source = "loanProduct.name", target = "loanProductName")
|
||||||
|
@Mapping(target = "arrears", ignore = true)
|
||||||
LoanPointInTimeData map(Loan source);
|
LoanPointInTimeData map(Loan source);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+7
-1
@@ -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.LoanPointInTimeData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
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.stereotype.Service;
|
||||||
import org.springframework.transaction.annotation.Transactional;
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
import org.springframework.transaction.interceptor.TransactionInterceptor;
|
||||||
@@ -49,6 +50,7 @@ public class LoanPointInTimeServiceImpl implements LoanPointInTimeService {
|
|||||||
private final LoanAssembler loanAssembler;
|
private final LoanAssembler loanAssembler;
|
||||||
private final LoanPointInTimeData.Mapper dataMapper;
|
private final LoanPointInTimeData.Mapper dataMapper;
|
||||||
private final EntityManager entityManager;
|
private final EntityManager entityManager;
|
||||||
|
private final LoanArrearsAgingService arrearsAgingService;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public LoanPointInTimeData retrieveAt(Long loanId, LocalDate date) {
|
public LoanPointInTimeData retrieveAt(Long loanId, LocalDate date) {
|
||||||
@@ -69,7 +71,11 @@ public class LoanPointInTimeServiceImpl implements LoanPointInTimeService {
|
|||||||
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
|
ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, null, null);
|
||||||
loanScheduleService.recalculateSchedule(loan, scheduleGeneratorDTO);
|
loanScheduleService.recalculateSchedule(loan, scheduleGeneratorDTO);
|
||||||
|
|
||||||
return dataMapper.map(loan);
|
LoanArrearsData arrearsData = arrearsAgingService.calculateArrearsForLoan(loan);
|
||||||
|
|
||||||
|
LoanPointInTimeData result = dataMapper.map(loan);
|
||||||
|
result.setArrears(arrearsData);
|
||||||
|
return result;
|
||||||
} finally {
|
} finally {
|
||||||
entityManager.clear();
|
entityManager.clear();
|
||||||
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
|
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
|
||||||
|
|||||||
+9
@@ -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) {
|
protected void placeHardLockOnLoan(Long loanId) {
|
||||||
loanAccountLockHelper.placeSoftLockOnLoanAccount(loanId.intValue(), "LOAN_COB_CHUNK_PROCESSING");
|
loanAccountLockHelper.placeSoftLockOnLoanAccount(loanId.intValue(), "LOAN_COB_CHUNK_PROCESSING");
|
||||||
}
|
}
|
||||||
|
|||||||
+98
@@ -656,4 +656,102 @@ public class LoanPointInTimeTest extends BaseLoanIntegrationTest {
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void test_LoanPointInTimeDataWorks_ForArrealDataCalculation() {
|
||||||
|
AtomicReference<Long> 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") //
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user