FINERACT-2080: Extract loanLifecycleStateMachine from Loan entity, refactor transition functionality

This commit is contained in:
Oleksii Novikov
2025-04-23 10:12:53 +02:00
committed by Adam Saghy
parent 79c006774e
commit 64dddfca48
24 changed files with 419 additions and 494 deletions
@@ -19,21 +19,21 @@
package org.apache.fineract.portfolio.loanaccount.domain;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanStatusChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;
// TODO: introduce tests for the state machine
@Slf4j
@Component
@RequiredArgsConstructor
public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachine {
private static Logger LOG = LoggerFactory.getLogger(DefaultLoanLifecycleStateMachine.class);
private static final List<LoanStatus> ALLOWED_LOAN_STATUSES = List.of(LoanStatus.values());
private final BusinessEventNotifierService businessEventNotifierService;
@@ -46,7 +46,29 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
@Override
public void transition(final LoanEvent loanEvent, final Loan loan) {
loan.updateLoanSummaryDerivedFields();
internalTransition(loanEvent, loan);
}
@Override
public void determineAndTransition(final Loan loan, final LocalDate transactionDate) {
if (loan.getStatus() == null) {
return;
}
loan.updateLoanSummaryDerivedFields();
final LoanStatusTransition transition = determineTransition(loan, loan.getStatus(), transactionDate);
if (MathUtil.isEmpty(loan.getTotalOverpaid())) {
loan.setOverpaidOnDate(null);
}
if (transition.transitionNeeded()) {
internalTransition(transition.event(), loan);
}
}
private void internalTransition(final LoanEvent loanEvent, final Loan loan) {
LoanStatus oldStatus = loan.getStatus();
LoanStatus newStatus = getNextStatus(loanEvent, loan);
if (newStatus != null) {
@@ -58,7 +80,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
}
// set mandatory field states based on new status after the transition
LOG.debug("Transitioning loan {} status from {} to {}", loan.getId(), oldStatus, newStatus);
log.debug("Transitioning loan {} status from {} to {}", loan.getId(), oldStatus, newStatus);
switch (newStatus) {
case SUBMITTED_AND_PENDING_APPROVAL -> {
loan.setApprovedOnDate(null);
@@ -276,4 +298,89 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
return allowed;
}
private LoanStatusTransition determineTransition(final Loan loan, final LoanStatus currentStatus, final LocalDate transactionDate) {
final boolean hasOutstanding = loan.getSummary().getTotalOutstanding(loan.getCurrency()).isGreaterThanZero();
final boolean isRepaidInFull = loan.getSummary().isRepaidInFull(loan.getCurrency());
final boolean isOverpaid = MathUtil.isGreaterThanZero(loan.getTotalOverpaid());
final boolean isAllChargesPaid = loan.getLoanCharges().stream().allMatch(
charge -> !charge.isActive() || charge.amount().compareTo(BigDecimal.ZERO) <= 0 || charge.isPaid() || charge.isWaived());
if (currentStatus.isOverpaid()) {
return determineTransitionFromOverpaid(loan, isOverpaid, isRepaidInFull, isAllChargesPaid, hasOutstanding, transactionDate);
} else if (currentStatus.isClosedObligationsMet()) {
return determineTransitionFromClosedObligationsMet(loan, isOverpaid, hasOutstanding, transactionDate);
} else if (currentStatus.isActive()) {
return determineTransitionFromActive(loan, isOverpaid, isRepaidInFull, isAllChargesPaid, transactionDate);
} else if (currentStatus.isClosedWrittenOff() || currentStatus.isClosedWithOutsandingAmountMarkedForReschedule()) {
return determineTransitionFromClosedWrittenOffOrRescheduled(loan, hasOutstanding);
}
return LoanStatusTransition.noTransition(currentStatus);
}
private LoanStatusTransition determineTransitionFromOverpaid(final Loan loan, final boolean isOverpaid, final boolean isRepaidInFull,
final boolean isAllChargesPaid, final boolean hasOutstanding, final LocalDate transactionDate) {
if (!isOverpaid) {
if (isRepaidInFull && isAllChargesPaid) {
loan.setClosedOnDate(transactionDate);
loan.setActualMaturityDate(transactionDate);
return LoanStatusTransition.to(LoanStatus.CLOSED_OBLIGATIONS_MET, LoanEvent.LOAN_CREDIT_BALANCE_REFUND);
} else if (hasOutstanding) {
loan.handleMaturityDateActivate();
return LoanStatusTransition.to(LoanStatus.ACTIVE, LoanEvent.LOAN_REPAYMENT_OR_WAIVER);
}
}
return LoanStatusTransition.noTransition(LoanStatus.OVERPAID);
}
private LoanStatusTransition determineTransitionFromClosedObligationsMet(final Loan loan, final boolean isOverpaid,
final boolean hasOutstanding, LocalDate transactionDate) {
if (isOverpaid) {
loan.setOverpaidOnDate(transactionDate);
loan.setClosedOnDate(null);
loan.setActualMaturityDate(null);
return LoanStatusTransition.to(LoanStatus.OVERPAID, LoanEvent.LOAN_OVERPAYMENT);
} else if (hasOutstanding) {
loan.setClosedOnDate(null);
loan.setActualMaturityDate(null);
loan.handleMaturityDateActivate();
return LoanStatusTransition.to(LoanStatus.ACTIVE, LoanEvent.LOAN_REPAYMENT_OR_WAIVER);
}
return LoanStatusTransition.noTransition(LoanStatus.CLOSED_OBLIGATIONS_MET);
}
private LoanStatusTransition determineTransitionFromActive(final Loan loan, final boolean isOverpaid, final boolean isRepaidInFull,
final boolean isAllChargesPaid, final LocalDate transactionDate) {
if (isOverpaid) {
loan.setOverpaidOnDate(transactionDate);
loan.setActualMaturityDate(null);
return LoanStatusTransition.to(LoanStatus.OVERPAID, LoanEvent.LOAN_OVERPAYMENT);
} else if (isRepaidInFull && isAllChargesPaid) {
loan.setClosedOnDate(transactionDate);
loan.setActualMaturityDate(transactionDate);
return LoanStatusTransition.to(LoanStatus.CLOSED_OBLIGATIONS_MET, LoanEvent.REPAID_IN_FULL);
}
return LoanStatusTransition.noTransition(LoanStatus.ACTIVE);
}
private LoanStatusTransition determineTransitionFromClosedWrittenOffOrRescheduled(final Loan loan, final boolean hasOutstanding) {
if (hasOutstanding) {
return LoanStatusTransition.to(LoanStatus.ACTIVE, LoanEvent.LOAN_ADJUST_TRANSACTION);
}
return LoanStatusTransition.noTransition(loan.getStatus());
}
private record LoanStatusTransition(LoanStatus targetStatus, LoanEvent event, boolean transitionNeeded) {
public static LoanStatusTransition to(final LoanStatus targetStatus, final LoanEvent event) {
return new LoanStatusTransition(targetStatus, event, true);
}
public static LoanStatusTransition noTransition(final LoanStatus currentStatus) {
return new LoanStatusTransition(currentStatus, null, false);
}
}
}
@@ -45,10 +45,8 @@ import java.util.ArrayList;
import java.util.Collection;
import java.util.Comparator;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.ListIterator;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.Set;
@@ -82,7 +80,6 @@ import org.apache.fineract.portfolio.collateral.domain.LoanCollateral;
import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.fund.domain.Fund;
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
@@ -90,7 +87,6 @@ import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransact
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.apache.fineract.portfolio.rate.domain.Rate;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
import org.apache.fineract.useradministration.domain.AppUser;
@@ -298,6 +294,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "loan_product_counter")
private Integer loanProductCounter;
@Setter
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private Set<LoanCharge> charges = new HashSet<>();
@@ -321,15 +318,13 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private List<LoanTransaction> loanTransactions = new ArrayList<>();
@Setter
@Embedded
private LoanSummary summary;
@Transient
private boolean accountNumberRequiresAutoGeneration;
@Transient
private LoanLifecycleStateMachine loanLifecycleStateMachine;
@Setter()
@Column(name = "principal_amount_proposed", scale = 6, precision = 19, nullable = false)
private BigDecimal proposedPrincipal;
@@ -638,40 +633,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return collateral;
}
public void addLoanCharge(final LoanCharge loanCharge) {
loanCharge.update(this);
final BigDecimal amount = calculateAmountPercentageAppliedTo(loanCharge);
BigDecimal chargeAmt;
BigDecimal totalChargeAmt = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageBased()) {
chargeAmt = loanCharge.getPercentage();
if (loanCharge.isInstalmentFee()) {
totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge);
} else if (loanCharge.isOverdueInstallmentCharge()) {
totalChargeAmt = loanCharge.amountOutstanding();
}
} else {
chargeAmt = loanCharge.amountOrPercentage();
}
loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt);
// NOTE: must add new loan charge to set of loan charges before
// reprocessing the repayment schedule.
if (this.charges == null) {
this.charges = new HashSet<>();
}
this.charges.add(loanCharge);
this.summary = updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
// store Id's of existing loan transactions and existing reversed loan transactions
final SingleLoanChargeRepaymentScheduleProcessingWrapper wrapper = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), loanCharge);
updateLoanSummaryDerivedFields();
loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_ADDED, this);
}
/**
* Creates a loanTransaction for "Apply Charge Event" with transaction date set to "suppliedTransactionDate". The
* newly created transaction is also added to the Loan on which this method is called.
@@ -771,9 +732,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
public LoanDisbursementDetails addLoanDisbursementDetails(final LocalDate expectedDisbursementDate, final BigDecimal principal) {
final LocalDate actualDisbursementDate = null;
final LoanDisbursementDetails disbursementDetails = new LoanDisbursementDetails(expectedDisbursementDate, actualDisbursementDate,
principal, this.netDisbursalAmount, false);
final LoanDisbursementDetails disbursementDetails = new LoanDisbursementDetails(expectedDisbursementDate, null, principal,
this.netDisbursalAmount, false);
disbursementDetails.updateLoan(this);
this.disbursementDetails.add(disbursementDetails);
return disbursementDetails;
@@ -992,11 +952,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
}
}
public void updateLoanSummaryAndStatus() {
updateLoanSummaryDerivedFields();
doPostLoanTransactionChecks(getLastUserTransactionDate(), loanLifecycleStateMachine);
}
private boolean isInterestRecalculationEnabledForProduct() {
return this.loanProduct.isInterestRecalculationEnabled();
}
@@ -1111,35 +1066,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return maturityDate;
}
public Map<String, Object> undoApproval(final LoanLifecycleStateMachine loanLifecycleStateMachine) {
final Map<String, Object> actualChanges = new LinkedHashMap<>();
final LoanStatus currentStatus = getStatus();
final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_APPROVAL_UNDO, this);
if (!statusEnum.hasStateOf(currentStatus)) {
loanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVAL_UNDO, this);
actualChanges.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus));
this.approvedOnDate = null;
this.approvedBy = null;
if (this.approvedPrincipal.compareTo(this.proposedPrincipal) != 0) {
this.approvedPrincipal = this.proposedPrincipal;
this.loanRepaymentScheduleDetail.setPrincipal(this.proposedPrincipal);
actualChanges.put(LoanApiConstants.approvedLoanAmountParameterName, this.proposedPrincipal);
actualChanges.put(LoanApiConstants.disbursementPrincipalParameterName, this.proposedPrincipal);
}
actualChanges.put(APPROVED_ON_DATE, "");
this.loanOfficerHistory.clear();
}
return actualChanges;
}
public List<Long> findExistingTransactionIds() {
return getLoanTransactions().stream() //
.filter(loanTransaction -> loanTransaction.getId() != null) //
@@ -1161,18 +1087,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.collect(Collectors.toList());
}
public boolean canDisburse() {
final LoanStatus statusEnum = this.loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_DISBURSED, this);
boolean isMultiTrancheDisburse = false;
LoanStatus actualLoanStatus = getStatus();
if ((actualLoanStatus.isActive() || actualLoanStatus.isClosedObligationsMet() || actualLoanStatus.isOverpaid())
&& isAllTranchesNotDisbursed()) {
isMultiTrancheDisburse = true;
}
return !statusEnum.hasStateOf(actualLoanStatus) || isMultiTrancheDisburse;
}
public Collection<LoanDisbursementDetails> fetchUndisbursedDetail() {
Collection<LoanDisbursementDetails> disbursementDetails = new ArrayList<>();
LocalDate date = null;
@@ -1304,60 +1218,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList());
}
public boolean doPostLoanTransactionChecks(final LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
boolean statusChanged = checkAndHandleLoanStatus(transactionDate, loanLifecycleStateMachine);
resetOverpaidDateIfNeeded();
return statusChanged;
}
private boolean checkAndHandleLoanStatus(final LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
boolean statusChanged = false;
boolean isOverpaid = MathUtil.isGreaterThanZero(totalOverpaid);
if (isOverpaid) {
handleLoanOverpayment(transactionDate, loanLifecycleStateMachine);
statusChanged = true;
} else if (this.summary.isRepaidInFull(getCurrency())) {
handleLoanRepaymentInFull(transactionDate, loanLifecycleStateMachine);
statusChanged = true;
} else {
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, this);
}
return statusChanged;
}
private void resetOverpaidDateIfNeeded() {
if (MathUtil.isEmpty(totalOverpaid)) {
this.overpaidOnDate = null;
}
}
private void handleLoanRepaymentInFull(final LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
boolean isAllChargesPaid = this.charges.stream() //
.allMatch(charge -> !charge.isActive() || charge.amount().compareTo(BigDecimal.ZERO) <= 0 || charge.isPaid()
|| charge.isWaived());
if (isAllChargesPaid) {
this.closedOnDate = transactionDate;
this.actualMaturityDate = transactionDate;
loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this);
} else if (getStatus().isOverpaid()) {
if (MathUtil.isEmpty(totalOverpaid)) {
this.overpaidOnDate = null;
}
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, this);
}
}
private void handleLoanOverpayment(LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.overpaidOnDate = transactionDate;
loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, this);
this.closedOnDate = null;
this.actualMaturityDate = null;
}
public boolean isChronologicallyLatestRepaymentOrWaiver(final LoanTransaction loanTransaction) {
boolean isChronologicallyLatestRepaymentOrWaiver = true;
@@ -1852,10 +1712,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return receivableInterest;
}
public void setHelpers(final LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanLifecycleStateMachine = loanLifecycleStateMachine;
}
public boolean isSyncDisbursementWithMeeting() {
return this.syncDisbursementWithMeeting != null && this.syncDisbursementWithMeeting;
}
@@ -2486,10 +2342,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
Money[] receivables = retrieveIncomeOutstandingTillDate(closureDate);
Money totalPrincipal = Money.of(getCurrency(), this.getSummary().getTotalPrincipalOutstanding());
totalPrincipal = totalPrincipal.minus(receivables[3]);
final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = null;
final LocalDate currentDate = DateUtils.getBusinessLocalDate();
return new LoanRepaymentScheduleInstallment(null, 0, currentDate, currentDate, totalPrincipal.getAmount(),
receivables[0].getAmount(), receivables[1].getAmount(), receivables[2].getAmount(), false, compoundingDetails);
receivables[0].getAmount(), receivables[1].getAmount(), receivables[2].getAmount(), false, null);
}
private Money[] retrieveIncomeOutstandingTillDate(final LocalDate paymentDate) {
@@ -18,9 +18,27 @@
*/
package org.apache.fineract.portfolio.loanaccount.domain;
import java.time.LocalDate;
public interface LoanLifecycleStateMachine {
LoanStatus dryTransition(LoanEvent loanEvent, Loan loan);
void transition(LoanEvent loanEvent, Loan loan);
/**
* Determines the appropriate loan status based on the loan's current financial condition. This method is designed
* to be called at the end of processing to ensure accurate status transitions.
*
* For example, it can determine whether a loan that was OVERPAID should transition to: - CLOSED_OBLIGATIONS_MET (if
* overpayment amount is now zero) - ACTIVE (if there is still outstanding balance) - Remain as OVERPAID (if still
* overpaid)
*
* @param loan
* The loan whose status should be evaluated
* @param transactionDate
* The date of the transaction that may have affected the loan status
*/
void determineAndTransition(Loan loan, LocalDate transactionDate);
}
@@ -39,8 +39,6 @@ public interface LoanAssembler {
Loan assembleFrom(JsonCommand command);
void setHelpers(Loan loanAccount);
void accountNumberGeneration(JsonCommand command, Loan loan);
CodeValue findCodeValueByIdIfProvided(Long codeValueId);
@@ -32,6 +32,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachin
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
@@ -41,6 +42,7 @@ public class LoanChargeService {
private final LoanChargeValidator loanChargeValidator;
private final LoanTransactionProcessingService loanTransactionProcessingService;
private final LoanLifecycleStateMachine loanLifecycleStateMachine;
public void recalculateAllCharges(final Loan loan) {
Set<LoanCharge> charges = loan.getActiveCharges();
@@ -76,9 +78,8 @@ public class LoanChargeService {
}
}
public void makeChargePayment(final Loan loan, final Long chargeId, final LoanLifecycleStateMachine loanLifecycleStateMachine,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final LoanTransaction paymentTransaction, final Integer installmentNumber) {
public void makeChargePayment(final Loan loan, final Long chargeId, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final LoanTransaction paymentTransaction, final Integer installmentNumber) {
loanChargeValidator.validateChargePaymentNotInFuture(paymentTransaction);
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
@@ -88,11 +89,11 @@ public class LoanChargeService {
charge = loanCharge;
}
}
handleChargePaidTransaction(loan, charge, paymentTransaction, loanLifecycleStateMachine, installmentNumber);
handleChargePaidTransaction(loan, charge, paymentTransaction, installmentNumber);
}
public void handleChargePaidTransaction(final Loan loan, final LoanCharge charge, final LoanTransaction chargesPayment,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final Integer installmentNumber) {
final Integer installmentNumber) {
chargesPayment.updateLoan(loan);
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge,
chargesPayment.getAmount(loan.getCurrency()).getAmount(), installmentNumber);
@@ -118,7 +119,41 @@ public class LoanChargeService {
new TransactionCtx(loan.getCurrency(), chargePaymentInstallments, loanCharges,
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
loan.updateLoanSummaryDerivedFields();
loan.doPostLoanTransactionChecks(chargesPayment.getTransactionDate(), loanLifecycleStateMachine);
loanLifecycleStateMachine.determineAndTransition(loan, chargesPayment.getTransactionDate());
}
public void addLoanCharge(final Loan loan, final LoanCharge loanCharge) {
loanCharge.update(loan);
final BigDecimal amount = loan.calculateAmountPercentageAppliedTo(loanCharge);
BigDecimal chargeAmt;
BigDecimal totalChargeAmt = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageBased()) {
chargeAmt = loanCharge.getPercentage();
if (loanCharge.isInstalmentFee()) {
totalChargeAmt = loan.calculatePerInstallmentChargeAmount(loanCharge);
} else if (loanCharge.isOverdueInstallmentCharge()) {
totalChargeAmt = loanCharge.amountOutstanding();
}
} else {
chargeAmt = loanCharge.amountOrPercentage();
}
loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt);
// NOTE: must add new loan charge to set of loan charges before
// reprocessing the repayment schedule.
if (loan.getLoanCharges() == null) {
loan.setCharges(new HashSet<>());
}
loan.getLoanCharges().add(loanCharge);
loan.setSummary(loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement()));
// store Id's of existing loan transactions and existing reversed loan transactions
final SingleLoanChargeRepaymentScheduleProcessingWrapper wrapper = new SingleLoanChargeRepaymentScheduleProcessingWrapper();
wrapper.reprocess(loan.getCurrency(), loan.getDisbursementDate(), loan.getRepaymentScheduleInstallments(), loanCharge);
loan.updateLoanSummaryDerivedFields();
loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_ADDED, loan);
}
}
@@ -21,7 +21,6 @@ package org.apache.fineract.portfolio.loanaccount.service;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
public interface LoanDownPaymentHandlerService {
@@ -30,6 +29,5 @@ public interface LoanDownPaymentHandlerService {
LoanTransaction disbursementTransaction, Loan loan);
void handleRepaymentOrRecoveryOrWaiverTransaction(Loan loan, LoanTransaction newTransactionDetail,
LoanLifecycleStateMachine loanLifecycleStateMachine, LoanTransaction transactionForAdjustment,
ScheduleGeneratorDTO scheduleGeneratorDTO);
LoanTransaction transactionForAdjustment, ScheduleGeneratorDTO scheduleGeneratorDTO);
}
@@ -61,6 +61,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
private final LoanRefundValidator loanRefundValidator;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanTransactionProcessingService loanTransactionProcessingService;
private final LoanLifecycleStateMachine loanLifecycleStateMachine;
@Override
public LoanTransaction handleDownPayment(ScheduleGeneratorDTO scheduleGeneratorDTO, JsonCommand command,
@@ -77,8 +78,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
@Override
public void handleRepaymentOrRecoveryOrWaiverTransaction(final Loan loan, final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction,
final ScheduleGeneratorDTO scheduleGeneratorDTO) {
final LoanTransaction adjustedTransaction, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
if (loanTransaction.isRecoveryRepayment()) {
loanLifecycleStateMachine.transition(LoanEvent.LOAN_RECOVERY_PAYMENT, loan);
}
@@ -158,13 +158,13 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
reprocessLoanTransactionsService.reprocessTransactions(loan);
}
loan.updateLoanSummaryDerivedFields();
/**
* FIXME: Vishwas, skipping post loan transaction checks for Loan recoveries
**/
if (loanTransaction.isNotRecoveryRepayment()) {
loan.doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
loanLifecycleStateMachine.determineAndTransition(loan, loanTransaction.getTransactionDate());
} else {
loan.updateLoanSummaryDerivedFields();
}
if (loan.getLoanProduct().isMultiDisburseLoan()) {
@@ -223,8 +223,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
loanDownPaymentTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(downPaymentTransaction.getTransactionDate(),
holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
handleRepaymentOrRecoveryOrWaiverTransaction(loan, downPaymentTransaction, loan.getLoanLifecycleStateMachine(), null,
scheduleGeneratorDTO);
handleRepaymentOrRecoveryOrWaiverTransaction(loan, downPaymentTransaction, null, scheduleGeneratorDTO);
return downPaymentTransaction;
} else {
return null;
@@ -21,7 +21,6 @@ package org.apache.fineract.portfolio.loanaccount.service;
import java.time.LocalDate;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
@@ -35,9 +34,9 @@ public class LoanRefundService {
private final LoanRefundValidator loanRefundValidator;
private final LoanTransactionProcessingService loadTransactionProcessingService;
private final LoanLifecycleStateMachine loanLifecycleStateMachine;
public void makeRefund(final Loan loan, final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
public void makeRefund(final Loan loan, final LoanTransaction loanTransaction, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
@@ -49,8 +48,7 @@ public class LoanRefundService {
if (loanTransaction.isNotZero()) {
loan.addLoanTransaction(loanTransaction);
}
loan.updateLoanSummaryDerivedFields();
loan.doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
loanLifecycleStateMachine.determineAndTransition(loan, loanTransaction.getTransactionDate());
}
public LocalDate extractTransactionDate(final Loan loan, final LoanTransaction loanTransaction) {
@@ -59,18 +57,16 @@ public class LoanRefundService {
return loanTransactionDate;
}
public void makeRefundForActiveLoan(final Loan loan, final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
public void makeRefundForActiveLoan(final Loan loan, final LoanTransaction loanTransaction, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
handleRefundTransaction(loan, loanTransaction, loanLifecycleStateMachine);
handleRefundTransaction(loan, loanTransaction);
}
public void creditBalanceRefund(final Loan loan, final LoanTransaction newCreditBalanceRefundTransaction,
final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) {
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) {
loanRefundValidator.validateCreditBalanceRefund(loan, newCreditBalanceRefundTransaction);
existingTransactionIds.addAll(loan.findExistingTransactionIds());
@@ -78,17 +74,10 @@ public class LoanRefundService {
loan.getLoanTransactions().add(newCreditBalanceRefundTransaction);
loan.updateLoanSummaryDerivedFields();
if (MathUtil.isEmpty(loan.getTotalOverpaid())) {
loan.setOverpaidOnDate(null);
loan.setClosedOnDate(newCreditBalanceRefundTransaction.getTransactionDate());
defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_CREDIT_BALANCE_REFUND, loan);
}
loanLifecycleStateMachine.determineAndTransition(loan, newCreditBalanceRefundTransaction.getTransactionDate());
}
private void handleRefundTransaction(final Loan loan, final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
private void handleRefundTransaction(final Loan loan, final LoanTransaction loanTransaction) {
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REFUND, loan);
loanTransaction.updateLoan(loan);
@@ -110,7 +99,6 @@ public class LoanRefundService {
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
loan.updateLoanSummaryDerivedFields();
loan.doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
loanLifecycleStateMachine.determineAndTransition(loan, loanTransaction.getTransactionDate());
}
}