FINERACT-2059: Re-aging repayment schedule handling

This commit is contained in:
Adam Saghy
2024-03-19 10:30:18 +01:00
parent 314758abb5
commit d955bc26cb
21 changed files with 413 additions and 114 deletions
@@ -285,8 +285,10 @@ final class LoanTransactionsApiResourceSwagger {
public Long writeoffReasonId;
// command=reAge START
@Schema(example = "frequency")
public String frequency;
@Schema(example = "frequencyType")
public String frequencyType;
@Schema(example = "frequencyNumber")
public Integer frequencyNumber;
@Schema(example = "startDate")
public String startDate;
@Schema(example = "numberOfInstallments")
@@ -1854,7 +1854,8 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
holidayDetailDTO);
updateMapWithAmount(principalPortionMap, unprocessed, applicableDate);
installment.addPrincipalAmount(unprocessed);
LoanRepaymentScheduleInstallment lastInstallment = installments.get(installments.size() - 1);
LoanRepaymentScheduleInstallment lastInstallment = installments.stream().filter(i -> !i.isDownPayment())
.reduce((first, second) -> second).orElseThrow();
lastInstallment.updatePrincipal(lastInstallment.getPrincipal(unprocessed.getCurrency()).plus(unprocessed).getAmount());
lastInstallment.payPrincipalComponent(detail.getTransactionDate(), unprocessed);
}
@@ -181,6 +181,12 @@ public abstract class AbstractProgressiveLoanScheduleGenerator implements LoanSc
// }
}
// If the disbursement happened after maturity date
if (loanApplicationTerms.isMultiDisburseLoan()) {
processDisbursements(loanApplicationTerms, chargesDueAtTimeOfDisbursement, scheduleParams, periods,
DateUtils.getBusinessLocalDate().plusDays(1));
}
// determine fees and penalties for charges which depends on total
// loan interest
updatePeriodsWithCharges(currency, scheduleParams, periods, nonCompoundingCharges, mc);
@@ -498,7 +498,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
}
if (!changes.isEmpty()) {
loan.updateLoanScheduleDependentDerivedFields();
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
final String noteText = command.stringValueOfParameterNamed("note");
@@ -24,6 +24,7 @@ import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
@@ -41,12 +42,18 @@ import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.loanaccount.api.LoanReAgingApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionComparator;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgeParameter;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -61,6 +68,8 @@ public class LoanReAgingServiceImpl {
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanTransactionRepository loanTransactionRepository;
private final LoanReAgingParameterRepository reAgingParameterRepository;
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
private final NoteRepository noteRepository;
public CommandProcessingResult reAge(Long loanId, JsonCommand command) {
Loan loan = loanAssembler.assembleFrom(loanId);
@@ -77,6 +86,15 @@ public class LoanReAgingServiceImpl {
LoanReAgeParameter reAgeParameter = createReAgeParameter(reAgeTransaction, command);
reAgingParameterRepository.saveAndFlush(reAgeParameter);
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
.determineProcessor(loan.transactionProcessingStrategy());
loanRepaymentScheduleTransactionProcessor.processLatestTransaction(reAgeTransaction,
new LoanRepaymentScheduleTransactionProcessor.TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney())));
loan.updateLoanScheduleDependentDerivedFields();
persistNote(loan, command, changes);
// delinquency recalculation will be triggered by the event in a decoupled way via a listener
businessEventNotifierService.notifyPostBusinessEvent(new LoanReAgeBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanReAgeTransactionBusinessEvent(reAgeTransaction));
@@ -91,15 +109,6 @@ public class LoanReAgingServiceImpl {
.with(changes).build();
}
private LoanReAgeParameter createReAgeParameter(LoanTransaction reAgeTransaction, JsonCommand command) {
// TODO: these parameters should be checked when the validations are implemented
PeriodFrequencyType periodFrequencyType = command.enumValueOfParameterNamed(LoanReAgingApiConstants.frequency,
PeriodFrequencyType.class);
LocalDate startDate = command.dateValueOfParameterNamed(LoanReAgingApiConstants.startDate);
Integer numberOfInstallments = command.integerValueOfParameterNamed(LoanReAgingApiConstants.numberOfInstallments);
return new LoanReAgeParameter(reAgeTransaction.getId(), periodFrequencyType, startDate, numberOfInstallments);
}
public CommandProcessingResult undoReAge(Long loanId, JsonCommand command) {
Loan loan = loanAssembler.assembleFrom(loanId);
reAgingValidator.validateUndoReAge(loan, command);
@@ -115,6 +124,10 @@ public class LoanReAgingServiceImpl {
reverseReAgeTransaction(reAgeTransaction, command);
loanTransactionRepository.saveAndFlush(reAgeTransaction);
reProcessLoanTransactions(reAgeTransaction.getLoan());
loan.updateLoanScheduleDependentDerivedFields();
persistNote(loan, command, changes);
// delinquency recalculation will be triggered by the event in a decoupled way via a listener
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoReAgeBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(new LoanUndoReAgeTransactionBusinessEvent(reAgeTransaction));
@@ -156,4 +169,35 @@ public class LoanReAgingServiceImpl {
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.REAGE.getValue(), transactionDate, txPrincipalAmount,
txPrincipalAmount, ZERO, ZERO, ZERO, null, false, null, txExternalId);
}
private LoanReAgeParameter createReAgeParameter(LoanTransaction reAgeTransaction, JsonCommand command) {
// TODO: these parameters should be checked when the validations are implemented
PeriodFrequencyType periodFrequencyType = command.enumValueOfParameterNamed(LoanReAgingApiConstants.frequencyType,
PeriodFrequencyType.class);
LocalDate startDate = command.dateValueOfParameterNamed(LoanReAgingApiConstants.startDate);
Integer numberOfInstallments = command.integerValueOfParameterNamed(LoanReAgingApiConstants.numberOfInstallments);
Integer periodFrequencyNumber = command.integerValueOfParameterNamed(LoanReAgingApiConstants.frequencyNumber);
return new LoanReAgeParameter(reAgeTransaction.getId(), periodFrequencyType, periodFrequencyNumber, startDate,
numberOfInstallments);
}
private void reProcessLoanTransactions(Loan loan) {
final List<LoanTransaction> filteredTransactions = loan.getLoanTransactions().stream().filter(LoanTransaction::isNotReversed)
.filter(t -> t.isChargeOff() || !t.isNonMonetaryTransaction()).sorted(LoanTransactionComparator.INSTANCE).toList();
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loanRepaymentScheduleTransactionProcessorFactory
.determineProcessor(loan.transactionProcessingStrategy());
loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(), filteredTransactions,
loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
}
private void persistNote(Loan loan, JsonCommand command, Map<String, Object> changes) {
if (command.hasParameter("note")) {
final String note = command.stringValueOfParameterNamed("note");
final Note newNote = Note.loanNote(loan, note);
changes.put("note", note);
this.noteRepository.saveAndFlush(newNote);
}
}
}
@@ -20,6 +20,7 @@ package org.apache.fineract.portfolio.loanaccount.starter;
import java.util.List;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor;
@@ -103,8 +104,9 @@ public class LoanAccountAutoStarter {
@Bean
@Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class)
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor() {
return new AdvancedPaymentScheduleTransactionProcessor();
public AdvancedPaymentScheduleTransactionProcessor advancedPaymentScheduleTransactionProcessor(
LoanReAgingParameterRepository reAgingParameterRepository) {
return new AdvancedPaymentScheduleTransactionProcessor(reAgingParameterRepository);
}
}
@@ -71,4 +71,12 @@
</column>
</addColumn>
</changeSet>
<changeSet id="4" author="fineract">
<addColumn tableName="m_loan_reage_parameter">
<column name="frequency_number" type="SMALLINT" defaultValueNumeric="1">
<constraints nullable="false"/>
</column>
</addColumn>
<renameColumn tableName="m_loan_reage_parameter" oldColumnName="frequency" newColumnName="frequency_type" columnDataType="VARCHAR(100)"/>
</changeSet>
</databaseChangeLog>
@@ -61,6 +61,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.domain.reaging.LoanReAgingParameterRepository;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
@@ -89,6 +90,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
private static final MonetaryCurrency MONETARY_CURRENCY = new MonetaryCurrency("USD", 2, 1);
private static final MockedStatic<MoneyHelper> MONEY_HELPER = mockStatic(MoneyHelper.class);
private AdvancedPaymentScheduleTransactionProcessor underTest;
private LoanReAgingParameterRepository reAgingParameterRepository = Mockito.mock(LoanReAgingParameterRepository.class);
@BeforeAll
public static void init() {
@@ -102,7 +104,7 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
@BeforeEach
public void setUp() {
underTest = new AdvancedPaymentScheduleTransactionProcessor();
underTest = new AdvancedPaymentScheduleTransactionProcessor(reAgingParameterRepository);
ThreadLocalContextUtil.setTenant(new FineractPlatformTenant(1L, "default", "Default", "Asia/Kolkata", null));
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);