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; package org.apache.fineract.portfolio.loanaccount.domain;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; 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.domain.loan.LoanStatusChangedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
// TODO: introduce tests for the state machine // TODO: introduce tests for the state machine
@Slf4j
@Component @Component
@RequiredArgsConstructor @RequiredArgsConstructor
public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachine { 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 static final List<LoanStatus> ALLOWED_LOAN_STATUSES = List.of(LoanStatus.values());
private final BusinessEventNotifierService businessEventNotifierService; private final BusinessEventNotifierService businessEventNotifierService;
@@ -46,7 +46,29 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
@Override @Override
public void transition(final LoanEvent loanEvent, final Loan loan) { public void transition(final LoanEvent loanEvent, final Loan loan) {
loan.updateLoanSummaryDerivedFields(); 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 oldStatus = loan.getStatus();
LoanStatus newStatus = getNextStatus(loanEvent, loan); LoanStatus newStatus = getNextStatus(loanEvent, loan);
if (newStatus != null) { if (newStatus != null) {
@@ -58,7 +80,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
} }
// set mandatory field states based on new status after the transition // 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) { switch (newStatus) {
case SUBMITTED_AND_PENDING_APPROVAL -> { case SUBMITTED_AND_PENDING_APPROVAL -> {
loan.setApprovedOnDate(null); loan.setApprovedOnDate(null);
@@ -276,4 +298,89 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
return allowed; 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.Collection;
import java.util.Comparator; import java.util.Comparator;
import java.util.HashSet; import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.ListIterator; import java.util.ListIterator;
import java.util.Map;
import java.util.Objects; import java.util.Objects;
import java.util.Optional; import java.util.Optional;
import java.util.Set; 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.common.domain.PeriodFrequencyType;
import org.apache.fineract.portfolio.fund.domain.Fund; import org.apache.fineract.portfolio.fund.domain.Fund;
import org.apache.fineract.portfolio.group.domain.Group; 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.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; 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.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes; 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.rate.domain.Rate;
import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks;
import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.useradministration.domain.AppUser;
@@ -298,6 +294,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
@Column(name = "loan_product_counter") @Column(name = "loan_product_counter")
private Integer loanProductCounter; private Integer loanProductCounter;
@Setter
@OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY) @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private Set<LoanCharge> charges = new HashSet<>(); 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) @OneToMany(cascade = CascadeType.ALL, mappedBy = "loan", orphanRemoval = true, fetch = FetchType.LAZY)
private List<LoanTransaction> loanTransactions = new ArrayList<>(); private List<LoanTransaction> loanTransactions = new ArrayList<>();
@Setter
@Embedded @Embedded
private LoanSummary summary; private LoanSummary summary;
@Transient @Transient
private boolean accountNumberRequiresAutoGeneration; private boolean accountNumberRequiresAutoGeneration;
@Transient
private LoanLifecycleStateMachine loanLifecycleStateMachine;
@Setter() @Setter()
@Column(name = "principal_amount_proposed", scale = 6, precision = 19, nullable = false) @Column(name = "principal_amount_proposed", scale = 6, precision = 19, nullable = false)
private BigDecimal proposedPrincipal; private BigDecimal proposedPrincipal;
@@ -638,40 +633,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return collateral; 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 * 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. * 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) { public LoanDisbursementDetails addLoanDisbursementDetails(final LocalDate expectedDisbursementDate, final BigDecimal principal) {
final LocalDate actualDisbursementDate = null; final LoanDisbursementDetails disbursementDetails = new LoanDisbursementDetails(expectedDisbursementDate, null, principal,
final LoanDisbursementDetails disbursementDetails = new LoanDisbursementDetails(expectedDisbursementDate, actualDisbursementDate, this.netDisbursalAmount, false);
principal, this.netDisbursalAmount, false);
disbursementDetails.updateLoan(this); disbursementDetails.updateLoan(this);
this.disbursementDetails.add(disbursementDetails); this.disbursementDetails.add(disbursementDetails);
return disbursementDetails; return disbursementDetails;
@@ -992,11 +952,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
} }
} }
public void updateLoanSummaryAndStatus() {
updateLoanSummaryDerivedFields();
doPostLoanTransactionChecks(getLastUserTransactionDate(), loanLifecycleStateMachine);
}
private boolean isInterestRecalculationEnabledForProduct() { private boolean isInterestRecalculationEnabledForProduct() {
return this.loanProduct.isInterestRecalculationEnabled(); return this.loanProduct.isInterestRecalculationEnabled();
} }
@@ -1111,35 +1066,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return maturityDate; 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() { public List<Long> findExistingTransactionIds() {
return getLoanTransactions().stream() // return getLoanTransactions().stream() //
.filter(loanTransaction -> loanTransaction.getId() != null) // .filter(loanTransaction -> loanTransaction.getId() != null) //
@@ -1161,18 +1087,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.collect(Collectors.toList()); .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() { public Collection<LoanDisbursementDetails> fetchUndisbursedDetail() {
Collection<LoanDisbursementDetails> disbursementDetails = new ArrayList<>(); Collection<LoanDisbursementDetails> disbursementDetails = new ArrayList<>();
LocalDate date = null; LocalDate date = null;
@@ -1304,60 +1218,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); .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) { public boolean isChronologicallyLatestRepaymentOrWaiver(final LoanTransaction loanTransaction) {
boolean isChronologicallyLatestRepaymentOrWaiver = true; boolean isChronologicallyLatestRepaymentOrWaiver = true;
@@ -1852,10 +1712,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return receivableInterest; return receivableInterest;
} }
public void setHelpers(final LoanLifecycleStateMachine loanLifecycleStateMachine) {
this.loanLifecycleStateMachine = loanLifecycleStateMachine;
}
public boolean isSyncDisbursementWithMeeting() { public boolean isSyncDisbursementWithMeeting() {
return this.syncDisbursementWithMeeting != null && this.syncDisbursementWithMeeting; return this.syncDisbursementWithMeeting != null && this.syncDisbursementWithMeeting;
} }
@@ -2486,10 +2342,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
Money[] receivables = retrieveIncomeOutstandingTillDate(closureDate); Money[] receivables = retrieveIncomeOutstandingTillDate(closureDate);
Money totalPrincipal = Money.of(getCurrency(), this.getSummary().getTotalPrincipalOutstanding()); Money totalPrincipal = Money.of(getCurrency(), this.getSummary().getTotalPrincipalOutstanding());
totalPrincipal = totalPrincipal.minus(receivables[3]); totalPrincipal = totalPrincipal.minus(receivables[3]);
final Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = null;
final LocalDate currentDate = DateUtils.getBusinessLocalDate(); final LocalDate currentDate = DateUtils.getBusinessLocalDate();
return new LoanRepaymentScheduleInstallment(null, 0, currentDate, currentDate, totalPrincipal.getAmount(), 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) { private Money[] retrieveIncomeOutstandingTillDate(final LocalDate paymentDate) {
@@ -18,9 +18,27 @@
*/ */
package org.apache.fineract.portfolio.loanaccount.domain; package org.apache.fineract.portfolio.loanaccount.domain;
import java.time.LocalDate;
public interface LoanLifecycleStateMachine { public interface LoanLifecycleStateMachine {
LoanStatus dryTransition(LoanEvent loanEvent, Loan loan); LoanStatus dryTransition(LoanEvent loanEvent, Loan loan);
void transition(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); Loan assembleFrom(JsonCommand command);
void setHelpers(Loan loanAccount);
void accountNumberGeneration(JsonCommand command, Loan loan); void accountNumberGeneration(JsonCommand command, Loan loan);
CodeValue findCodeValueByIdIfProvided(Long codeValueId); 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.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; 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.MoneyHolder;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
@@ -41,6 +42,7 @@ public class LoanChargeService {
private final LoanChargeValidator loanChargeValidator; private final LoanChargeValidator loanChargeValidator;
private final LoanTransactionProcessingService loanTransactionProcessingService; private final LoanTransactionProcessingService loanTransactionProcessingService;
private final LoanLifecycleStateMachine loanLifecycleStateMachine;
public void recalculateAllCharges(final Loan loan) { public void recalculateAllCharges(final Loan loan) {
Set<LoanCharge> charges = loan.getActiveCharges(); Set<LoanCharge> charges = loan.getActiveCharges();
@@ -76,9 +78,8 @@ public class LoanChargeService {
} }
} }
public void makeChargePayment(final Loan loan, final Long chargeId, final LoanLifecycleStateMachine loanLifecycleStateMachine, public void makeChargePayment(final Loan loan, final Long chargeId, final List<Long> existingTransactionIds,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, final List<Long> existingReversedTransactionIds, final LoanTransaction paymentTransaction, final Integer installmentNumber) {
final LoanTransaction paymentTransaction, final Integer installmentNumber) {
loanChargeValidator.validateChargePaymentNotInFuture(paymentTransaction); loanChargeValidator.validateChargePaymentNotInFuture(paymentTransaction);
existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
@@ -88,11 +89,11 @@ public class LoanChargeService {
charge = loanCharge; 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, public void handleChargePaidTransaction(final Loan loan, final LoanCharge charge, final LoanTransaction chargesPayment,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final Integer installmentNumber) { final Integer installmentNumber) {
chargesPayment.updateLoan(loan); chargesPayment.updateLoan(loan);
final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge,
chargesPayment.getAmount(loan.getCurrency()).getAmount(), installmentNumber); chargesPayment.getAmount(loan.getCurrency()).getAmount(), installmentNumber);
@@ -118,7 +119,41 @@ public class LoanChargeService {
new TransactionCtx(loan.getCurrency(), chargePaymentInstallments, loanCharges, new TransactionCtx(loan.getCurrency(), chargePaymentInstallments, loanCharges,
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
loan.updateLoanSummaryDerivedFields(); loanLifecycleStateMachine.determineAndTransition(loan, chargesPayment.getTransactionDate());
loan.doPostLoanTransactionChecks(chargesPayment.getTransactionDate(), loanLifecycleStateMachine);
} }
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.infrastructure.core.api.JsonCommand;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
public interface LoanDownPaymentHandlerService { public interface LoanDownPaymentHandlerService {
@@ -30,6 +29,5 @@ public interface LoanDownPaymentHandlerService {
LoanTransaction disbursementTransaction, Loan loan); LoanTransaction disbursementTransaction, Loan loan);
void handleRepaymentOrRecoveryOrWaiverTransaction(Loan loan, LoanTransaction newTransactionDetail, void handleRepaymentOrRecoveryOrWaiverTransaction(Loan loan, LoanTransaction newTransactionDetail,
LoanLifecycleStateMachine loanLifecycleStateMachine, LoanTransaction transactionForAdjustment, LoanTransaction transactionForAdjustment, ScheduleGeneratorDTO scheduleGeneratorDTO);
ScheduleGeneratorDTO scheduleGeneratorDTO);
} }
@@ -61,6 +61,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
private final LoanRefundValidator loanRefundValidator; private final LoanRefundValidator loanRefundValidator;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService; private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanTransactionProcessingService loanTransactionProcessingService; private final LoanTransactionProcessingService loanTransactionProcessingService;
private final LoanLifecycleStateMachine loanLifecycleStateMachine;
@Override @Override
public LoanTransaction handleDownPayment(ScheduleGeneratorDTO scheduleGeneratorDTO, JsonCommand command, public LoanTransaction handleDownPayment(ScheduleGeneratorDTO scheduleGeneratorDTO, JsonCommand command,
@@ -77,8 +78,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
@Override @Override
public void handleRepaymentOrRecoveryOrWaiverTransaction(final Loan loan, final LoanTransaction loanTransaction, public void handleRepaymentOrRecoveryOrWaiverTransaction(final Loan loan, final LoanTransaction loanTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction, final LoanTransaction adjustedTransaction, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
final ScheduleGeneratorDTO scheduleGeneratorDTO) {
if (loanTransaction.isRecoveryRepayment()) { if (loanTransaction.isRecoveryRepayment()) {
loanLifecycleStateMachine.transition(LoanEvent.LOAN_RECOVERY_PAYMENT, loan); loanLifecycleStateMachine.transition(LoanEvent.LOAN_RECOVERY_PAYMENT, loan);
} }
@@ -158,13 +158,13 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
reprocessLoanTransactionsService.reprocessTransactions(loan); reprocessLoanTransactionsService.reprocessTransactions(loan);
} }
loan.updateLoanSummaryDerivedFields();
/** /**
* FIXME: Vishwas, skipping post loan transaction checks for Loan recoveries * FIXME: Vishwas, skipping post loan transaction checks for Loan recoveries
**/ **/
if (loanTransaction.isNotRecoveryRepayment()) { if (loanTransaction.isNotRecoveryRepayment()) {
loan.doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine); loanLifecycleStateMachine.determineAndTransition(loan, loanTransaction.getTransactionDate());
} else {
loan.updateLoanSummaryDerivedFields();
} }
if (loan.getLoanProduct().isMultiDisburseLoan()) { if (loan.getLoanProduct().isMultiDisburseLoan()) {
@@ -223,8 +223,7 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler
loanDownPaymentTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(downPaymentTransaction.getTransactionDate(), loanDownPaymentTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(downPaymentTransaction.getTransactionDate(),
holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay()); holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
handleRepaymentOrRecoveryOrWaiverTransaction(loan, downPaymentTransaction, loan.getLoanLifecycleStateMachine(), null, handleRepaymentOrRecoveryOrWaiverTransaction(loan, downPaymentTransaction, null, scheduleGeneratorDTO);
scheduleGeneratorDTO);
return downPaymentTransaction; return downPaymentTransaction;
} else { } else {
return null; return null;
@@ -21,7 +21,6 @@ package org.apache.fineract.portfolio.loanaccount.service;
import java.time.LocalDate; import java.time.LocalDate;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor; 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.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
@@ -35,9 +34,9 @@ public class LoanRefundService {
private final LoanRefundValidator loanRefundValidator; private final LoanRefundValidator loanRefundValidator;
private final LoanTransactionProcessingService loadTransactionProcessingService; private final LoanTransactionProcessingService loadTransactionProcessingService;
private final LoanLifecycleStateMachine loanLifecycleStateMachine;
public void makeRefund(final Loan loan, final LoanTransaction loanTransaction, public void makeRefund(final Loan loan, final LoanTransaction loanTransaction, final List<Long> existingTransactionIds,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) { final List<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
@@ -49,8 +48,7 @@ public class LoanRefundService {
if (loanTransaction.isNotZero()) { if (loanTransaction.isNotZero()) {
loan.addLoanTransaction(loanTransaction); loan.addLoanTransaction(loanTransaction);
} }
loan.updateLoanSummaryDerivedFields(); loanLifecycleStateMachine.determineAndTransition(loan, loanTransaction.getTransactionDate());
loan.doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
} }
public LocalDate extractTransactionDate(final Loan loan, final LoanTransaction loanTransaction) { public LocalDate extractTransactionDate(final Loan loan, final LoanTransaction loanTransaction) {
@@ -59,18 +57,16 @@ public class LoanRefundService {
return loanTransactionDate; return loanTransactionDate;
} }
public void makeRefundForActiveLoan(final Loan loan, final LoanTransaction loanTransaction, public void makeRefundForActiveLoan(final Loan loan, final LoanTransaction loanTransaction, final List<Long> existingTransactionIds,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds) { final List<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
handleRefundTransaction(loan, loanTransaction, loanLifecycleStateMachine); handleRefundTransaction(loan, loanTransaction);
} }
public void creditBalanceRefund(final Loan loan, final LoanTransaction newCreditBalanceRefundTransaction, public void creditBalanceRefund(final Loan loan, final LoanTransaction newCreditBalanceRefundTransaction,
final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, final List<Long> existingTransactionIds, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) {
final List<Long> existingReversedTransactionIds) {
loanRefundValidator.validateCreditBalanceRefund(loan, newCreditBalanceRefundTransaction); loanRefundValidator.validateCreditBalanceRefund(loan, newCreditBalanceRefundTransaction);
existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingTransactionIds.addAll(loan.findExistingTransactionIds());
@@ -78,17 +74,10 @@ public class LoanRefundService {
loan.getLoanTransactions().add(newCreditBalanceRefundTransaction); loan.getLoanTransactions().add(newCreditBalanceRefundTransaction);
loan.updateLoanSummaryDerivedFields(); loanLifecycleStateMachine.determineAndTransition(loan, newCreditBalanceRefundTransaction.getTransactionDate());
if (MathUtil.isEmpty(loan.getTotalOverpaid())) {
loan.setOverpaidOnDate(null);
loan.setClosedOnDate(newCreditBalanceRefundTransaction.getTransactionDate());
defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_CREDIT_BALANCE_REFUND, loan);
}
} }
private void handleRefundTransaction(final Loan loan, final LoanTransaction loanTransaction, private void handleRefundTransaction(final Loan loan, final LoanTransaction loanTransaction) {
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REFUND, loan); loanLifecycleStateMachine.transition(LoanEvent.LOAN_REFUND, loan);
loanTransaction.updateLoan(loan); loanTransaction.updateLoan(loan);
@@ -110,7 +99,6 @@ public class LoanRefundService {
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
loan.updateLoanSummaryDerivedFields(); loanLifecycleStateMachine.determineAndTransition(loan, loanTransaction.getTransactionDate());
loan.doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine);
} }
} }
@@ -211,12 +211,12 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
@Override @Override
@Transactional @Transactional
public void reverseTransfersWithFromAccountType(final Long accountNumber, final PortfolioAccountType accountTypeId) { public void reverseTransfersWithFromAccountType(final Long accountNumber, final PortfolioAccountType accountTypeId) {
List<AccountTransferTransaction> acccountTransfers = null; List<AccountTransferTransaction> accountTransfers = null;
if (accountTypeId.isLoanAccount()) { if (accountTypeId.isLoanAccount()) {
acccountTransfers = this.accountTransferRepository.findByFromLoanId(accountNumber); accountTransfers = this.accountTransferRepository.findByFromLoanId(accountNumber);
} }
if (acccountTransfers != null && acccountTransfers.size() > 0) { if (accountTransfers != null && !accountTransfers.isEmpty()) {
undoTransactions(acccountTransfers); undoTransactions(accountTransfers);
} }
} }
@@ -225,35 +225,31 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
@Transactional @Transactional
public void reverseTransfersWithFromAccountTransactions(final Collection<Long> fromTransactionIds, public void reverseTransfersWithFromAccountTransactions(final Collection<Long> fromTransactionIds,
final PortfolioAccountType accountTypeId) { final PortfolioAccountType accountTypeId) {
List<AccountTransferTransaction> acccountTransfers = new ArrayList<>(); List<AccountTransferTransaction> accountTransfers = new ArrayList<>();
if (accountTypeId.isLoanAccount()) { if (accountTypeId.isLoanAccount()) {
List<List<Long>> partitions = Lists.partition(fromTransactionIds.stream().toList(), List<List<Long>> partitions = Lists.partition(fromTransactionIds.stream().toList(),
fineractProperties.getQuery().getInClauseParameterSizeLimit()); fineractProperties.getQuery().getInClauseParameterSizeLimit());
partitions.forEach(partition -> acccountTransfers.addAll(this.accountTransferRepository.findByFromLoanTransactions(partition))); partitions.forEach(partition -> accountTransfers.addAll(this.accountTransferRepository.findByFromLoanTransactions(partition)));
} }
if (acccountTransfers.size() > 0) { if (!accountTransfers.isEmpty()) {
undoTransactions(acccountTransfers); undoTransactions(accountTransfers);
} }
} }
@Override @Override
@Transactional @Transactional
public void reverseAllTransactions(final Long accountId, final PortfolioAccountType accountTypeId) { public void reverseAllTransactions(final Long accountId, final PortfolioAccountType accountTypeId) {
List<AccountTransferTransaction> acccountTransfers = null; List<AccountTransferTransaction> accountTransfers = null;
if (accountTypeId.isLoanAccount()) { if (accountTypeId.isLoanAccount()) {
acccountTransfers = this.accountTransferRepository.findAllByLoanId(accountId); accountTransfers = this.accountTransferRepository.findAllByLoanId(accountId);
} }
if (acccountTransfers != null && acccountTransfers.size() > 0) { if (accountTransfers != null && !accountTransfers.isEmpty()) {
undoTransactions(acccountTransfers); undoTransactions(accountTransfers);
} }
} }
/** private void undoTransactions(final List<AccountTransferTransaction> accountTransfers) {
* @param acccountTransfers for (final AccountTransferTransaction accountTransfer : accountTransfers) {
*/
private void undoTransactions(final List<AccountTransferTransaction> acccountTransfers) {
for (final AccountTransferTransaction accountTransfer : acccountTransfers) {
if (accountTransfer.getFromLoanTransaction() != null) { if (accountTransfer.getFromLoanTransaction() != null) {
this.loanAccountDomainService.reverseTransfer(accountTransfer.getFromLoanTransaction()); this.loanAccountDomainService.reverseTransfer(accountTransfer.getFromLoanTransaction());
} }
@@ -298,14 +294,12 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId()); toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId());
} else { } else {
toLoanAccount = accountTransferDTO.getLoan(); toLoanAccount = accountTransferDTO.getLoan();
this.loanAccountAssembler.setHelpers(toLoanAccount);
} }
} else { } else {
fromSavingsAccount = accountTransferDetails.fromSavingsAccount(); fromSavingsAccount = accountTransferDetails.fromSavingsAccount();
this.savingsAccountAssembler.setHelpers(fromSavingsAccount); this.savingsAccountAssembler.setHelpers(fromSavingsAccount);
toLoanAccount = accountTransferDetails.toLoanAccount(); toLoanAccount = accountTransferDetails.toLoanAccount();
this.loanAccountAssembler.setHelpers(toLoanAccount);
} }
final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer, final SavingsTransactionBooleanValues transactionBooleanValues = new SavingsTransactionBooleanValues(isAccountTransfer,
@@ -414,12 +408,10 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
fromLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId()); fromLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId());
} else { } else {
fromLoanAccount = accountTransferDTO.getLoan(); fromLoanAccount = accountTransferDTO.getLoan();
this.loanAccountAssembler.setHelpers(fromLoanAccount);
} }
toSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId(), backdatedTxnsAllowedTill); toSavingsAccount = this.savingsAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId(), backdatedTxnsAllowedTill);
} else { } else {
fromLoanAccount = accountTransferDetails.fromLoanAccount(); fromLoanAccount = accountTransferDetails.fromLoanAccount();
this.loanAccountAssembler.setHelpers(fromLoanAccount);
toSavingsAccount = accountTransferDetails.toSavingsAccount(); toSavingsAccount = accountTransferDetails.toSavingsAccount();
this.savingsAccountAssembler.setHelpers(toSavingsAccount); this.savingsAccountAssembler.setHelpers(toSavingsAccount);
} }
@@ -472,14 +464,12 @@ public class AccountTransfersWritePlatformServiceImpl implements AccountTransfer
fromLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId()); fromLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getFromAccountId());
} else { } else {
fromLoanAccount = accountTransferDTO.getFromLoan(); fromLoanAccount = accountTransferDTO.getFromLoan();
this.loanAccountAssembler.setHelpers(fromLoanAccount);
} }
Loan toLoanAccount = null; Loan toLoanAccount = null;
if (accountTransferDTO.getToLoan() == null) { if (accountTransferDTO.getToLoan() == null) {
toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId()); toLoanAccount = this.loanAccountAssembler.assembleFrom(accountTransferDTO.getToAccountId());
} else { } else {
toLoanAccount = accountTransferDTO.getToLoan(); toLoanAccount = accountTransferDTO.getToLoan();
this.loanAccountAssembler.setHelpers(toLoanAccount);
} }
ExternalId externalIdForDisbursement = accountTransferDTO.getTxnExternalId(); ExternalId externalIdForDisbursement = accountTransferDTO.getTxnExternalId();
@@ -141,7 +141,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final PostDatedChecksRepository postDatedChecksRepository; private final PostDatedChecksRepository postDatedChecksRepository;
private final LoanCollateralManagementRepository loanCollateralManagementRepository; private final LoanCollateralManagementRepository loanCollateralManagementRepository;
private final DelinquencyWritePlatformService delinquencyWritePlatformService; private final DelinquencyWritePlatformService delinquencyWritePlatformService;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final LoanLifecycleStateMachine loanLifecycleStateMachine;
private final ExternalIdFactory externalIdFactory; private final ExternalIdFactory externalIdFactory;
private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService; private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService;
private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper; private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
@@ -259,8 +259,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
loanDownPaymentTransactionValidator.validateRepaymentTypeAccountStatus(loan, newRepaymentTransaction, event); loanDownPaymentTransactionValidator.validateRepaymentTypeAccountStatus(loan, newRepaymentTransaction, event);
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, event, loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, event,
newRepaymentTransaction.getTransactionDate()); newRepaymentTransaction.getTransactionDate());
makeRepayment(loan, newRepaymentTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, makeRepayment(loan, newRepaymentTransaction, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO);
existingReversedTransactionIds, scheduleGeneratorDTO);
if (loan.isInterestBearingAndInterestRecalculationEnabled()) { if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan);
@@ -421,8 +420,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
LoanEvent.LOAN_CHARGE_PAYMENT); LoanEvent.LOAN_CHARGE_PAYMENT);
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_CHARGE_PAYMENT, loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_CHARGE_PAYMENT,
newPaymentTransaction.getTransactionDate()); newPaymentTransaction.getTransactionDate());
loanChargeService.makeChargePayment(loan, chargeId, defaultLoanLifecycleStateMachine, existingTransactionIds, loanChargeService.makeChargePayment(loan, chargeId, existingTransactionIds, existingReversedTransactionIds,
existingReversedTransactionIds, newPaymentTransaction, installmentNumber); newPaymentTransaction, installmentNumber);
} }
loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(newPaymentTransaction); loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(newPaymentTransaction);
loanAccountService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan); loanAccountService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
@@ -520,8 +519,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newRefundTransaction.getTransactionDate(), workingDays, loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newRefundTransaction.getTransactionDate(), workingDays,
allowTransactionsOnNonWorkingDay); allowTransactionsOnNonWorkingDay);
loanRefundService.makeRefund(loan, newRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, loanRefundService.makeRefund(loan, newRefundTransaction, existingTransactionIds, existingReversedTransactionIds);
existingReversedTransactionIds);
loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(newRefundTransaction); loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(newRefundTransaction);
this.loanRepositoryWrapper.saveAndFlush(loan); this.loanRepositoryWrapper.saveAndFlush(loan);
@@ -655,8 +653,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_CREDIT_BALANCE_REFUND); loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_CREDIT_BALANCE_REFUND);
loanTransactionValidator.validateRefundDateIsAfterLastRepayment(loan, newCreditBalanceRefundTransaction.getTransactionDate()); loanTransactionValidator.validateRefundDateIsAfterLastRepayment(loan, newCreditBalanceRefundTransaction.getTransactionDate());
loanRefundService.creditBalanceRefund(loan, newCreditBalanceRefundTransaction, defaultLoanLifecycleStateMachine, loanRefundService.creditBalanceRefund(loan, newCreditBalanceRefundTransaction, existingTransactionIds,
existingTransactionIds, existingReversedTransactionIds); existingReversedTransactionIds);
newCreditBalanceRefundTransaction = this.loanTransactionRepository.saveAndFlush(newCreditBalanceRefundTransaction); newCreditBalanceRefundTransaction = this.loanTransactionRepository.saveAndFlush(newCreditBalanceRefundTransaction);
@@ -709,8 +707,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
allowTransactionsOnNonWorkingDay); allowTransactionsOnNonWorkingDay);
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_REFUND, loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_REFUND,
newRefundTransaction.getTransactionDate()); newRefundTransaction.getTransactionDate());
loanRefundService.makeRefundForActiveLoan(loan, newRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, loanRefundService.makeRefundForActiveLoan(loan, newRefundTransaction, existingTransactionIds, existingReversedTransactionIds);
existingReversedTransactionIds);
this.loanTransactionRepository.saveAndFlush(newRefundTransaction); this.loanTransactionRepository.saveAndFlush(newRefundTransaction);
@@ -777,7 +774,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
loanForeclosureValidator.validateForForeclosure(loan, payment.getTransactionDate()); loanForeclosureValidator.validateForForeclosure(loan, payment.getTransactionDate());
} }
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_FORECLOSURE); loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_FORECLOSURE);
handleForeClosureTransactions(loan, payment, defaultLoanLifecycleStateMachine, scheduleGeneratorDTO); handleForeClosureTransactions(loan, payment, scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.isInterestBearingAndInterestRecalculationEnabled()) { if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
@@ -918,8 +915,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
} }
} }
loan.updateLoanSummaryDerivedFields(); loanLifecycleStateMachine.determineAndTransition(loan, transactionDate);
loan.doPostLoanTransactionChecks(transactionDate, defaultLoanLifecycleStateMachine);
switch (loanTransactionType) { switch (loanTransactionType) {
case MERCHANT_ISSUED_REFUND -> businessEventNotifierService case MERCHANT_ISSUED_REFUND -> businessEventNotifierService
@@ -1062,21 +1058,19 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
} }
@SuppressWarnings("null") @SuppressWarnings("null")
private void makeRepayment(final Loan loan, final LoanTransaction repaymentTransaction, private void makeRepayment(final Loan loan, final LoanTransaction repaymentTransaction, final List<Long> existingTransactionIds,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final ScheduleGeneratorDTO scheduleGeneratorDTO) { final List<Long> existingReversedTransactionIds, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loan, repaymentTransaction, "created"); loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loan, repaymentTransaction, "created");
existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, repaymentTransaction, loanLifecycleStateMachine, loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, repaymentTransaction, null, scheduleGeneratorDTO);
null, scheduleGeneratorDTO);
} }
private void handleForeClosureTransactions(final Loan loan, final LoanTransaction repaymentTransaction, private void handleForeClosureTransactions(final Loan loan, final LoanTransaction repaymentTransaction,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final ScheduleGeneratorDTO scheduleGeneratorDTO) { final ScheduleGeneratorDTO scheduleGeneratorDTO) {
loan.setLoanSubStatus(LoanSubStatus.FORECLOSED); loan.setLoanSubStatus(LoanSubStatus.FORECLOSED);
loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, repaymentTransaction, loanLifecycleStateMachine, loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, repaymentTransaction, null, scheduleGeneratorDTO);
null, scheduleGeneratorDTO);
} }
} }
@@ -160,7 +160,7 @@ public class LoanScheduleAssembler {
private final LoanUtilService loanUtilService; private final LoanUtilService loanUtilService;
private final LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler; private final LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler;
private final LoanRepositoryWrapper loanRepositoryWrapper; private final LoanRepositoryWrapper loanRepositoryWrapper;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final LoanLifecycleStateMachine loanLifecycleStateMachine;
private final LoanAccrualsProcessingService loanAccrualsProcessingService; private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final LoanDisbursementService loanDisbursementService; private final LoanDisbursementService loanDisbursementService;
private final LoanChargeService loanChargeService; private final LoanChargeService loanChargeService;
@@ -1479,7 +1479,7 @@ public class LoanScheduleAssembler {
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
final Map<String, Object> actualChanges = new HashMap<>(); final Map<String, Object> actualChanges = new HashMap<>();
defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVED, loan); loanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVED, loan);
actualChanges.put(PARAM_STATUS, LoanEnumerations.status(loan.getStatus())); actualChanges.put(PARAM_STATUS, LoanEnumerations.status(loan.getStatus()));
LocalDate approvedOn = command.localDateValueOfParameterNamed(APPROVED_ON_DATE); LocalDate approvedOn = command.localDateValueOfParameterNamed(APPROVED_ON_DATE);
@@ -35,7 +35,6 @@ import org.apache.fineract.portfolio.loanaccount.data.OutstandingAmountsDTO;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
@@ -63,7 +62,6 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC
private final CurrencyReadPlatformService currencyReadPlatformService; private final CurrencyReadPlatformService currencyReadPlatformService;
private final LoanUtilService loanUtilService; private final LoanUtilService loanUtilService;
private final LoanRepositoryWrapper loanRepository; private final LoanRepositoryWrapper loanRepository;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
private final LoanTermVariationsMapper loanTermVariationsMapper; private final LoanTermVariationsMapper loanTermVariationsMapper;
@Override @Override
@@ -204,16 +202,12 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC
} }
private LoanApplicationTerms constructLoanApplicationTerms(final Loan loan) { private LoanApplicationTerms constructLoanApplicationTerms(final Loan loan) {
final LocalDate recalculateFrom = null; final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null);
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
return loanTermVariationsMapper.constructLoanApplicationTerms(scheduleGeneratorDTO, loan); return loanTermVariationsMapper.constructLoanApplicationTerms(scheduleGeneratorDTO, loan);
} }
private Loan fetchLoan(final Long accountId) { private Loan fetchLoan(final Long accountId) {
final Loan loanAccount = this.loanRepository.findOneWithNotFoundDetection(accountId, true); return loanRepository.findOneWithNotFoundDetection(accountId, true);
loanAccount.setHelpers(defaultLoanLifecycleStateMachine);
return loanAccount;
} }
} }
@@ -30,6 +30,7 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.codes.domain.CodeValue;
import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper; import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper;
@@ -51,7 +52,6 @@ import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData; import org.apache.fineract.portfolio.loanaccount.data.LoanTermVariationsData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
@@ -84,8 +84,6 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService; import org.apache.fineract.portfolio.loanaccount.service.ReprocessLoanTransactionsService;
import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent; import org.apache.fineract.portfolio.loanaccount.service.schedule.LoanScheduleComponent;
import org.apache.fineract.useradministration.domain.AppUser; import org.apache.fineract.useradministration.domain.AppUser;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DataIntegrityViolationException; import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.dao.NonTransientDataAccessException; import org.springframework.dao.NonTransientDataAccessException;
@@ -93,11 +91,11 @@ import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
@Slf4j
@Service @Service
@RequiredArgsConstructor @RequiredArgsConstructor
public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanRescheduleRequestWritePlatformService { public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanRescheduleRequestWritePlatformService {
private static final Logger LOG = LoggerFactory.getLogger(LoanRescheduleRequestWritePlatformServiceImpl.class);
private static final DefaultScheduledDateGenerator DEFAULT_SCHEDULED_DATE_GENERATOR = new DefaultScheduledDateGenerator(); private static final DefaultScheduledDateGenerator DEFAULT_SCHEDULED_DATE_GENERATOR = new DefaultScheduledDateGenerator();
private final CodeValueRepositoryWrapper codeValueRepositoryWrapper; private final CodeValueRepositoryWrapper codeValueRepositoryWrapper;
@@ -258,17 +256,15 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
if (rescheduleFromDate != null && endDate != null && emi != null) { if (rescheduleFromDate != null && endDate != null && emi != null) {
LoanTermVariations parent = null; LoanTermVariations parent = null;
LocalDate rescheduleFromLocDate = rescheduleFromDate;
LocalDate endDateLocDate = endDate;
final Integer termType = LoanTermVariationType.EMI_AMOUNT.getValue(); final Integer termType = LoanTermVariationType.EMI_AMOUNT.getValue();
List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments(); List<LoanRepaymentScheduleInstallment> installments = loan.getRepaymentScheduleInstallments();
for (LoanRepaymentScheduleInstallment installment : installments) { for (LoanRepaymentScheduleInstallment installment : installments) {
if (!DateUtils.isBefore(installment.getDueDate(), rescheduleFromLocDate) if (!DateUtils.isBefore(installment.getDueDate(), rescheduleFromDate)
&& !DateUtils.isAfter(installment.getDueDate(), endDateLocDate)) { && !DateUtils.isAfter(installment.getDueDate(), endDate)) {
createLoanTermVariations(loanRescheduleRequest, termType, loan, installment.getDueDate(), installment.getDueDate(), createLoanTermVariations(loanRescheduleRequest, termType, loan, installment.getDueDate(), installment.getDueDate(),
loanRescheduleRequestToTermVariationMappings, isActive, true, emi, parent); loanRescheduleRequestToTermVariationMappings, isActive, true, emi, parent);
} }
if (DateUtils.isAfter(installment.getDueDate(), endDateLocDate)) { if (DateUtils.isAfter(installment.getDueDate(), endDate)) {
break; break;
} }
} }
@@ -430,8 +426,6 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
.determineProcessor(loan.transactionProcessingStrategy()); .determineProcessor(loan.transactionProcessingStrategy());
final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getLoanScheduleType(), final LoanScheduleGenerator loanScheduleGenerator = this.loanScheduleFactory.create(loanApplicationTerms.getLoanScheduleType(),
loanApplicationTerms.getInterestMethod()); loanApplicationTerms.getInterestMethod());
final LoanLifecycleStateMachine loanLifecycleStateMachine = null;
loan.setHelpers(loanLifecycleStateMachine);
final LoanScheduleDTO loanScheduleDTO = loanScheduleGenerator.rescheduleNextInstallments(mathContext, loanApplicationTerms, final LoanScheduleDTO loanScheduleDTO = loanScheduleGenerator.rescheduleNextInstallments(mathContext, loanApplicationTerms,
loan, loanApplicationTerms.getHolidayDetailDTO(), loanRepaymentScheduleTransactionProcessor, rescheduleFromDate); loan, loanApplicationTerms.getHolidayDetailDTO(), loanRepaymentScheduleTransactionProcessor, rescheduleFromDate);
@@ -565,7 +559,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche
* *
**/ **/
private void handleDataIntegrityViolation(final NonTransientDataAccessException dve) { private void handleDataIntegrityViolation(final NonTransientDataAccessException dve) {
LOG.error("Error occured.", dve); log.error("Error occurred.", dve);
throw ErrorHandler.getMappable(dve, "error.msg.loan.reschedule.unknown.data.integrity.issue", throw ErrorHandler.getMappable(dve, "error.msg.loan.reschedule.unknown.data.integrity.issue",
"Unknown data integrity issue with resource."); "Unknown data integrity issue with resource.");
} }
@@ -199,7 +199,7 @@ public final class LoanApplicationValidator {
private final WorkingDaysRepositoryWrapper workingDaysRepository; private final WorkingDaysRepositoryWrapper workingDaysRepository;
private final HolidayRepository holidayRepository; private final HolidayRepository holidayRepository;
private final SavingsAccountRepositoryWrapper savingsAccountRepository; private final SavingsAccountRepositoryWrapper savingsAccountRepository;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final LoanLifecycleStateMachine loanLifecycleStateMachine;
private final CalendarInstanceRepository calendarInstanceRepository; private final CalendarInstanceRepository calendarInstanceRepository;
private final LoanUtilService loanUtilService; private final LoanUtilService loanUtilService;
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService; private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
@@ -799,7 +799,6 @@ public final class LoanApplicationValidator {
private void fixedLengthValidations(final JsonElement element) { private void fixedLengthValidations(final JsonElement element) {
validateOrThrow("loan", baseDataValidator -> { validateOrThrow("loan", baseDataValidator -> {
boolean isInterestBearing = false;
final String transactionProcessingStrategy = this.fromApiJsonHelper final String transactionProcessingStrategy = this.fromApiJsonHelper
.extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element); .extractStringNamed(LoanApiConstants.transactionProcessingStrategyCodeParameterName, element);
final Integer numberOfRepayments = this.fromApiJsonHelper final Integer numberOfRepayments = this.fromApiJsonHelper
@@ -809,7 +808,7 @@ public final class LoanApplicationValidator {
final BigDecimal interestRatePerPeriod = this.fromApiJsonHelper final BigDecimal interestRatePerPeriod = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRatePerPeriodParameterName, element); .extractBigDecimalWithLocaleNamed(LoanApiConstants.interestRatePerPeriodParameterName, element);
isInterestBearing = interestRatePerPeriod != null && interestRatePerPeriod.compareTo(BigDecimal.ZERO) > 0; final boolean isInterestBearing = interestRatePerPeriod != null && interestRatePerPeriod.compareTo(BigDecimal.ZERO) > 0;
loanProductDataValidator.fixedLengthValidations(transactionProcessingStrategy, isInterestBearing, numberOfRepayments, loanProductDataValidator.fixedLengthValidations(transactionProcessingStrategy, isInterestBearing, numberOfRepayments,
repaymentEvery, element, baseDataValidator); repaymentEvery, element, baseDataValidator);
}); });
@@ -1267,7 +1266,7 @@ public final class LoanApplicationValidator {
final Set<String> supportedParameters = new HashSet<>(Arrays.asList(LoanApiConstants.idParameterName, final Set<String> supportedParameters = new HashSet<>(Arrays.asList(LoanApiConstants.idParameterName,
LoanApiConstants.clientCollateralIdParameterName, LoanApiConstants.quantityParameterName)); LoanApiConstants.clientCollateralIdParameterName, LoanApiConstants.quantityParameterName));
final JsonArray array = topLevelJsonElement.get(LoanApiConstants.collateralParameterName).getAsJsonArray(); final JsonArray array = topLevelJsonElement.get(LoanApiConstants.collateralParameterName).getAsJsonArray();
if (array.size() > 0) { if (!array.isEmpty()) {
BigDecimal totalAmount = BigDecimal.ZERO; BigDecimal totalAmount = BigDecimal.ZERO;
for (int i = 1; i <= array.size(); i++) { for (int i = 1; i <= array.size(); i++) {
final JsonObject collateralItemElement = array.get(i - 1).getAsJsonObject(); final JsonObject collateralItemElement = array.get(i - 1).getAsJsonObject();
@@ -1688,7 +1687,7 @@ public final class LoanApplicationValidator {
final JsonArray variationArray = this.fromApiJsonHelper.extractJsonArrayNamed(LoanApiConstants.disbursementDataParameterName, final JsonArray variationArray = this.fromApiJsonHelper.extractJsonArrayNamed(LoanApiConstants.disbursementDataParameterName,
element); element);
List<LocalDate> expectedDisbursementDates = new ArrayList<>(); List<LocalDate> expectedDisbursementDates = new ArrayList<>();
if (variationArray != null && variationArray.size() > 0) { if (variationArray != null && !variationArray.isEmpty()) {
if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isEqualAmortizationParam, element)) { if (this.fromApiJsonHelper.parameterExists(LoanApiConstants.isEqualAmortizationParam, element)) {
boolean isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isEqualAmortizationParam, boolean isEqualAmortization = this.fromApiJsonHelper.extractBooleanNamed(LoanApiConstants.isEqualAmortizationParam,
element); element);
@@ -1890,15 +1889,14 @@ public final class LoanApplicationValidator {
? this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, element) ? this.fromApiJsonHelper.extractLocalDateNamed(LoanApiConstants.expectedDisbursementDateParameterName, element)
: originalExpectedDisbursementDate; : originalExpectedDisbursementDate;
String defaultUserMessage = "";
if (DateUtils.isBefore(submittedOnDate, startDate)) { if (DateUtils.isBefore(submittedOnDate, startDate)) {
defaultUserMessage = "submittedOnDate cannot be before the loan product startDate."; String defaultUserMessage = "submittedOnDate cannot be before the loan product startDate.";
throw new LoanApplicationDateException("submitted.on.date.cannot.be.before.the.loan.product.start.date", defaultUserMessage, throw new LoanApplicationDateException("submitted.on.date.cannot.be.before.the.loan.product.start.date", defaultUserMessage,
submittedOnDate.toString(), startDate.toString()); submittedOnDate.toString(), startDate.toString());
} }
if (closeDate != null && DateUtils.isAfter(submittedOnDate, closeDate)) { if (closeDate != null && DateUtils.isAfter(submittedOnDate, closeDate)) {
defaultUserMessage = "submittedOnDate cannot be after the loan product closeDate."; String defaultUserMessage = "submittedOnDate cannot be after the loan product closeDate.";
throw new LoanApplicationDateException("submitted.on.date.cannot.be.after.the.loan.product.close.date", defaultUserMessage, throw new LoanApplicationDateException("submitted.on.date.cannot.be.after.the.loan.product.close.date", defaultUserMessage,
submittedOnDate.toString(), closeDate.toString()); submittedOnDate.toString(), closeDate.toString());
} }
@@ -2024,7 +2022,6 @@ public final class LoanApplicationValidator {
baseDataValidator.reset().parameter(LoanApiConstants.noteParameterName).value(note).notExceedingLengthOf(1000); baseDataValidator.reset().parameter(LoanApiConstants.noteParameterName).value(note).notExceedingLengthOf(1000);
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
loan.setHelpers(defaultLoanLifecycleStateMachine);
final Client client = loan.client(); final Client client = loan.client();
if (client != null && client.isNotActive()) { if (client != null && client.isNotActive()) {
@@ -2116,7 +2113,7 @@ public final class LoanApplicationValidator {
throw new InvalidLoanStateTransitionException("approval", "cannot.be.a.future.date", errorMessage, approvedOnDate); throw new InvalidLoanStateTransitionException("approval", "cannot.be.a.future.date", errorMessage, approvedOnDate);
} }
final LoanStatus newStatus = defaultLoanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_APPROVED, loan); final LoanStatus newStatus = loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_APPROVED, loan);
if (newStatus.hasStateOf(loan.getStatus())) { if (newStatus.hasStateOf(loan.getStatus())) {
final String defaultUserMessage = "Loan is already approved."; final String defaultUserMessage = "Loan is already approved.";
final ApiParameterError error = ApiParameterError final ApiParameterError error = ApiParameterError
@@ -18,6 +18,8 @@
*/ */
package org.apache.fineract.portfolio.loanaccount.service; package org.apache.fineract.portfolio.loanaccount.service;
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.APPROVED_ON_DATE;
import static org.apache.fineract.portfolio.loanaccount.domain.Loan.PARAM_STATUS;
import static org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType.SAME_AS_REPAYMENT_PERIOD; import static org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType.SAME_AS_REPAYMENT_PERIOD;
import com.google.gson.JsonArray; import com.google.gson.JsonArray;
@@ -29,6 +31,7 @@ import java.time.LocalDate;
import java.time.temporal.ChronoField; import java.time.temporal.ChronoField;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap; import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
@@ -75,7 +78,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement;
import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
@@ -87,6 +89,7 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationVa
import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator;
import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants;
import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType; import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType;
import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations;
import org.apache.fineract.portfolio.note.domain.Note; import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository; import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.savings.data.GroupSavingsIndividualMonitoringAccountData; import org.apache.fineract.portfolio.savings.data.GroupSavingsIndividualMonitoringAccountData;
@@ -108,7 +111,6 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
private final LoanRepositoryWrapper loanRepositoryWrapper; private final LoanRepositoryWrapper loanRepositoryWrapper;
private final NoteRepository noteRepository; private final NoteRepository noteRepository;
private final LoanAssembler loanAssembler; private final LoanAssembler loanAssembler;
private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory;
private final CalendarRepository calendarRepository; private final CalendarRepository calendarRepository;
private final CalendarInstanceRepository calendarInstanceRepository; private final CalendarInstanceRepository calendarInstanceRepository;
private final SavingsAccountRepositoryWrapper savingsAccountRepository; private final SavingsAccountRepositoryWrapper savingsAccountRepository;
@@ -121,7 +123,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
private final GLIMAccountInfoRepository glimRepository; private final GLIMAccountInfoRepository glimRepository;
private final LoanRepository loanRepository; private final LoanRepository loanRepository;
private final GSIMReadPlatformService gsimReadPlatformService; private final GSIMReadPlatformService gsimReadPlatformService;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final LoanLifecycleStateMachine loanLifecycleStateMachine;
private final LoanAccrualsProcessingService loanAccrualsProcessingService; private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator; private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator;
private final LoanScheduleService loanScheduleService; private final LoanScheduleService loanScheduleService;
@@ -505,9 +507,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
@Transactional @Transactional
@Override @Override
public CommandProcessingResult approveGLIMLoanAppication(final Long loanId, final JsonCommand command) { public CommandProcessingResult approveGLIMLoanAppication(final Long loanId, final JsonCommand command) {
GroupLoanIndividualMonitoringAccount parentLoan = glimRepository.findById(loanId).orElseThrow();
final Long parentLoanId = loanId;
GroupLoanIndividualMonitoringAccount parentLoan = glimRepository.findById(parentLoanId).orElseThrow();
JsonArray approvalFormData = command.arrayOfParameterNamed("approvalFormData"); JsonArray approvalFormData = command.arrayOfParameterNamed("approvalFormData");
JsonObject jsonObject = null; JsonObject jsonObject = null;
@@ -579,11 +579,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
@Transactional @Transactional
@Override @Override
public CommandProcessingResult undoGLIMLoanApplicationApproval(final Long loanId, final JsonCommand command) { public CommandProcessingResult undoGLIMLoanApplicationApproval(final Long loanId, final JsonCommand command) {
GroupLoanIndividualMonitoringAccount parentLoan = glimRepository.findById(loanId).orElseThrow();
// GroupLoanIndividualMonitoringAccount
// glimAccount=glimRepository.findOne(loanId);
final Long parentLoanId = loanId;
GroupLoanIndividualMonitoringAccount parentLoan = glimRepository.findById(parentLoanId).orElseThrow();
List<Loan> childLoans = this.loanRepository.findByGlimId(loanId); List<Loan> childLoans = this.loanRepository.findByGlimId(loanId);
CommandProcessingResult result = null; CommandProcessingResult result = null;
@@ -610,19 +606,16 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
@Transactional @Transactional
@Override @Override
public CommandProcessingResult undoApplicationApproval(final Long loanId, final JsonCommand command) { public CommandProcessingResult undoApplicationApproval(final Long loanId, final JsonCommand command) {
this.loanApplicationValidator.validateForUndo(command.json()); this.loanApplicationValidator.validateForUndo(command.json());
Loan loan = retrieveLoanBy(loanId); Loan loan = retrieveLoanBy(loanId);
loanApplicationTransitionValidator.checkClientOrGroupActive(loan); loanApplicationTransitionValidator.checkClientOrGroupActive(loan);
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_APPROVAL_UNDO); loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_APPROVAL_UNDO);
final Map<String, Object> changes = loan.undoApproval(defaultLoanLifecycleStateMachine); final Map<String, Object> changes = undoApproval(loan);
if (!changes.isEmpty()) { if (!changes.isEmpty()) {
// If loan approved amount is not same as loan amount demanded, then // If loan approved amount is not same as loan amount demanded, then
// during undo, restore the demand amount to principal amount. // during undo, restore the demand amount to principal amount.
if (changes.containsKey(LoanApiConstants.approvedLoanAmountParameterName) if (changes.containsKey(LoanApiConstants.approvedLoanAmountParameterName)
|| changes.containsKey(LoanApiConstants.disbursementPrincipalParameterName)) { || changes.containsKey(LoanApiConstants.disbursementPrincipalParameterName)) {
LocalDate recalculateFrom = null; LocalDate recalculateFrom = null;
@@ -655,11 +648,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
@Transactional @Transactional
@Override @Override
public CommandProcessingResult rejectGLIMApplicationApproval(final Long glimId, final JsonCommand command) { public CommandProcessingResult rejectGLIMApplicationApproval(final Long glimId, final JsonCommand command) {
GroupLoanIndividualMonitoringAccount parentLoan = glimRepository.findById(glimId).orElseThrow();
// GroupLoanIndividualMonitoringAccount
// glimAccount=glimRepository.findOne(loanId);
final Long parentLoanId = glimId;
GroupLoanIndividualMonitoringAccount parentLoan = glimRepository.findById(parentLoanId).orElseThrow();
List<Loan> childLoans = this.loanRepository.findByGlimId(glimId); List<Loan> childLoans = this.loanRepository.findByGlimId(glimId);
CommandProcessingResult result = null; CommandProcessingResult result = null;
@@ -699,7 +688,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
// loan application rejection // loan application rejection
final AppUser currentUser = getAppUserIfPresent(); final AppUser currentUser = getAppUserIfPresent();
defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECTED, loan); loanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECTED, loan);
final Map<String, Object> changes = loanAssembler.updateLoanApplicationAttributesForRejection(loan, command, currentUser); final Map<String, Object> changes = loanAssembler.updateLoanApplicationAttributesForRejection(loan, command, currentUser);
if (!changes.isEmpty()) { if (!changes.isEmpty()) {
@@ -737,7 +726,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
// loan application withdrawal // loan application withdrawal
final AppUser currentUser = getAppUserIfPresent(); final AppUser currentUser = getAppUserIfPresent();
defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAWN, loan); loanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAWN, loan);
final Map<String, Object> changes = loanAssembler.updateLoanApplicationAttributesForWithdrawal(loan, command, currentUser); final Map<String, Object> changes = loanAssembler.updateLoanApplicationAttributesForWithdrawal(loan, command, currentUser);
// Release attached collaterals // Release attached collaterals
if (loan.getLoanType().isIndividualAccount()) { if (loan.getLoanType().isIndividualAccount()) {
@@ -764,9 +753,7 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
} }
private Loan retrieveLoanBy(final Long loanId) { private Loan retrieveLoanBy(final Long loanId) {
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); return loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
loan.setHelpers(defaultLoanLifecycleStateMachine);
return loan;
} }
private AppUser getAppUserIfPresent() { private AppUser getAppUserIfPresent() {
@@ -862,4 +849,32 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
loan.updateLoanCollateral(loanCollateralManagements); loan.updateLoanCollateral(loanCollateralManagements);
} }
private Map<String, Object> undoApproval(final Loan loan) {
final Map<String, Object> actualChanges = new LinkedHashMap<>();
final LoanStatus currentStatus = loan.getStatus();
final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_APPROVAL_UNDO, loan);
if (!statusEnum.hasStateOf(currentStatus)) {
loanLifecycleStateMachine.transition(LoanEvent.LOAN_APPROVAL_UNDO, loan);
actualChanges.put(PARAM_STATUS, LoanEnumerations.status(loan.getStatus()));
loan.setApprovedOnDate(null);
loan.setApprovedBy(null);
if (loan.getApprovedPrincipal().compareTo(loan.getProposedPrincipal()) != 0) {
loan.setApprovedPrincipal(loan.getProposedPrincipal());
loan.getLoanRepaymentScheduleDetail().setPrincipal(loan.getProposedPrincipal());
actualChanges.put(LoanApiConstants.approvedLoanAmountParameterName, loan.getProposedPrincipal());
actualChanges.put(LoanApiConstants.disbursementPrincipalParameterName, loan.getProposedPrincipal());
}
actualChanges.put(APPROVED_ON_DATE, "");
loan.getLoanOfficerHistory().clear();
}
return actualChanges;
}
} }
@@ -76,7 +76,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule; import org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule; import org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
@@ -123,7 +122,6 @@ public class LoanAssemblerImpl implements LoanAssembler {
private final ConfigurationDomainService configurationDomainService; private final ConfigurationDomainService configurationDomainService;
private final WorkingDaysRepositoryWrapper workingDaysRepository; private final WorkingDaysRepositoryWrapper workingDaysRepository;
private final RateAssembler rateAssembler; private final RateAssembler rateAssembler;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine;
private final ExternalIdFactory externalIdFactory; private final ExternalIdFactory externalIdFactory;
private final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository; private final AccountNumberFormatRepositoryWrapper accountNumberFormatRepository;
private final GLIMAccountInfoRepository glimRepository; private final GLIMAccountInfoRepository glimRepository;
@@ -147,31 +145,17 @@ public class LoanAssemblerImpl implements LoanAssembler {
@Override @Override
public Loan assembleFrom(final Long accountId, final boolean loadLazyCollections) { public Loan assembleFrom(final Long accountId, final boolean loadLazyCollections) {
final Loan loanAccount = loanRepository.findOneWithNotFoundDetection(accountId, loadLazyCollections); return loanRepository.findOneWithNotFoundDetection(accountId, loadLazyCollections);
setHelpers(loanAccount);
return loanAccount;
} }
@Override @Override
public Loan assembleFrom(final ExternalId externalId) { public Loan assembleFrom(final ExternalId externalId) {
final Loan loanAccount = loanRepository.findOneWithNotFoundDetection(externalId, true); return loanRepository.findOneWithNotFoundDetection(externalId, true);
setHelpers(loanAccount);
return loanAccount;
} }
@Override @Override
public Loan assembleFrom(final ExternalId externalId, final boolean loadLazyCollections) { public Loan assembleFrom(final ExternalId externalId, final boolean loadLazyCollections) {
final Loan loanAccount = loanRepository.findOneWithNotFoundDetection(externalId, loadLazyCollections); return loanRepository.findOneWithNotFoundDetection(externalId, loadLazyCollections);
setHelpers(loanAccount);
return loanAccount;
}
@Override
public void setHelpers(final Loan loanAccount) {
loanAccount.setHelpers(defaultLoanLifecycleStateMachine);
} }
@Override @Override
@@ -298,7 +282,6 @@ public class LoanAssemblerImpl implements LoanAssembler {
loanSchedule.updateLoanSchedule(loanApplication, loanScheduleModel); loanSchedule.updateLoanSchedule(loanApplication, loanScheduleModel);
copyAdvancedPaymentRulesIfApplicable(transactionProcessingStrategyCode, loanProduct, loanApplication); copyAdvancedPaymentRulesIfApplicable(transactionProcessingStrategyCode, loanProduct, loanApplication);
loanApplication.setHelpers(defaultLoanLifecycleStateMachine);
// TODO: review // TODO: review
loanChargeService.recalculateAllCharges(loanApplication); loanChargeService.recalculateAllCharges(loanApplication);
topUpLoanConfiguration(element, loanApplication); topUpLoanConfiguration(element, loanApplication);
@@ -162,7 +162,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
private final LoanWritePlatformService loanWritePlatformService; private final LoanWritePlatformService loanWritePlatformService;
private final LoanUtilService loanUtilService; private final LoanUtilService loanUtilService;
private final LoanChargeReadPlatformService loanChargeReadPlatformService; private final LoanChargeReadPlatformService loanChargeReadPlatformService;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final LoanLifecycleStateMachine loanLifecycleStateMachine;
private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService; private final AccountAssociationsReadPlatformService accountAssociationsReadPlatformService;
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
private final ConfigurationDomainService configurationDomainService; private final ConfigurationDomainService configurationDomainService;
@@ -181,6 +181,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
private final LoanAccountService loanAccountService; private final LoanAccountService loanAccountService;
private final LoanAdjustmentService loanAdjustmentService; private final LoanAdjustmentService loanAdjustmentService;
private final LoanAccountingBridgeMapper loanAccountingBridgeMapper; private final LoanAccountingBridgeMapper loanAccountingBridgeMapper;
private final LoanChargeService loanChargeService;
private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) { private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) {
return DateUtils.isAfter(loanCharge.getDueDate(), e.getFromDate()) && !DateUtils.isAfter(loanCharge.getDueDate(), e.getDueDate()); return DateUtils.isAfter(loanCharge.getDueDate(), e.getFromDate()) && !DateUtils.isAfter(loanCharge.getDueDate(), e.getDueDate());
@@ -305,7 +306,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
} else { } else {
reprocessLoanTransactionsService.reprocessTransactions(loan); reprocessLoanTransactionsService.reprocessTransactions(loan);
} }
loan.doPostLoanTransactionChecks(transactionDate, loan.getLoanLifecycleStateMachine()); loanLifecycleStateMachine.determineAndTransition(loan, transactionDate);
loan = loanAccountService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan); loan = loanAccountService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
} }
@@ -571,15 +572,13 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
} }
loanChargeValidator.validateLoanIsNotClosed(loan, loanCharge); loanChargeValidator.validateLoanIsNotClosed(loan, loanCharge);
final LoanTransaction waiveTransaction = waiveLoanCharge(loan, loanCharge, defaultLoanLifecycleStateMachine, changes, final LoanTransaction waiveTransaction = waiveLoanCharge(loan, loanCharge, changes, existingTransactionIds,
existingTransactionIds, existingReversedTransactionIds, loanInstallmentNumber, scheduleGeneratorDTO, accruedCharge, existingReversedTransactionIds, loanInstallmentNumber, scheduleGeneratorDTO, accruedCharge, externalId);
externalId);
if (loan.isInterestBearingAndInterestRecalculationEnabled() if (loan.isInterestBearingAndInterestRecalculationEnabled()
&& DateUtils.isBefore(loanCharge.getDueLocalDate(), DateUtils.getBusinessLocalDate())) { && DateUtils.isBefore(loanCharge.getDueLocalDate(), DateUtils.getBusinessLocalDate())) {
loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan);
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
} }
this.loanTransactionRepository.saveAndFlush(waiveTransaction); this.loanTransactionRepository.saveAndFlush(waiveTransaction);
@@ -928,7 +927,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
} }
loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(loanChargeAdjustmentTransaction); loanAccountService.saveLoanTransactionWithDataIntegrityViolationChecks(loanChargeAdjustmentTransaction);
loan.updateLoanSummaryAndStatus(); loanLifecycleStateMachine.determineAndTransition(loan, loan.getLastUserTransactionDate());
loanAccountService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan); loanAccountService.saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
@@ -1109,7 +1108,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
loanChargeValidator.validateChargeAdditionForDisbursedLoan(loan, loanCharge); loanChargeValidator.validateChargeAdditionForDisbursedLoan(loan, loanCharge);
loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate()); loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate());
loan.addLoanCharge(loanCharge); loanChargeService.addLoanCharge(loan, loanCharge);
loanCharge = this.loanChargeRepository.saveAndFlush(loanCharge); loanCharge = this.loanChargeRepository.saveAndFlush(loanCharge);
// we want to apply charge transactions only for those loans charges that are applied when a loan is active and // we want to apply charge transactions only for those loans charges that are applied when a loan is active and
@@ -1433,8 +1432,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
} }
} }
public LoanTransaction waiveLoanCharge(final Loan loan, final LoanCharge loanCharge, public LoanTransaction waiveLoanCharge(final Loan loan, final LoanCharge loanCharge, final Map<String, Object> changes,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map<String, Object> changes,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, final Integer loanInstallmentNumber, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, final Integer loanInstallmentNumber,
final ScheduleGeneratorDTO scheduleGeneratorDTO, final Money accruedCharge, final ExternalId externalId) { final ScheduleGeneratorDTO scheduleGeneratorDTO, final Money accruedCharge, final ExternalId externalId) {
final Money amountWaived = loanCharge.waive(loan.getCurrency(), loanInstallmentNumber); final Money amountWaived = loanCharge.waive(loan.getCurrency(), loanInstallmentNumber);
@@ -1512,9 +1510,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo
loan.getActiveCharges()); loan.getActiveCharges());
} }
loan.updateLoanSummaryDerivedFields(); loanLifecycleStateMachine.determineAndTransition(loan, waiveLoanChargeTransaction.getTransactionDate());
loan.doPostLoanTransactionChecks(waiveLoanChargeTransaction.getTransactionDate(), loanLifecycleStateMachine);
return waiveLoanChargeTransaction; return waiveLoanChargeTransaction;
} }
@@ -62,6 +62,7 @@ public class LoanDisbursementService {
private final LoanChargeValidator loanChargeValidator; private final LoanChargeValidator loanChargeValidator;
private final LoanDisbursementValidator loanDisbursementValidator; private final LoanDisbursementValidator loanDisbursementValidator;
private final ReprocessLoanTransactionsService reprocessLoanTransactionsService; private final ReprocessLoanTransactionsService reprocessLoanTransactionsService;
private final LoanChargeService loanChargeService;
public void updateDisbursementDetails(final Loan loan, final JsonCommand jsonCommand, final Map<String, Object> actualChanges) { public void updateDisbursementDetails(final Loan loan, final JsonCommand jsonCommand, final Map<String, Object> actualChanges) {
final List<Long> disbursementList = loan.fetchDisbursementIds(); final List<Long> disbursementList = loan.fetchDisbursementIds();
@@ -251,7 +252,7 @@ public class LoanDisbursementService {
loanChargeValidator.validateChargeAdditionForDisbursedLoan(loan, loanCharge); loanChargeValidator.validateChargeAdditionForDisbursedLoan(loan, loanCharge);
loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate()); loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate());
loan.addLoanCharge(loanCharge); loanChargeService.addLoanCharge(loan, loanCharge);
} }
actualChanges.put(LoanApiConstants.disbursementDataParameterName, expectedDisbursementDate + "-" + principal); actualChanges.put(LoanApiConstants.disbursementDataParameterName, expectedDisbursementDate + "-" + principal);
actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true); actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true);
@@ -368,7 +368,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final Locale locale = command.extractLocale(); final Locale locale = command.extractLocale();
final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale); final DateTimeFormatter fmt = DateTimeFormatter.ofPattern(command.dateFormat()).withLocale(locale);
if (loan.canDisburse()) { if (canDisburse(loan)) {
// Get netDisbursalAmount from disbursal screen field. // Get netDisbursalAmount from disbursal screen field.
final BigDecimal netDisbursalAmount = command final BigDecimal netDisbursalAmount = command
.bigDecimalValueOfParameterNamed(LoanApiConstants.disbursementNetDisbursalAmountParameterName); .bigDecimalValueOfParameterNamed(LoanApiConstants.disbursementNetDisbursalAmountParameterName);
@@ -756,8 +756,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
// disbursement and actual disbursement happens on same date // disbursement and actual disbursement happens on same date
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_DISBURSED); loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_DISBURSED);
updateLoanCounters(loan, actualDisbursementDate); updateLoanCounters(loan, actualDisbursementDate);
boolean canDisburse = loan.canDisburse(); if (canDisburse(loan)) {
if (canDisburse) {
Money amountBeforeAdjust = loan.getPrincipal(); Money amountBeforeAdjust = loan.getPrincipal();
Money disburseAmount = loanDisbursementService.adjustDisburseAmount(loan, command, actualDisbursementDate); Money disburseAmount = loanDisbursementService.adjustDisburseAmount(loan, command, actualDisbursementDate);
boolean recalculateSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincipal()); boolean recalculateSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincipal());
@@ -960,76 +959,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
chargeRefundChargeType); chargeRefundChargeType);
} }
private void handleLoanOverpayment(Loan loan, LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) {
loan.setOverpaidOnDate(transactionDate);
loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, loan);
loan.setClosedOnDate(null);
loan.setActualMaturityDate(null);
}
private void handleLoanRepaymentInFull(final Loan loan, final LocalDate transactionDate,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
boolean isAllChargesPaid = true;
for (final LoanCharge loanCharge : loan.getCharges()) {
if (loanCharge.isActive() && loanCharge.amount().compareTo(BigDecimal.ZERO) > 0
&& !(loanCharge.isPaid() || loanCharge.isWaived())) {
isAllChargesPaid = false;
break;
}
}
if (isAllChargesPaid) {
loan.setClosedOnDate(transactionDate);
loan.setActualMaturityDate(transactionDate);
loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, loan);
} else if (loan.getLoanStatus().isOverpaid()) {
if (loan.getTotalOverpaid() == null || BigDecimal.ZERO.compareTo(loan.getTotalOverpaid()) == 0) {
loan.setOverpaidOnDate(null);
}
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan);
}
}
private boolean doPostLoanTransactionChecks(final Loan loan, final LocalDate transactionDate,
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
boolean statusChanged = false;
boolean isOverpaid = loan.getTotalOverpaid() != null && loan.getTotalOverpaid().compareTo(BigDecimal.ZERO) > 0;
if (isOverpaid) {
// FIXME - kw - update account balance to negative amount.
handleLoanOverpayment(loan, transactionDate, loanLifecycleStateMachine);
statusChanged = true;
} else if (loan.getSummary().isRepaidInFull(loan.getCurrency())) {
handleLoanRepaymentInFull(loan, transactionDate, loanLifecycleStateMachine);
statusChanged = true;
} else {
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan);
}
if (loan.getTotalOverpaid() == null || BigDecimal.ZERO.compareTo(loan.getTotalOverpaid()) == 0) {
loan.setOverpaidOnDate(null);
}
return statusChanged;
}
// TODO check
public boolean isChronologicallyTheLatestTransaction(final LoanTransaction loanTransaction,
final List<LoanTransaction> loanTransactions) {
boolean isChronologicallyLatestRepaymentOrWaiver = true;
final LocalDate currentTransactionDate = loanTransaction.getTransactionDate();
for (final LoanTransaction previousTransaction : loanTransactions) {
if (!previousTransaction.isDisbursement() && previousTransaction.isNotReversed()
&& (DateUtils.isBefore(currentTransactionDate, previousTransaction.getTransactionDate())
|| (DateUtils.isEqual(currentTransactionDate, previousTransaction.getTransactionDate())
&& ((loanTransaction.getId() == null && previousTransaction.getId() == null)
|| (loanTransaction.getId() != null && (previousTransaction.getId() == null
|| loanTransaction.getId().compareTo(previousTransaction.getId()) < 0)))))) {
isChronologicallyLatestRepaymentOrWaiver = false;
break;
}
}
return isChronologicallyLatestRepaymentOrWaiver;
}
private void recalculateLoanWithInterestPaymentWaiverTxn(Loan loan, LoanTransaction newInterestPaymentWaiverTransaction) { private void recalculateLoanWithInterestPaymentWaiverTxn(Loan loan, LoanTransaction newInterestPaymentWaiverTransaction) {
LocalDate recalculateFrom = null; LocalDate recalculateFrom = null;
LocalDate transactionDate = newInterestPaymentWaiverTransaction.getTransactionDate(); LocalDate transactionDate = newInterestPaymentWaiverTransaction.getTransactionDate();
@@ -1078,9 +1007,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
reprocessChangedLoanTransactions(loan, scheduleGeneratorDTO); reprocessChangedLoanTransactions(loan, scheduleGeneratorDTO);
} }
loan.updateLoanSummaryDerivedFields(); loanLifecycleStateMachine.determineAndTransition(loan, newInterestPaymentWaiverTransaction.getTransactionDate());
doPostLoanTransactionChecks(loan, newInterestPaymentWaiverTransaction.getTransactionDate(), loanLifecycleStateMachine);
} }
private void reprocessChangedLoanTransactions(Loan loan, ScheduleGeneratorDTO scheduleGeneratorDTO) { private void reprocessChangedLoanTransactions(Loan loan, ScheduleGeneratorDTO scheduleGeneratorDTO) {
@@ -1411,7 +1338,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
LoanTransactionRelationTypeEnum.CHARGEBACK); LoanTransactionRelationTypeEnum.CHARGEBACK);
this.loanTransactionRelationRepository.save(loanTransactionRelation); this.loanTransactionRelationRepository.save(loanTransactionRelation);
handleChargebackTransaction(loan, newTransaction, loanLifecycleStateMachine); handleChargebackTransaction(loan, newTransaction);
newTransaction = this.loanTransactionRepository.saveAndFlush(newTransaction); newTransaction = this.loanTransactionRepository.saveAndFlush(newTransaction);
@@ -1502,8 +1429,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
LoanEvent.LOAN_REPAYMENT_OR_WAIVER); LoanEvent.LOAN_REPAYMENT_OR_WAIVER);
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_REPAYMENT_OR_WAIVER,
waiveInterestTransaction.getTransactionDate()); waiveInterestTransaction.getTransactionDate());
waiveInterest(loan, waiveInterestTransaction, loanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, waiveInterest(loan, waiveInterestTransaction, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO);
scheduleGeneratorDTO);
if (loan.isInterestBearingAndInterestRecalculationEnabled()) { if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan);
@@ -1598,8 +1524,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
.withLoanId(loanId) // .withLoanId(loanId) //
.with(changes); .with(changes);
final Optional<LoanTransaction> loanTransactionOptional = closeAsWrittenOff(loan, command, loanLifecycleStateMachine, changes, final Optional<LoanTransaction> loanTransactionOptional = closeAsWrittenOff(loan, command, changes, existingTransactionIds,
existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO); existingReversedTransactionIds, currentUser, scheduleGeneratorDTO);
if (loanTransactionOptional.isPresent()) { if (loanTransactionOptional.isPresent()) {
final LoanTransaction loanTransaction = loanTransactionOptional.get(); final LoanTransaction loanTransaction = loanTransactionOptional.get();
@@ -1666,8 +1592,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LocalDate closureDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE); final LocalDate closureDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE);
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_CLOSED); loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_CLOSED);
loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.REPAID_IN_FULL, closureDate); loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.REPAID_IN_FULL, closureDate);
final Optional<LoanTransaction> loanTransactionOptional = close(loan, command, loanLifecycleStateMachine, changes, final Optional<LoanTransaction> loanTransactionOptional = close(loan, command, changes, existingTransactionIds,
existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); existingReversedTransactionIds, scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.isInterestBearingAndInterestRecalculationEnabled()) { if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
@@ -1747,7 +1673,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
changes.put("locale", command.locale()); changes.put("locale", command.locale());
changes.put("dateFormat", command.dateFormat()); changes.put("dateFormat", command.dateFormat());
closeAsMarkedForReschedule(loan, command, loanLifecycleStateMachine, changes); closeAsMarkedForReschedule(loan, command, changes);
saveLoanWithDataIntegrityViolationChecks(loan); saveLoanWithDataIntegrityViolationChecks(loan);
@@ -1816,8 +1742,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Transactional @Transactional
@Override @Override
public LoanTransaction initiateLoanTransfer(final Loan loan, final LocalDate transferDate) { public LoanTransaction initiateLoanTransfer(final Loan loan, final LocalDate transferDate) {
this.loanAssembler.setHelpers(loan);
checkClientOrGroupActive(loan); checkClientOrGroupActive(loan);
validateTransactionsForTransfer(loan, transferDate); validateTransactionsForTransfer(loan, transferDate);
@@ -1828,7 +1752,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
ExternalId externalId = externalIdFactory.create(); ExternalId externalId = externalIdFactory.create();
final LoanTransaction newTransferTransaction = LoanTransaction.initiateTransfer(loan.getOffice(), loan, transferDate, externalId); final LoanTransaction newTransferTransaction = LoanTransaction.initiateTransfer(loan.getOffice(), loan, transferDate, externalId);
loan.addLoanTransaction(newTransferTransaction); loan.addLoanTransaction(newTransferTransaction);
LoanLifecycleStateMachine loanLifecycleStateMachine = this.loanLifecycleStateMachine;
loanLifecycleStateMachine.transition(LoanEvent.LOAN_INITIATE_TRANSFER, loan); loanLifecycleStateMachine.transition(LoanEvent.LOAN_INITIATE_TRANSFER, loan);
this.loanTransactionRepository.saveAndFlush(newTransferTransaction); this.loanTransactionRepository.saveAndFlush(newTransferTransaction);
@@ -1843,7 +1766,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override @Override
public LoanTransaction acceptLoanTransfer(final Loan loan, final LocalDate transferDate, final Office acceptedInOffice, public LoanTransaction acceptLoanTransfer(final Loan loan, final LocalDate transferDate, final Office acceptedInOffice,
final Staff loanOfficer) { final Staff loanOfficer) {
this.loanAssembler.setHelpers(loan);
businessEventNotifierService.notifyPreBusinessEvent(new LoanAcceptTransferBusinessEvent(loan)); businessEventNotifierService.notifyPreBusinessEvent(new LoanAcceptTransferBusinessEvent(loan));
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds()); final List<Long> existingReversedTransactionIds = new ArrayList<>(loan.findExistingReversedTransactionIds());
@@ -1851,7 +1773,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.approveTransfer(acceptedInOffice, loan, transferDate, final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.approveTransfer(acceptedInOffice, loan, transferDate,
externalId); externalId);
loan.addLoanTransaction(newTransferAcceptanceTransaction); loan.addLoanTransaction(newTransferAcceptanceTransaction);
LoanLifecycleStateMachine loanLifecycleStateMachine = this.loanLifecycleStateMachine;
if (loan.getTotalOverpaid() != null) { if (loan.getTotalOverpaid() != null) {
loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, loan); loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, loan);
} else { } else {
@@ -1873,7 +1794,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Transactional @Transactional
@Override @Override
public LoanTransaction withdrawLoanTransfer(final Loan loan, final LocalDate transferDate) { public LoanTransaction withdrawLoanTransfer(final Loan loan, final LocalDate transferDate) {
this.loanAssembler.setHelpers(loan);
businessEventNotifierService.notifyPreBusinessEvent(new LoanWithdrawTransferBusinessEvent(loan)); businessEventNotifierService.notifyPreBusinessEvent(new LoanWithdrawTransferBusinessEvent(loan));
final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds()); final List<Long> existingTransactionIds = new ArrayList<>(loan.findExistingTransactionIds());
@@ -1884,7 +1804,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.withdrawTransfer(loan.getOffice(), loan, transferDate, final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.withdrawTransfer(loan.getOffice(), loan, transferDate,
externalId); externalId);
loan.addLoanTransaction(newTransferAcceptanceTransaction); loan.addLoanTransaction(newTransferAcceptanceTransaction);
LoanLifecycleStateMachine loanLifecycleStateMachine = this.loanLifecycleStateMachine;
loanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAW_TRANSFER, loan); loanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAW_TRANSFER, loan);
this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction); this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction);
@@ -1899,9 +1818,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Transactional @Transactional
@Override @Override
public void rejectLoanTransfer(final Loan loan) { public void rejectLoanTransfer(final Loan loan) {
this.loanAssembler.setHelpers(loan);
businessEventNotifierService.notifyPreBusinessEvent(new LoanRejectTransferBusinessEvent(loan)); businessEventNotifierService.notifyPreBusinessEvent(new LoanRejectTransferBusinessEvent(loan));
LoanLifecycleStateMachine loanLifecycleStateMachine = this.loanLifecycleStateMachine;
loanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECT_TRANSFER, loan); loanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECT_TRANSFER, loan);
saveLoanWithDataIntegrityViolationChecks(loan); saveLoanWithDataIntegrityViolationChecks(loan);
businessEventNotifierService.notifyPostBusinessEvent(new LoanRejectTransferBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanRejectTransferBusinessEvent(loan));
@@ -2281,7 +2198,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
businessEventNotifierService.notifyPreBusinessEvent(new LoanUndoWrittenOffBusinessEvent(writeOffTransaction)); businessEventNotifierService.notifyPreBusinessEvent(new LoanUndoWrittenOffBusinessEvent(writeOffTransaction));
loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.WRITE_OFF_OUTSTANDING_UNDO); loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.WRITE_OFF_OUTSTANDING_UNDO);
undoWrittenOff(loan, loanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds); undoWrittenOff(loan, existingTransactionIds, existingReversedTransactionIds);
loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.isInterestBearingAndInterestRecalculationEnabled()) { if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); loanAccrualsProcessingService.processIncomePostingAndAccruals(loan);
@@ -3079,8 +2996,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
.build(); .build();
} }
public void handleChargebackTransaction(final Loan loan, LoanTransaction chargebackTransaction, public void handleChargebackTransaction(final Loan loan, LoanTransaction chargebackTransaction) {
final LoanLifecycleStateMachine loanLifecycleStateMachine) {
loanTransactionValidator.validateIfTransactionIsChargeback(chargebackTransaction); loanTransactionValidator.validateIfTransactionIsChargeback(chargebackTransaction);
loan.addLoanTransaction(chargebackTransaction); loan.addLoanTransaction(chargebackTransaction);
@@ -3093,10 +3009,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(),
new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); new MoneyHolder(loan.getTotalOverpaidAsMoney()), null));
} }
loan.updateLoanSummaryDerivedFields(); loanLifecycleStateMachine.determineAndTransition(loan, chargebackTransaction.getTransactionDate());
if (!loan.doPostLoanTransactionChecks(chargebackTransaction.getTransactionDate(), loanLifecycleStateMachine)) {
loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGEBACK, loan);
}
} }
private void validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(Loan loan) { private void validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(Loan loan) {
@@ -3232,8 +3145,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.getLoanTransactions().retainAll(retainTransactions); loan.getLoanTransactions().retainAll(retainTransactions);
} }
private Optional<LoanTransaction> closeAsWrittenOff(final Loan loan, final JsonCommand command, private Optional<LoanTransaction> closeAsWrittenOff(final Loan loan, final JsonCommand command, final Map<String, Object> changes,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map<String, Object> changes,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, final AppUser currentUser, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, final AppUser currentUser,
final ScheduleGeneratorDTO scheduleGeneratorDTO) { final ScheduleGeneratorDTO scheduleGeneratorDTO) {
closeDisbursements(loan, scheduleGeneratorDTO); closeDisbursements(loan, scheduleGeneratorDTO);
@@ -3308,12 +3220,11 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
reprocessLoanTransactionsService.reprocessTransactions(loan); reprocessLoanTransactionsService.reprocessTransactions(loan);
LocalDate lastLoanTransactionDate = loan.getLatestTransactionDate(); LocalDate lastLoanTransactionDate = loan.getLatestTransactionDate();
loan.doPostLoanTransactionChecks(lastLoanTransactionDate, loanLifecycleStateMachine); loanLifecycleStateMachine.determineAndTransition(loan, lastLoanTransactionDate);
} }
} }
private Optional<LoanTransaction> close(final Loan loan, final JsonCommand command, private Optional<LoanTransaction> close(final Loan loan, final JsonCommand command, final Map<String, Object> changes,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map<String, Object> changes,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final ScheduleGeneratorDTO scheduleGeneratorDTO) { final ScheduleGeneratorDTO scheduleGeneratorDTO) {
existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingTransactionIds.addAll(loan.findExistingTransactionIds());
@@ -3458,25 +3369,21 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loanScheduleService.recalculateSchedule(loan, scheduleGeneratorDTO); loanScheduleService.recalculateSchedule(loan, scheduleGeneratorDTO);
actualChanges.put("undolastdisbursal", "true"); actualChanges.put("undolastdisbursal", "true");
actualChanges.put("disbursedAmount", loan.getDisbursedAmount()); actualChanges.put("disbursedAmount", loan.getDisbursedAmount());
loan.updateLoanSummaryDerivedFields(); loanLifecycleStateMachine.determineAndTransition(loan, loan.getLastUserTransactionDate());
loan.doPostLoanTransactionChecks(loan.getLastUserTransactionDate(), loanLifecycleStateMachine);
return actualChanges; return actualChanges;
} }
private void waiveInterest(final Loan loan, final LoanTransaction waiveInterestTransaction, private void waiveInterest(final Loan loan, final LoanTransaction waiveInterestTransaction, final List<Long> existingTransactionIds,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final List<Long> existingTransactionIds,
final List<Long> existingReversedTransactionIds, final ScheduleGeneratorDTO scheduleGeneratorDTO) { final List<Long> existingReversedTransactionIds, final ScheduleGeneratorDTO scheduleGeneratorDTO) {
existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, waiveInterestTransaction, loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, waiveInterestTransaction, null,
loanLifecycleStateMachine, null, scheduleGeneratorDTO); scheduleGeneratorDTO);
} }
private void undoWrittenOff(final Loan loan, final LoanLifecycleStateMachine loanLifecycleStateMachine, private void undoWrittenOff(final Loan loan, final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) {
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds) {
existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
final LoanTransaction writeOffTransaction = loan.findWriteOffTransaction(); final LoanTransaction writeOffTransaction = loan.findWriteOffTransaction();
@@ -3528,8 +3435,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
* Behaviour added to comply with capability of previous mifos product to support easier transition to fineract * Behaviour added to comply with capability of previous mifos product to support easier transition to fineract
* platform. * platform.
*/ */
public void closeAsMarkedForReschedule(final Loan loan, final JsonCommand command, public void closeAsMarkedForReschedule(final Loan loan, final JsonCommand command, final Map<String, Object> changes) {
final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map<String, Object> changes) {
final LocalDate rescheduledOn = command.localDateValueOfParameterNamed(TRANSACTION_DATE); final LocalDate rescheduledOn = command.localDateValueOfParameterNamed(TRANSACTION_DATE);
loan.setClosedOnDate(rescheduledOn); loan.setClosedOnDate(rescheduledOn);
@@ -3545,4 +3451,16 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loanTransactionValidator.validateLoanRescheduleDate(loan); loanTransactionValidator.validateLoanRescheduleDate(loan);
} }
private boolean canDisburse(final Loan loan) {
final LoanStatus statusEnum = this.loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_DISBURSED, loan);
boolean isMultiTrancheDisburse = false;
LoanStatus actualLoanStatus = loan.getStatus();
if ((actualLoanStatus.isActive() || actualLoanStatus.isClosedObligationsMet() || actualLoanStatus.isOverpaid())
&& loan.isAllTranchesNotDisbursed()) {
isMultiTrancheDisburse = true;
}
return !statusEnum.hasStateOf(actualLoanStatus) || isMultiTrancheDisburse;
}
} }
@@ -141,8 +141,8 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newTransactionDetail.getTransactionDate(), loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newTransactionDetail.getTransactionDate(),
holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay()); holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
adjustExistingTransaction(loan, newTransactionDetail, loanLifecycleStateMachine, transactionToAdjust, existingTransactionIds, adjustExistingTransaction(loan, newTransactionDetail, transactionToAdjust, existingTransactionIds, existingReversedTransactionIds,
existingReversedTransactionIds, scheduleGeneratorDTO, reversalTxnExternalId); scheduleGeneratorDTO, reversalTxnExternalId);
loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.isInterestBearingAndInterestRecalculationEnabled()) { if (loan.isInterestBearingAndInterestRecalculationEnabled()) {
@@ -185,7 +185,7 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
this.accountTransfersWritePlatformService.reverseTransfersWithFromAccountTransactions(transactionIds, this.accountTransfersWritePlatformService.reverseTransfersWithFromAccountTransactions(transactionIds,
PortfolioAccountType.LOAN); PortfolioAccountType.LOAN);
} }
loan.updateLoanSummaryAndStatus(); loanLifecycleStateMachine.determineAndTransition(loan, loan.getLastUserTransactionDate());
loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan, loan.isInterestBearingAndInterestRecalculationEnabled(), loanAccrualsProcessingService.processAccrualsOnInterestRecalculation(loan, loan.isInterestBearingAndInterestRecalculationEnabled(),
false); false);
@@ -221,9 +221,9 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
} }
public void adjustExistingTransaction(final Loan loan, final LoanTransaction newTransactionDetail, public void adjustExistingTransaction(final Loan loan, final LoanTransaction newTransactionDetail,
final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction transactionForAdjustment, final LoanTransaction transactionForAdjustment, final List<Long> existingTransactionIds,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, final List<Long> existingReversedTransactionIds, final ScheduleGeneratorDTO scheduleGeneratorDTO,
final ScheduleGeneratorDTO scheduleGeneratorDTO, final ExternalId reversalExternalId) { final ExternalId reversalExternalId) {
existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingTransactionIds.addAll(loan.findExistingTransactionIds());
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
@@ -268,8 +268,8 @@ public class LoanAdjustmentServiceImpl implements LoanAdjustmentService {
} }
if (newTransactionDetail.isRepaymentLikeType() || newTransactionDetail.isInterestWaiver()) { if (newTransactionDetail.isRepaymentLikeType() || newTransactionDetail.isInterestWaiver()) {
loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, newTransactionDetail, loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, newTransactionDetail, transactionForAdjustment,
loanLifecycleStateMachine, transactionForAdjustment, scheduleGeneratorDTO); scheduleGeneratorDTO);
} }
} }
@@ -201,21 +201,19 @@ public class LoanAccountConfiguration {
public LoanApplicationWritePlatformService loanApplicationWritePlatformService(PlatformSecurityContext context, public LoanApplicationWritePlatformService loanApplicationWritePlatformService(PlatformSecurityContext context,
LoanApplicationTransitionValidator loanApplicationTransitionValidator, LoanApplicationValidator loanApplicationValidator, LoanApplicationTransitionValidator loanApplicationTransitionValidator, LoanApplicationValidator loanApplicationValidator,
LoanRepositoryWrapper loanRepositoryWrapper, NoteRepository noteRepository, LoanAssembler loanAssembler, LoanRepositoryWrapper loanRepositoryWrapper, NoteRepository noteRepository, LoanAssembler loanAssembler,
LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
CalendarRepository calendarRepository, CalendarInstanceRepository calendarInstanceRepository, CalendarRepository calendarRepository, CalendarInstanceRepository calendarInstanceRepository,
SavingsAccountRepositoryWrapper savingsAccountRepository, AccountAssociationsRepository accountAssociationsRepository, SavingsAccountRepositoryWrapper savingsAccountRepository, AccountAssociationsRepository accountAssociationsRepository,
BusinessEventNotifierService businessEventNotifierService, LoanScheduleAssembler loanScheduleAssembler, BusinessEventNotifierService businessEventNotifierService, LoanScheduleAssembler loanScheduleAssembler,
LoanUtilService loanUtilService, CalendarReadPlatformService calendarReadPlatformService, LoanUtilService loanUtilService, CalendarReadPlatformService calendarReadPlatformService,
EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GLIMAccountInfoRepository glimRepository, EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GLIMAccountInfoRepository glimRepository,
LoanRepository loanRepository, GSIMReadPlatformService gsimReadPlatformService, LoanRepository loanRepository, GSIMReadPlatformService gsimReadPlatformService,
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanAccrualsProcessingService loanAccrualsProcessingService, LoanLifecycleStateMachine loanLifecycleStateMachine, LoanAccrualsProcessingService loanAccrualsProcessingService,
LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanScheduleService loanScheduleService) { LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanScheduleService loanScheduleService) {
return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, loanApplicationTransitionValidator, return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, loanApplicationTransitionValidator,
loanApplicationValidator, loanRepositoryWrapper, noteRepository, loanAssembler, loanApplicationValidator, loanRepositoryWrapper, noteRepository, loanAssembler, calendarRepository,
loanRepaymentScheduleTransactionProcessorFactory, calendarRepository, calendarInstanceRepository, savingsAccountRepository, calendarInstanceRepository, savingsAccountRepository, accountAssociationsRepository, businessEventNotifierService,
accountAssociationsRepository, businessEventNotifierService, loanScheduleAssembler, loanUtilService, loanScheduleAssembler, loanUtilService, calendarReadPlatformService, entityDatatableChecksWritePlatformService,
calendarReadPlatformService, entityDatatableChecksWritePlatformService, glimRepository, loanRepository, glimRepository, loanRepository, gsimReadPlatformService, loanLifecycleStateMachine, loanAccrualsProcessingService,
gsimReadPlatformService, defaultLoanLifecycleStateMachine, loanAccrualsProcessingService,
loanDownPaymentTransactionValidator, loanScheduleService); loanDownPaymentTransactionValidator, loanScheduleService);
} }
@@ -235,8 +233,7 @@ public class LoanAccountConfiguration {
LoanCollateralAssembler collateralAssembler, LoanCollateralAssembler collateralAssembler,
LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
HolidayRepository holidayRepository, ConfigurationDomainService configurationDomainService, HolidayRepository holidayRepository, ConfigurationDomainService configurationDomainService,
WorkingDaysRepositoryWrapper workingDaysRepository, RateAssembler rateAssembler, WorkingDaysRepositoryWrapper workingDaysRepository, RateAssembler rateAssembler, ExternalIdFactory externalIdFactory,
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, ExternalIdFactory externalIdFactory,
AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, GLIMAccountInfoRepository glimRepository, AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, GLIMAccountInfoRepository glimRepository,
AccountNumberGenerator accountNumberGenerator, GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService, AccountNumberGenerator accountNumberGenerator, GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService,
LoanCollateralAssembler loanCollateralAssembler, LoanScheduleCalculationPlatformService calculationPlatformService, LoanCollateralAssembler loanCollateralAssembler, LoanScheduleCalculationPlatformService calculationPlatformService,
@@ -247,10 +244,10 @@ public class LoanAccountConfiguration {
return new LoanAssemblerImpl(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository, return new LoanAssemblerImpl(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository,
fundRepository, staffRepository, codeValueRepository, loanScheduleAssembler, loanChargeAssembler, collateralAssembler, fundRepository, staffRepository, codeValueRepository, loanScheduleAssembler, loanChargeAssembler, collateralAssembler,
loanRepaymentScheduleTransactionProcessorFactory, holidayRepository, configurationDomainService, workingDaysRepository, loanRepaymentScheduleTransactionProcessorFactory, holidayRepository, configurationDomainService, workingDaysRepository,
rateAssembler, defaultLoanLifecycleStateMachine, externalIdFactory, accountNumberFormatRepository, glimRepository, rateAssembler, externalIdFactory, accountNumberFormatRepository, glimRepository, accountNumberGenerator,
accountNumberGenerator, glimAccountInfoWritePlatformService, loanCollateralAssembler, calculationPlatformService, glimAccountInfoWritePlatformService, loanCollateralAssembler, calculationPlatformService, loanDisbursementDetailsAssembler,
loanDisbursementDetailsAssembler, loanChargeMapper, loanCollateralManagementMapper, loanAccrualsProcessingService, loanChargeMapper, loanCollateralManagementMapper, loanAccrualsProcessingService, loanDisbursementService, loanChargeService,
loanDisbursementService, loanChargeService, loanOfficerService, loanSchedule); loanOfficerService, loanSchedule);
} }
@Bean @Bean
@@ -293,7 +290,7 @@ public class LoanAccountConfiguration {
AccountTransfersWritePlatformService accountTransfersWritePlatformService, LoanRepositoryWrapper loanRepositoryWrapper, AccountTransfersWritePlatformService accountTransfersWritePlatformService, LoanRepositoryWrapper loanRepositoryWrapper,
JournalEntryWritePlatformService journalEntryWritePlatformService, LoanAccountDomainService loanAccountDomainService, JournalEntryWritePlatformService journalEntryWritePlatformService, LoanAccountDomainService loanAccountDomainService,
LoanChargeRepository loanChargeRepository, LoanWritePlatformService loanWritePlatformService, LoanUtilService loanUtilService, LoanChargeRepository loanChargeRepository, LoanWritePlatformService loanWritePlatformService, LoanUtilService loanUtilService,
LoanChargeReadPlatformService loanChargeReadPlatformService, LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanChargeReadPlatformService loanChargeReadPlatformService, LoanLifecycleStateMachine loanLifecycleStateMachine,
AccountAssociationsReadPlatformService accountAssociationsReadPlatformService, FromJsonHelper fromApiJsonHelper, AccountAssociationsReadPlatformService accountAssociationsReadPlatformService, FromJsonHelper fromApiJsonHelper,
ConfigurationDomainService configurationDomainService, ConfigurationDomainService configurationDomainService,
LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory, LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory,
@@ -304,16 +301,16 @@ public class LoanAccountConfiguration {
LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanChargeValidator loanChargeValidator, LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanChargeValidator loanChargeValidator,
LoanScheduleService loanScheduleService, ReprocessLoanTransactionsService reprocessLoanTransactionsService, LoanScheduleService loanScheduleService, ReprocessLoanTransactionsService reprocessLoanTransactionsService,
LoanAccountService loanAccountService, LoanAdjustmentService loanAdjustmentService, LoanAccountService loanAccountService, LoanAdjustmentService loanAdjustmentService,
LoanAccountingBridgeMapper loanAccountingBridgeMapper) { LoanAccountingBridgeMapper loanAccountingBridgeMapper, LoanChargeService loanChargeService) {
return new LoanChargeWritePlatformServiceImpl(loanChargeApiJsonValidator, loanAssembler, chargeRepository, return new LoanChargeWritePlatformServiceImpl(loanChargeApiJsonValidator, loanAssembler, chargeRepository,
businessEventNotifierService, loanTransactionRepository, accountTransfersWritePlatformService, loanRepositoryWrapper, businessEventNotifierService, loanTransactionRepository, accountTransfersWritePlatformService, loanRepositoryWrapper,
journalEntryWritePlatformService, loanAccountDomainService, loanChargeRepository, loanWritePlatformService, loanUtilService, journalEntryWritePlatformService, loanAccountDomainService, loanChargeRepository, loanWritePlatformService, loanUtilService,
loanChargeReadPlatformService, defaultLoanLifecycleStateMachine, accountAssociationsReadPlatformService, fromApiJsonHelper, loanChargeReadPlatformService, loanLifecycleStateMachine, accountAssociationsReadPlatformService, fromApiJsonHelper,
configurationDomainService, loanRepaymentScheduleTransactionProcessorFactory, externalIdFactory, configurationDomainService, loanRepaymentScheduleTransactionProcessorFactory, externalIdFactory,
accountTransferDetailRepository, loanChargeAssembler, paymentDetailWritePlatformService, noteRepository, accountTransferDetailRepository, loanChargeAssembler, paymentDetailWritePlatformService, noteRepository,
loanAccrualTransactionBusinessEventService, loanAccrualsProcessingService, loanDownPaymentTransactionValidator, loanAccrualTransactionBusinessEventService, loanAccrualsProcessingService, loanDownPaymentTransactionValidator,
loanChargeValidator, loanScheduleService, reprocessLoanTransactionsService, loanAccountService, loanAdjustmentService, loanChargeValidator, loanScheduleService, reprocessLoanTransactionsService, loanAccountService, loanAdjustmentService,
loanAccountingBridgeMapper); loanAccountingBridgeMapper, loanChargeService);
} }
@Bean @Bean
@@ -395,7 +392,7 @@ public class LoanAccountConfiguration {
RepaymentWithPostDatedChecksAssembler repaymentWithPostDatedChecksAssembler, RepaymentWithPostDatedChecksAssembler repaymentWithPostDatedChecksAssembler,
PostDatedChecksRepository postDatedChecksRepository, PostDatedChecksRepository postDatedChecksRepository,
LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository, LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository,
LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanAccountLockService loanAccountLockService, LoanLifecycleStateMachine loanLifecycleStateMachine, LoanAccountLockService loanAccountLockService,
ExternalIdFactory externalIdFactory, LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService, ExternalIdFactory externalIdFactory, LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService,
ErrorHandler errorHandler, LoanDownPaymentHandlerService loanDownPaymentHandlerService, ErrorHandler errorHandler, LoanDownPaymentHandlerService loanDownPaymentHandlerService,
LoanTransactionAssembler loanTransactionAssembler, LoanAccrualsProcessingService loanAccrualsProcessingService, LoanTransactionAssembler loanTransactionAssembler, LoanAccrualsProcessingService loanAccrualsProcessingService,
@@ -415,12 +412,12 @@ public class LoanAccountConfiguration {
loanApplicationValidator, accountAssociationRepository, accountTransferDetailRepository, businessEventNotifierService, loanApplicationValidator, accountAssociationRepository, accountTransferDetailRepository, businessEventNotifierService,
guarantorDomainService, loanUtilService, entityDatatableChecksWritePlatformService, codeValueRepository, guarantorDomainService, loanUtilService, entityDatatableChecksWritePlatformService, codeValueRepository,
cashierTransactionDataValidator, glimRepository, loanRepository, repaymentWithPostDatedChecksAssembler, cashierTransactionDataValidator, glimRepository, loanRepository, repaymentWithPostDatedChecksAssembler,
postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository, defaultLoanLifecycleStateMachine, postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository, loanLifecycleStateMachine, loanAccountLockService,
loanAccountLockService, externalIdFactory, loanAccrualTransactionBusinessEventService, errorHandler, externalIdFactory, loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService,
loanDownPaymentHandlerService, loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator, loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator, loanDownPaymentTransactionValidator,
loanDownPaymentTransactionValidator, loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService, loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService, reprocessLoanTransactionsService,
reprocessLoanTransactionsService, loanAccountService, journalEntryPoster, loanAdjustmentService, loanAccountingBridgeMapper, loanAccountService, journalEntryPoster, loanAdjustmentService, loanAccountingBridgeMapper, loanMapper,
loanMapper, loanTransactionProcessingService); loanTransactionProcessingService);
} }
@Bean @Bean
@@ -437,10 +434,10 @@ public class LoanAccountConfiguration {
LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanScheduleService loanScheduleService, LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanScheduleService loanScheduleService,
LoanRefundService loanRefundService, LoanRefundValidator loanRefundValidator, LoanRefundService loanRefundService, LoanRefundValidator loanRefundValidator,
ReprocessLoanTransactionsService reprocessLoanTransactionsService, ReprocessLoanTransactionsService reprocessLoanTransactionsService,
LoanTransactionProcessingService loanTransactionProcessingService) { LoanTransactionProcessingService loanTransactionProcessingService, LoanLifecycleStateMachine loanLifecycleStateMachine) {
return new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService, return new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService,
loanDownPaymentTransactionValidator, loanScheduleService, loanRefundService, loanRefundValidator, loanDownPaymentTransactionValidator, loanScheduleService, loanRefundService, loanRefundValidator,
reprocessLoanTransactionsService, loanTransactionProcessingService); reprocessLoanTransactionsService, loanTransactionProcessingService, loanLifecycleStateMachine);
} }
@Bean @Bean
@@ -452,15 +449,17 @@ public class LoanAccountConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(LoanDisbursementService.class) @ConditionalOnMissingBean(LoanDisbursementService.class)
public LoanDisbursementService loanDisbursementService(LoanChargeValidator loanChargeValidator, public LoanDisbursementService loanDisbursementService(LoanChargeValidator loanChargeValidator,
LoanDisbursementValidator loanDisbursementValidator, ReprocessLoanTransactionsService reprocessLoanTransactionsService) { LoanDisbursementValidator loanDisbursementValidator, ReprocessLoanTransactionsService reprocessLoanTransactionsService,
return new LoanDisbursementService(loanChargeValidator, loanDisbursementValidator, reprocessLoanTransactionsService); LoanChargeService loanChargeService) {
return new LoanDisbursementService(loanChargeValidator, loanDisbursementValidator, reprocessLoanTransactionsService,
loanChargeService);
} }
@Bean @Bean
@ConditionalOnMissingBean(LoanChargeService.class) @ConditionalOnMissingBean(LoanChargeService.class)
public LoanChargeService loanChargeService(LoanChargeValidator loanChargeValidator, public LoanChargeService loanChargeService(LoanChargeValidator loanChargeValidator,
LoanTransactionProcessingService loanTransactionProcessingService) { LoanTransactionProcessingService loanTransactionProcessingService, LoanLifecycleStateMachine loanLifecycleStateMachine) {
return new LoanChargeService(loanChargeValidator, loanTransactionProcessingService); return new LoanChargeService(loanChargeValidator, loanTransactionProcessingService, loanLifecycleStateMachine);
} }
@Bean @Bean
@@ -480,9 +479,10 @@ public class LoanAccountConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(LoanRefundService.class) @ConditionalOnMissingBean(LoanRefundService.class)
public LoanRefundService loanRefundService(LoanRefundValidator loanRefundValidator, public LoanRefundService loanRefundService(final LoanRefundValidator loanRefundValidator,
LoanTransactionProcessingService loanTransactionProcessingService) { final LoanTransactionProcessingService loanTransactionProcessingService,
return new LoanRefundService(loanRefundValidator, loanTransactionProcessingService); final LoanLifecycleStateMachine loanLifecycleStateMachine) {
return new LoanRefundService(loanRefundValidator, loanTransactionProcessingService, loanLifecycleStateMachine);
} }
@Bean @Bean
@@ -241,8 +241,8 @@ class LoanAdjustmentServiceImplTest {
when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true); when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
// Act // Act
underTest.adjustExistingTransaction(loan, newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment, underTest.adjustExistingTransaction(loan, newTransactionDetail, transactionForAdjustment, existingTransactionIds,
existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO, reversalExternalId); existingReversedTransactionIds, scheduleGeneratorDTO, reversalExternalId);
// Assert // Assert
// Verify that related transaction is reversed and event is triggered // Verify that related transaction is reversed and event is triggered
@@ -286,8 +286,8 @@ class LoanAdjustmentServiceImplTest {
when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true); when(newTransactionDetail.isRepaymentLikeType()).thenReturn(true);
// Act // Act
underTest.adjustExistingTransaction(loan, newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment, underTest.adjustExistingTransaction(loan, newTransactionDetail, transactionForAdjustment, existingTransactionIds,
existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO, reversalExternalId); existingReversedTransactionIds, scheduleGeneratorDTO, reversalExternalId);
// Assert // Assert
// Verify that no related transactions are reversed // Verify that no related transactions are reversed
@@ -30,8 +30,11 @@ import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify; import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when; import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.time.LocalDate; import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashSet;
import java.util.List; import java.util.List;
import java.util.stream.Stream; import java.util.stream.Stream;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
@@ -41,7 +44,9 @@ import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanAccrualTransactionCreatedBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.Charge;
import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType;
import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode; import org.apache.fineract.portfolio.charge.domain.ChargePaymentMode;
import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper; import org.apache.fineract.portfolio.charge.domain.ChargeRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO; import org.apache.fineract.portfolio.loanaccount.data.AccountingBridgeDataDTO;
@@ -50,9 +55,12 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountService; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
import org.apache.fineract.portfolio.loanaccount.mapper.LoanAccountingBridgeMapper; import org.apache.fineract.portfolio.loanaccount.mapper.LoanAccountingBridgeMapper;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator;
import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator;
@@ -150,6 +158,18 @@ class LoanChargeWritePlatformServiceImplTest {
@Mock @Mock
private LoanAccountingBridgeMapper loanAccountingBridgeMapper; private LoanAccountingBridgeMapper loanAccountingBridgeMapper;
@Mock
private LoanChargeService loanChargeService;
@Mock
private ChargeCalculationType chargeCalculationType;
@Mock
private SingleLoanChargeRepaymentScheduleProcessingWrapper wrapper;
@Mock
private LoanLifecycleStateMachine loanLifecycleStateMachine;
@BeforeEach @BeforeEach
void setUp() { void setUp() {
when(loanAssembler.assembleFrom(LOAN_ID)).thenReturn(loan); when(loanAssembler.assembleFrom(LOAN_ID)).thenReturn(loan);
@@ -158,8 +178,13 @@ class LoanChargeWritePlatformServiceImplTest {
when(chargeDefinition.getCurrencyCode()).thenReturn(CURRENCY_CODE); when(chargeDefinition.getCurrencyCode()).thenReturn(CURRENCY_CODE);
when(loanChargeAssembler.createNewFromJson(loan, chargeDefinition, jsonCommand)).thenReturn(loanCharge); when(loanChargeAssembler.createNewFromJson(loan, chargeDefinition, jsonCommand)).thenReturn(loanCharge);
when(loan.repaymentScheduleDetail()).thenReturn(loanRepaymentScheduleDetail); when(loan.repaymentScheduleDetail()).thenReturn(loanRepaymentScheduleDetail);
when(loanRepaymentScheduleDetail.getLoanScheduleType()).thenReturn(LoanScheduleType.CUMULATIVE);
when(loan.getLoanRepaymentScheduleDetail()).thenReturn(loanRepaymentScheduleDetail);
when(loan.hasCurrencyCodeOf(CURRENCY_CODE)).thenReturn(true); when(loan.hasCurrencyCodeOf(CURRENCY_CODE)).thenReturn(true);
when(loanCharge.getChargePaymentMode()).thenReturn(ChargePaymentMode.REGULAR); when(loanCharge.getChargePaymentMode()).thenReturn(ChargePaymentMode.REGULAR);
when(loanCharge.getChargeCalculation()).thenReturn(chargeCalculationType);
when(chargeCalculationType.isPercentageBased()).thenReturn(false);
when(loanCharge.amountOrPercentage()).thenReturn(BigDecimal.TEN);
when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE); when(loan.getStatus()).thenReturn(LoanStatus.ACTIVE);
when(loanChargeRepository.saveAndFlush(any(LoanCharge.class))).thenReturn(loanCharge); when(loanChargeRepository.saveAndFlush(any(LoanCharge.class))).thenReturn(loanCharge);
when(loan.getCurrency()).thenReturn(monetaryCurrency); when(loan.getCurrency()).thenReturn(monetaryCurrency);
@@ -170,12 +195,23 @@ class LoanChargeWritePlatformServiceImplTest {
when(loan.findExistingTransactionIds()).thenReturn(existingTransactionIds); when(loan.findExistingTransactionIds()).thenReturn(existingTransactionIds);
when(loan.findExistingReversedTransactionIds()).thenReturn(existingReversedTransactionIds); when(loan.findExistingReversedTransactionIds()).thenReturn(existingReversedTransactionIds);
when(loan.getLoanCharges()).thenReturn(new HashSet<>());
when(loan.getDisbursementDate()).thenReturn(LocalDate.now(ZoneId.systemDefault()));
when(loan.getRepaymentScheduleInstallments()).thenReturn(new ArrayList<>());
when(loan.calculateAmountPercentageAppliedTo(any(LoanCharge.class))).thenReturn(BigDecimal.TEN);
when(loan.fetchNumberOfInstallmensAfterExceptions()).thenReturn(5);
when(loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(any(BigDecimal.class))).thenReturn(null);
when(loan.deriveSumTotalOfChargesDueAtDisbursement()).thenReturn(BigDecimal.ZERO);
when(loanCharge.getDueLocalDate()).thenReturn(LocalDate.now(ZoneId.systemDefault()));
when(loanCharge.getEffectiveDueDate()).thenReturn(LocalDate.now(ZoneId.systemDefault()));
when(loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()).thenReturn(false); when(loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()).thenReturn(false);
when(loan.isCashBasedAccountingEnabledOnLoanProduct()).thenReturn(false); when(loan.isCashBasedAccountingEnabledOnLoanProduct()).thenReturn(false);
when(loan.isUpfrontAccrualAccountingEnabledOnLoanProduct()).thenReturn(false); when(loan.isUpfrontAccrualAccountingEnabledOnLoanProduct()).thenReturn(false);
when(loanAccountingBridgeMapper.deriveAccountingBridgeData(anyString(), anyList(), anyList(), anyBoolean(), any(Loan.class))).thenReturn(new AccountingBridgeDataDTO()); when(loanAccountingBridgeMapper.deriveAccountingBridgeData(anyString(), anyList(), anyList(), anyBoolean(), any(Loan.class))).thenReturn(new AccountingBridgeDataDTO());
doNothing().when(journalEntryWritePlatformService).createJournalEntriesForLoan(any(AccountingBridgeDataDTO.class)); doNothing().when(journalEntryWritePlatformService).createJournalEntriesForLoan(any(AccountingBridgeDataDTO.class));
doNothing().when(loanChargeService).addLoanCharge(any(Loan.class), any(LoanCharge.class));
} }
@ParameterizedTest @ParameterizedTest
@@ -189,8 +225,13 @@ class LoanChargeWritePlatformServiceImplTest {
when(loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()).thenReturn(true); when(loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()).thenReturn(true);
} }
try (MockedStatic<DateUtils> mockedDateUtils = mockStatic(DateUtils.class)) { try (MockedStatic<DateUtils> mockedDateUtils = mockStatic(DateUtils.class);
MockedStatic<MoneyHelper> mockedMoneyHelper = mockStatic(MoneyHelper.class)) {
mockedDateUtils.when(DateUtils::getBusinessLocalDate).thenReturn(businessDate); mockedDateUtils.when(DateUtils::getBusinessLocalDate).thenReturn(businessDate);
mockedDateUtils.when(() -> DateUtils.isBeforeBusinessDate(any(LocalDate.class))).thenReturn(false);
mockedMoneyHelper.when(MoneyHelper::getMathContext).thenReturn(java.math.MathContext.DECIMAL64);
mockedMoneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(java.math.RoundingMode.HALF_EVEN);
loanChargeWritePlatformService.addLoanCharge(LOAN_ID, jsonCommand); loanChargeWritePlatformService.addLoanCharge(LOAN_ID, jsonCommand);
} }
@@ -45,6 +45,7 @@ import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
@@ -107,13 +108,16 @@ public class LoanDownPaymentHandlerServiceImplTest {
@Mock @Mock
private LoanTransactionProcessingService loanTransactionProcessingService; private LoanTransactionProcessingService loanTransactionProcessingService;
@Mock
private LoanLifecycleStateMachine loanLifecycleStateMachine;
private LoanDownPaymentHandlerServiceImpl underTest; private LoanDownPaymentHandlerServiceImpl underTest;
@BeforeEach @BeforeEach
public void setUp() { public void setUp() {
underTest = new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService, underTest = new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService,
loanDownPaymentTransactionValidator, loanScheduleService, loanRefundService, loanRefundValidator, loanDownPaymentTransactionValidator, loanScheduleService, loanRefundService, loanRefundValidator,
reprocessLoanTransactionsService, loanTransactionProcessingService); reprocessLoanTransactionsService, loanTransactionProcessingService, loanLifecycleStateMachine);
moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP)); moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP));
moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP); moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP);
tempConfigServiceMock.when(TemporaryConfigurationServiceContainer::isExternalIdAutoGenerationEnabled).thenReturn(true); tempConfigServiceMock.when(TemporaryConfigurationServiceContainer::isExternalIdAutoGenerationEnabled).thenReturn(true);