FINERACT-1734: Refactoring around Loan status changes as a preparation to emit Loan Status change business events

This commit is contained in:
Arnold Galovics
2022-09-09 13:13:37 +02:00
committed by Arnold Gálovics
parent 8bb2e9230c
commit d003326e48
4 changed files with 109 additions and 30 deletions
@@ -19,15 +19,13 @@
package org.apache.fineract.portfolio.loanaccount.domain; package org.apache.fineract.portfolio.loanaccount.domain;
import java.util.List; import java.util.List;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachine { public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachine {
private final List<LoanStatus> allowedLoanStatuses; private final List<LoanStatus> allowedLoanStatuses;
public DefaultLoanLifecycleStateMachine(final List<LoanStatus> allowedLoanStatuses) {
this.allowedLoanStatuses = allowedLoanStatuses;
}
@Override @Override
public LoanStatus transition(final LoanEvent loanEvent, final LoanStatus from) { public LoanStatus transition(final LoanEvent loanEvent, final LoanStatus from) {
@@ -36,77 +34,94 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
switch (loanEvent) { switch (loanEvent) {
case LOAN_CREATED: case LOAN_CREATED:
if (from == null) { if (from == null) {
newState = stateOf(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, this.allowedLoanStatuses); newState = submittedTransition();
} }
break; break;
case LOAN_REJECTED: case LOAN_REJECTED:
if (from.hasStateOf(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL)) { if (from.hasStateOf(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL)) {
newState = stateOf(LoanStatus.REJECTED, this.allowedLoanStatuses); newState = rejectedTransition();
} }
break; break;
case LOAN_APPROVED: case LOAN_APPROVED:
if (from.hasStateOf(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL)) { if (from.hasStateOf(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL)) {
newState = stateOf(LoanStatus.APPROVED, this.allowedLoanStatuses); newState = approvedTransition();
} }
break; break;
case LOAN_WITHDRAWN: case LOAN_WITHDRAWN:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.SUBMITTED_AND_PENDING_APPROVAL)) { if (anyOfAllowedWhenComingFrom(from, LoanStatus.SUBMITTED_AND_PENDING_APPROVAL)) {
newState = stateOf(LoanStatus.WITHDRAWN_BY_CLIENT, this.allowedLoanStatuses); newState = withdrawnByClientTransition();
} }
break; break;
case LOAN_DISBURSED: case LOAN_DISBURSED:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.APPROVED, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.OVERPAID)) { if (anyOfAllowedWhenComingFrom(from, LoanStatus.APPROVED, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.OVERPAID)) {
newState = stateOf(LoanStatus.ACTIVE, this.allowedLoanStatuses); newState = activeTransition();
} }
break; break;
case LOAN_APPROVAL_UNDO: case LOAN_APPROVAL_UNDO:
if (from.hasStateOf(LoanStatus.APPROVED)) { if (from.hasStateOf(LoanStatus.APPROVED)) {
newState = stateOf(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, this.allowedLoanStatuses); newState = submittedTransition();
} }
break; break;
case LOAN_DISBURSAL_UNDO: case LOAN_DISBURSAL_UNDO:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) { if (anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) {
newState = stateOf(LoanStatus.APPROVED, this.allowedLoanStatuses); newState = approvedTransition();
} }
break; break;
case LOAN_CHARGE_PAYMENT: case LOAN_CHARGE_PAYMENT:
case LOAN_REPAYMENT_OR_WAIVER: case LOAN_REPAYMENT_OR_WAIVER:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.OVERPAID)) { if (anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.OVERPAID)) {
newState = stateOf(LoanStatus.ACTIVE, this.allowedLoanStatuses); newState = activeTransition();
} else {
newState = from;
} }
break; break;
case REPAID_IN_FULL: case REPAID_IN_FULL:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE, LoanStatus.OVERPAID)) { if (anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE, LoanStatus.OVERPAID)) {
newState = stateOf(LoanStatus.CLOSED_OBLIGATIONS_MET, this.allowedLoanStatuses); newState = closeObligationsMetTransition();
} }
break; break;
case WRITE_OFF_OUTSTANDING: case WRITE_OFF_OUTSTANDING:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) { if (anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) {
newState = stateOf(LoanStatus.CLOSED_WRITTEN_OFF, this.allowedLoanStatuses); newState = closedWrittenOffTransition();
} }
break; break;
case LOAN_RESCHEDULE: case LOAN_RESCHEDULE:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) { if (anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE)) {
newState = stateOf(LoanStatus.CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT, this.allowedLoanStatuses); newState = closedRescheduleOutstandingAmountTransition();
} }
break; break;
case INTERST_REBATE_OWED: case INTERST_REBATE_OWED:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET)) { if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET)) {
newState = stateOf(LoanStatus.CLOSED_OBLIGATIONS_MET, this.allowedLoanStatuses); newState = closeObligationsMetTransition();
} }
break; break;
case LOAN_OVERPAYMENT: case LOAN_OVERPAYMENT:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.ACTIVE)) { if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.ACTIVE)) {
newState = stateOf(LoanStatus.OVERPAID, this.allowedLoanStatuses); newState = overpaidTransition();
} }
break; break;
case LOAN_ADJUST_TRANSACTION:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.CLOSED_WRITTEN_OFF,
LoanStatus.CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT)) {
newState = activeTransition();
}
break;
case LOAN_INITIATE_TRANSFER:
newState = transferInProgress();
break;
case LOAN_REJECT_TRANSFER:
newState = transferOnHold();
break;
case LOAN_WITHDRAW_TRANSFER:
newState = activeTransition();
break;
case LOAN_CLOSED: case LOAN_CLOSED:
break; break;
case WRITE_OFF_OUTSTANDING_UNDO: case WRITE_OFF_OUTSTANDING_UNDO:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_WRITTEN_OFF)) {
newState = activeTransition();
}
break; break;
case LOAN_CREDIT_BALANCE_REFUND: case LOAN_CREDIT_BALANCE_REFUND:
newState = closeObligationsMetTransition();
break; break;
default: default:
break; break;
@@ -115,6 +130,50 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
return newState; return newState;
} }
private LoanStatus transferOnHold() {
return stateOf(LoanStatus.TRANSFER_ON_HOLD, this.allowedLoanStatuses);
}
private LoanStatus transferInProgress() {
return stateOf(LoanStatus.TRANSFER_IN_PROGRESS, this.allowedLoanStatuses);
}
private LoanStatus overpaidTransition() {
return stateOf(LoanStatus.OVERPAID, this.allowedLoanStatuses);
}
private LoanStatus closedRescheduleOutstandingAmountTransition() {
return stateOf(LoanStatus.CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT, this.allowedLoanStatuses);
}
private LoanStatus closedWrittenOffTransition() {
return stateOf(LoanStatus.CLOSED_WRITTEN_OFF, this.allowedLoanStatuses);
}
private LoanStatus closeObligationsMetTransition() {
return stateOf(LoanStatus.CLOSED_OBLIGATIONS_MET, this.allowedLoanStatuses);
}
private LoanStatus activeTransition() {
return stateOf(LoanStatus.ACTIVE, this.allowedLoanStatuses);
}
private LoanStatus withdrawnByClientTransition() {
return stateOf(LoanStatus.WITHDRAWN_BY_CLIENT, this.allowedLoanStatuses);
}
private LoanStatus approvedTransition() {
return stateOf(LoanStatus.APPROVED, this.allowedLoanStatuses);
}
private LoanStatus rejectedTransition() {
return stateOf(LoanStatus.REJECTED, this.allowedLoanStatuses);
}
private LoanStatus submittedTransition() {
return stateOf(LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, this.allowedLoanStatuses);
}
private LoanStatus stateOf(final LoanStatus state, final List<LoanStatus> allowedLoanStatuses) { private LoanStatus stateOf(final LoanStatus state, final List<LoanStatus> allowedLoanStatuses) {
LoanStatus match = null; LoanStatus match = null;
for (final LoanStatus loanStatus : allowedLoanStatuses) { for (final LoanStatus loanStatus : allowedLoanStatuses) {
@@ -3635,7 +3635,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
} }
if (isClosedObligationsMet() || isClosedWrittenOff() || isClosedWithOutsandingAmountMarkedForReschedule()) { if (isClosedObligationsMet() || isClosedWrittenOff() || isClosedWithOutsandingAmountMarkedForReschedule()) {
this.loanStatus = LoanStatus.ACTIVE.getValue(); LoanStatus loanStatusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_ADJUST_TRANSACTION,
LoanStatus.fromInt(this.loanStatus));
this.loanStatus = loanStatusEnum.getValue();
} }
if (newTransactionDetail.isRepaymentType() || newTransactionDetail.isInterestWaiver()) { if (newTransactionDetail.isRepaymentType() || newTransactionDetail.isInterestWaiver()) {
@@ -3646,7 +3648,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return changedTransactionDetail; return changedTransactionDetail;
} }
public ChangedTransactionDetail undoWrittenOff(final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds, public ChangedTransactionDetail undoWrittenOff(LoanLifecycleStateMachine loanLifecycleStateMachine,
final List<Long> existingTransactionIds, final List<Long> existingReversedTransactionIds,
final ScheduleGeneratorDTO scheduleGeneratorDTO) { final ScheduleGeneratorDTO scheduleGeneratorDTO) {
validateAccountStatus(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO); validateAccountStatus(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO);
@@ -3654,7 +3657,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); existingReversedTransactionIds.addAll(findExistingReversedTransactionIds());
final LoanTransaction writeOffTransaction = findWriteOffTransaction(); final LoanTransaction writeOffTransaction = findWriteOffTransaction();
writeOffTransaction.reverse(); writeOffTransaction.reverse();
this.loanStatus = LoanStatus.ACTIVE.getValue(); LoanStatus loanStatusEnum = loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO,
LoanStatus.fromInt(this.loanStatus));
this.loanStatus = loanStatusEnum.getValue();
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategy); .determineProcessor(this.transactionProcessingStrategy);
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement(); final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retreiveListOfTransactionsPostDisbursement();
@@ -6068,7 +6073,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
updateLoanSummaryDerivedFields(); updateLoanSummaryDerivedFields();
if (this.totalOverpaid == null || BigDecimal.ZERO.compareTo(this.totalOverpaid) == 0) { if (this.totalOverpaid == null || BigDecimal.ZERO.compareTo(this.totalOverpaid) == 0) {
this.loanStatus = LoanStatus.CLOSED_OBLIGATIONS_MET.getValue(); LoanStatus statusEnum = defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_CREDIT_BALANCE_REFUND,
LoanStatus.fromInt(this.loanStatus));
this.loanStatus = statusEnum.getValue();
} }
} }
@@ -44,5 +44,9 @@ public enum LoanEvent {
LOAN_EDIT_MULTI_DISBURSE_DATE, // LOAN_EDIT_MULTI_DISBURSE_DATE, //
LOAN_REFUND, // LOAN_REFUND, //
LOAN_FORECLOSURE, // LOAN_FORECLOSURE, //
LOAN_ADJUST_TRANSACTION, //
LOAN_INITIATE_TRANSFER, //
LOAN_REJECT_TRANSFER, //
LOAN_WITHDRAW_TRANSFER, //
LOAN_CREDIT_BALANCE_REFUND; LOAN_CREDIT_BALANCE_REFUND;
} }
@@ -2260,7 +2260,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LoanTransaction newTransferTransaction = LoanTransaction.initiateTransfer(loan.getOffice(), loan, transferDate); final LoanTransaction newTransferTransaction = LoanTransaction.initiateTransfer(loan.getOffice(), loan, transferDate);
loan.addLoanTransaction(newTransferTransaction); loan.addLoanTransaction(newTransferTransaction);
loan.setLoanStatus(LoanStatus.TRANSFER_IN_PROGRESS.getValue()); LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine();
LoanStatus loanStatusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_INITIATE_TRANSFER, loan.status());
loan.setLoanStatus(loanStatusEnum.getValue());
this.loanTransactionRepository.saveAndFlush(newTransferTransaction); this.loanTransactionRepository.saveAndFlush(newTransferTransaction);
saveLoanWithDataIntegrityViolationChecks(loan); saveLoanWithDataIntegrityViolationChecks(loan);
@@ -2281,11 +2283,14 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.approveTransfer(acceptedInOffice, loan, transferDate); final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.approveTransfer(acceptedInOffice, loan, transferDate);
loan.addLoanTransaction(newTransferAcceptanceTransaction); loan.addLoanTransaction(newTransferAcceptanceTransaction);
LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine();
LoanStatus loanStatusEnum;
if (loan.getTotalOverpaid() != null) { if (loan.getTotalOverpaid() != null) {
loan.setLoanStatus(LoanStatus.OVERPAID.getValue()); loanStatusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_OVERPAYMENT, loan.status());
} else { } else {
loan.setLoanStatus(LoanStatus.ACTIVE.getValue()); loanStatusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan.status());
} }
loan.setLoanStatus(loanStatusEnum.getValue());
if (loanOfficer != null) { if (loanOfficer != null) {
loan.reassignLoanOfficer(loanOfficer, transferDate); loan.reassignLoanOfficer(loanOfficer, transferDate);
} }
@@ -2310,7 +2315,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.withdrawTransfer(loan.getOffice(), loan, transferDate); final LoanTransaction newTransferAcceptanceTransaction = LoanTransaction.withdrawTransfer(loan.getOffice(), loan, transferDate);
loan.addLoanTransaction(newTransferAcceptanceTransaction); loan.addLoanTransaction(newTransferAcceptanceTransaction);
loan.setLoanStatus(LoanStatus.ACTIVE.getValue()); LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine();
LoanStatus loanStatusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_WITHDRAW_TRANSFER, loan.status());
loan.setLoanStatus(loanStatusEnum.getValue());
this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction); this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction);
saveLoanWithDataIntegrityViolationChecks(loan); saveLoanWithDataIntegrityViolationChecks(loan);
@@ -2326,7 +2333,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
public void rejectLoanTransfer(final Loan loan) { public void rejectLoanTransfer(final Loan loan) {
this.loanAssembler.setHelpers(loan); this.loanAssembler.setHelpers(loan);
businessEventNotifierService.notifyPreBusinessEvent(new LoanRejectTransferBusinessEvent(loan)); businessEventNotifierService.notifyPreBusinessEvent(new LoanRejectTransferBusinessEvent(loan));
loan.setLoanStatus(LoanStatus.TRANSFER_ON_HOLD.getValue()); LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine();
LoanStatus loanStatusEnum = loanLifecycleStateMachine.transition(LoanEvent.LOAN_REJECT_TRANSFER, loan.status());
loan.setLoanStatus(loanStatusEnum.getValue());
saveLoanWithDataIntegrityViolationChecks(loan); saveLoanWithDataIntegrityViolationChecks(loan);
businessEventNotifierService.notifyPostBusinessEvent(new LoanRejectTransferBusinessEvent(loan)); businessEventNotifierService.notifyPostBusinessEvent(new LoanRejectTransferBusinessEvent(loan));
} }
@@ -2857,8 +2866,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
ChangedTransactionDetail changedTransactionDetail = loan.undoWrittenOff(existingTransactionIds, existingReversedTransactionIds, ChangedTransactionDetail changedTransactionDetail = loan.undoWrittenOff(defaultLoanLifecycleStateMachine(), existingTransactionIds,
scheduleGeneratorDTO); existingReversedTransactionIds, scheduleGeneratorDTO);
if (changedTransactionDetail != null) { if (changedTransactionDetail != null) {
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
this.loanTransactionRepository.save(mapEntry.getValue()); this.loanTransactionRepository.save(mapEntry.getValue());