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") //
+            );
+        });
+    }
 }

Reply via email to