FINERACT-2181: Prepayment flow test fixes

This commit is contained in:
Arnold Galovics
2025-06-19 11:10:36 +02:00
committed by Arnold Gálovics
parent cb82af119b
commit a158c930bc
5 changed files with 101 additions and 15 deletions
@@ -143,11 +143,11 @@ Feature: LoanAccrualTransaction
And Admin successfully approves the loan on "01 January 2023" with "1000" amount and expected disbursement date on "01 January 2023"
When Admin successfully disburse the loan on "01 January 2023" with "1000" EUR transaction amount
When Admin sets the business date to "02 January 2023"
And Customer makes "AUTOPAY" repayment on "02 January 2023" with 1010.19 EUR transaction amount
And Customer makes "AUTOPAY" repayment on "02 January 2023" with 1000.33 EUR transaction amount
Then Loan status will be "CLOSED_OBLIGATIONS_MET"
Then Loan Transactions tab has a transaction with date: "02 January 2023", and with the following data:
| Transaction Type | Amount | Principal | Interest | Fees | Penalties | Loan Balance |
| Accrual | 10.19 | 0.0 | 10.19 | 0.0 | 0.0 | 0.0 |
| Accrual | 0.33 | 0.0 | 0.33 | 0.0 | 0.0 | 0.0 |
Then LoanAccrualTransactionCreatedBusinessEvent is raised on "02 January 2023"
@TestRailId:C2683
@@ -164,6 +164,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final LoanTransactionProcessingService loanTransactionProcessingService;
private final LoanBalanceService loanBalanceService;
private final LoanTransactionService loanTransactionService;
private final LoanAccountDomainServiceJpaHelper loanAccountDomainServiceJpaHelper;
@Transactional
@Override
@@ -242,21 +243,14 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
}
LocalDate recalculateFrom = null;
if (loan.isInterestBearingAndInterestRecalculationEnabled() && loan.getLoanProduct().getProductInterestRecalculationDetails()
.getPreCloseInterestCalculationStrategy().calculateTillPreClosureDateEnabled()) {
if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
recalculateFrom = transactionDate;
}
final ScheduleGeneratorDTO scheduleGeneratorDTOForPrepay = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom,
transactionDate, holidayDetailDto);
Money outstanding = loanTransactionProcessingService.fetchPrepaymentDetail(scheduleGeneratorDTOForPrepay, transactionDate, loan)
.getTotalOutstanding();
LocalDate recalculateTill = null;
if (repaymentAmount.isGreaterThanOrEqualTo(outstanding) && loan.isInterestBearingAndInterestRecalculationEnabled()
&& loan.getLoanProduct().getProductInterestRecalculationDetails().getPreCloseInterestCalculationStrategy()
.calculateTillPreClosureDateEnabled()) {
recalculateTill = transactionDate;
}
LocalDate recalculateTill = loanAccountDomainServiceJpaHelper.calculateRecalculateTillDate(loan, transactionDate,
scheduleGeneratorDTOForPrepay, repaymentAmount);
final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom,
recalculateTill, holidayDetailDto);
@@ -0,0 +1,83 @@
/**
* 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;
import java.time.LocalDate;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.domain.BatchRequestContextHolder;
import org.apache.fineract.infrastructure.core.domain.FineractRequestContextHolder;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionProcessingService;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor
@Component
@Slf4j
public class LoanAccountDomainServiceJpaHelper {
private final LoanAssembler loanAssembler;
private final LoanTransactionProcessingService loanTransactionProcessingService;
@Transactional(propagation = Propagation.REQUIRES_NEW, readOnly = true)
public LocalDate calculateRecalculateTillDate(Loan loan, LocalDate transactionDate, ScheduleGeneratorDTO scheduleGeneratorDTOForPrepay,
Money repaymentAmount) {
LocalDate recalculateTill = null;
try {
if (FineractRequestContextHolder.isBatchRequest() && BatchRequestContextHolder.isEnclosingTransaction()) {
// In case of Batch requests with enclosing transaction, the current way of calculating the prepayment
// amount (since it changes
// the state of entities which would be written back to the DB) is incorrect, so we won't allow it for
// now.
// With enclosing transactions where the loan is created and repaid within the same batch request, due
// to REQUIRES_NEW, this method
// will simply not see that a loan has been created.
// Temporarily if you wanna use the batch API for prepayment, make sure to split the requests in a way
// that loan creation and
// repayment doesn't occur in the same batch request.
// Example testcase:
// org.apache.fineract.integrationtests.BatchApiTest.shouldReturnOkStatusOnSuccessfulGetDatatableEntryWithNoQueryParam
// TODO: this can be removed if the prepayment amount calculation below this is fixed in a way that it
// doesn't change any entity
// but works with DTOs
return null;
}
loan = loanAssembler.assembleFrom(loan.getId());
if (loan.isInterestBearingAndInterestRecalculationEnabled() && loan.getLoanProduct().getProductInterestRecalculationDetails()
.getPreCloseInterestCalculationStrategy().calculateTillPreClosureDateEnabled()) {
Money outstanding = loanTransactionProcessingService
.fetchPrepaymentDetail(scheduleGeneratorDTOForPrepay, transactionDate, loan).getTotalOutstanding();
if (repaymentAmount.isGreaterThanOrEqualTo(outstanding)) {
recalculateTill = transactionDate;
}
}
} catch (Exception e) {
// TODO: there's a bug where the prepayment calculation fails
// the test-case is org.apache.fineract.integrationtests.LoanTransactionAccrualActivityPostingTest.test in
// the integration-tests
// seems like it occurs only on CUMULATIVE loans, not PROGRESSIVE
log.warn("Unable to calculate prepayment amount", e);
}
return recalculateTill;
}
}
@@ -18,6 +18,8 @@
*/
package org.apache.fineract.portfolio.loanaccount.service;
import jakarta.persistence.EntityManager;
import jakarta.persistence.FlushModeType;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.HashMap;
@@ -35,19 +37,22 @@ import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.interceptor.TransactionInterceptor;
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
@Transactional
public class LoanPointInTimeServiceImpl implements LoanPointInTimeService {
private final LoanUtilService loanUtilService;
private final LoanScheduleService loanScheduleService;
private final LoanAssembler loanAssembler;
private final LoanPointInTimeData.Mapper dataMapper;
private final EntityManager entityManager;
@Override
public LoanPointInTimeData retrieveAt(Long loanId, LocalDate date) {
entityManager.setFlushMode(FlushModeType.COMMIT);
validateSingularRetrieval(loanId, date);
// Note: since everything is running in a readOnly transaction
@@ -66,6 +71,8 @@ public class LoanPointInTimeServiceImpl implements LoanPointInTimeService {
return dataMapper.map(loan);
} finally {
entityManager.clear();
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
ThreadLocalContextUtil.setBusinessDates(originalBDs);
}
}
@@ -89,7 +96,9 @@ public class LoanPointInTimeServiceImpl implements LoanPointInTimeService {
@Override
public List<LoanPointInTimeData> retrieveAt(List<Long> loanIds, LocalDate date) {
validateBulkRetrieval(loanIds, date);
return loanIds.stream().map(loanId -> retrieveAt(loanId, date)).toList();
List<LoanPointInTimeData> result = loanIds.stream().map(loanId -> retrieveAt(loanId, date)).toList();
TransactionInterceptor.currentTransactionStatus().setRollbackOnly();
return result;
}
private void validateBulkRetrieval(List<Long> loanIds, LocalDate date) {
@@ -342,7 +342,7 @@ public class LoanPointInTimeTest extends BaseLoanIntegrationTest {
transaction(500.0, "Repayment", "09 February 2023"), //
transaction(500.0, "Repayment", "01 March 2023"), //
transaction(5032.52, "Repayment", "05 March 2023"), //
transaction(1282.52, "Accrual", "05 March 2023") //
transaction(1110.08, "Accrual", "05 March 2023") //
);
});
}