This is an automated email from the ASF dual-hosted git repository.
arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 93bfc6fb66 FINERACT-2321: Adding arrears days to loan/at-date api
93bfc6fb66 is described below
commit 93bfc6fb661b3aed78576fe31258144be0516f4a
Author: soticsenge <[email protected]>
AuthorDate: Thu Jul 10 16:24:32 2025 +0200
FINERACT-2321: Adding arrears days to loan/at-date api
---
.../arrears/LoanArrearsData.java} | 27 +++---
.../service/LoanArrearsAgingService.java | 3 +
.../service/LoanArrearsAgingServiceImpl.java | 34 +++++++-
.../loanaccount/data/LoanPointInTimeData.java | 5 ++
.../service/LoanPointInTimeServiceImpl.java | 8 +-
.../integrationtests/BaseLoanIntegrationTest.java | 9 ++
.../loan/pointintime/LoanPointInTimeTest.java | 98 ++++++++++++++++++++++
7 files changed, 165 insertions(+), 19 deletions(-)
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/domain/arrears/LoanArrearsData.java
old mode 100755
new mode 100644
similarity index 50%
copy from
fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanArrearsAgingService.java
copy to
fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/arrears/LoanArrearsData.java
index a401ceca16..8791742047
---
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/domain/arrears/LoanArrearsData.java
@@ -16,23 +16,22 @@
* specific language governing permissions and limitations
* under the License.
*/
-package org.apache.fineract.portfolio.loanaccount.service;
+package org.apache.fineract.portfolio.loanaccount.domain.arrears;
-import java.util.List;
-import java.util.Map;
-import org.apache.fineract.portfolio.loanaccount.domain.Loan;
-import
org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData;
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import lombok.Data;
-public interface LoanArrearsAgingService {
+@Data
+public class LoanArrearsData {
- void updateLoanArrearsAgeingDetailsWithOriginalSchedule(Loan loan);
+ private BigDecimal principalOverdue;
+ private BigDecimal interestOverdue;
+ private BigDecimal feeOverdue;
+ private BigDecimal penaltyOverdue;
+ private BigDecimal totalOverdue;
- Map<Long, List<LoanSchedulePeriodData>> getScheduleDate(String loanId);
+ private LocalDate overDueSince;
- void updateLoanArrearsAgeingDetails(Loan loan);
-
- void createInsertStatements(List<String> insertStatement, Map<Long,
List<LoanSchedulePeriodData>> scheduleDate,
- boolean isInsertStatement);
-
- void updateScheduleWithPaidDetail(Map<Long, List<LoanSchedulePeriodData>>
scheduleDate, List<Map<String, Object>> loanSummary);
+ 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 a401ceca16..0625fdaf55 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<Long, List<LoanSchedulePeriodData>>
scheduleDate, List<Map<String, Object>> 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 2526b5ba7f..7e126b3bb6 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<LoanRepaymentScheduleInstallment> 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 08fd930dfe..f615edbe11 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 345c8e1e79..278c2a9a7d 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 46ce4b44b0..dfe969ca41 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 71d42240fe..969a870322 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<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") //
+ );
+ });
+ }
}