FINERACT-2080: Extract loanLifecycleStateMachine from Loan entity, refactor transition functionality
This commit is contained in:
committed by
Adam Saghy
parent
79c006774e
commit
64dddfca48
+112
-5
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-150
@@ -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
@@ -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);
|
||||
|
||||
}
|
||||
|
||||
-2
@@ -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);
|
||||
|
||||
+42
-7
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-3
@@ -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);
|
||||
}
|
||||
|
||||
+6
-7
@@ -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;
|
||||
|
||||
+9
-21
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user