FINERACT-1773: Allow 'specific due date charges' to be applied after the maturity date

This commit is contained in:
Adam Saghy
2022-10-14 17:45:29 +02:00
committed by Arnold Gálovics
parent 12e548ad82
commit b33e3ac892
7 changed files with 532 additions and 20 deletions
@@ -37,17 +37,20 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
@Override
public LoanStatus dryTransition(final LoanEvent loanEvent, final Loan loan) {
return getNextState(loanEvent, loan);
LoanStatus nextStatus = getNextState(loanEvent, loan);
return nextStatus != null ? nextStatus : loan.status();
}
@Override
public void transition(final LoanEvent loanEvent, final Loan loan) {
LoanStatus newState = getNextState(loanEvent, loan);
loan.setLoanStatus(newState.getValue());
if (newState != null) {
loan.setLoanStatus(newState.getValue());
if (isNotLoanCreation(loanEvent)) {
// in case of Loan creation, a LoanCreatedBusinessEvent is also raised, no need to send a status change
businessEventNotifierService.notifyPostBusinessEvent(new LoanStatusChangedBusinessEvent(loan));
if (isNotLoanCreation(loanEvent)) {
// in case of Loan creation, a LoanCreatedBusinessEvent is also raised, no need to send a status change
businessEventNotifierService.notifyPostBusinessEvent(new LoanStatusChangedBusinessEvent(loan));
}
}
}
@@ -62,7 +65,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
}
LoanStatus from = loan.status();
LoanStatus newState = from;
LoanStatus newState = null;
switch (loanEvent) {
case LOAN_REJECTED:
@@ -97,7 +100,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
break;
case LOAN_CHARGE_PAYMENT:
case LOAN_REPAYMENT_OR_WAIVER:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.ACTIVE, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.OVERPAID)) {
if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.OVERPAID)) {
newState = activeTransition();
}
break;
@@ -116,11 +119,6 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
newState = closedRescheduleOutstandingAmountTransition();
}
break;
case INTERST_REBATE_OWED:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET)) {
newState = closeObligationsMetTransition();
}
break;
case LOAN_OVERPAYMENT:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.ACTIVE)) {
newState = overpaidTransition();
@@ -141,8 +139,6 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
case LOAN_WITHDRAW_TRANSFER:
newState = activeTransition();
break;
case LOAN_CLOSED:
break;
case WRITE_OFF_OUTSTANDING_UNDO:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_WRITTEN_OFF)) {
newState = activeTransition();
@@ -151,6 +147,11 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
case LOAN_CREDIT_BALANCE_REFUND:
newState = closeObligationsMetTransition();
break;
case LOAN_CHARGE_ADDED:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET)) {
newState = activeTransition();
}
break;
case LOAN_CHARGEBACK:
if (anyOfAllowedWhenComingFrom(from, LoanStatus.CLOSED_OBLIGATIONS_MET, LoanStatus.OVERPAID)) {
newState = activeTransition();
@@ -572,8 +572,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
public void addLoanCharge(final LoanCharge loanCharge) {
validateLoanIsNotClosed(loanCharge);
if (isChargesAdditionAllowed() && loanCharge.isDueAtDisbursement()) {
// Note: added this constraint to restrict adding disbursement
// charges to a loan
@@ -596,7 +594,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
loanCharge.update(this);
final BigDecimal amount = calculateAmountPercentageAppliedTo(loanCharge);
BigDecimal chargeAmt = BigDecimal.ZERO;
BigDecimal chargeAmt;
BigDecimal totalChargeAmt = BigDecimal.ZERO;
if (loanCharge.getChargeCalculation().isPercentageBased()) {
chargeAmt = loanCharge.getPercentage();
@@ -626,6 +624,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), charges());
updateLoanSummaryDerivedFields();
loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGE_ADDED, this);
}
public ChangedTransactionDetail reprocessTransactions() {
@@ -743,7 +742,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
private void validateChargeHasValidSpecifiedDateIfApplicable(final LoanCharge loanCharge, final LocalDate disbursementDate,
final LocalDate lastRepaymentPeriodDueDate) {
if (loanCharge.isSpecifiedDueDate()
if (isInterestBearing() && loanCharge.isSpecifiedDueDate()
&& !loanCharge.isDueForCollectionFromAndUpToAndIncluding(disbursementDate, lastRepaymentPeriodDueDate)) {
final String defaultUserMessage = "This charge with specified due date cannot be added as the it is not in schedule range.";
throw new LoanChargeCannotBeAddedException("loanCharge", "specified.due.date.outside.range", defaultUserMessage,
@@ -5242,6 +5241,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom {
return null;
}
public boolean isInterestBearing() {
return getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()
&& BigDecimal.ZERO.compareTo(getLoanRepaymentScheduleDetail().getAnnualNominalInterestRate()) < 0;
}
public LocalDate getExpectedMaturityDate() {
return this.expectedMaturityDate;
}
@@ -49,5 +49,6 @@ public enum LoanEvent {
LOAN_REJECT_TRANSFER, //
LOAN_WITHDRAW_TRANSFER, //
LOAN_CREDIT_BALANCE_REFUND, //
LOAN_CHARGEBACK;
LOAN_CHARGEBACK, //
LOAN_CHARGE_ADDED;
}
@@ -142,7 +142,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
for (final LoanTransaction loanTransaction : transactionstoBeProcessed) {
// TODO: analyze and remove this
if (!loanTransaction.getTypeOf().equals(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN)) {
final Comparator<LoanRepaymentScheduleInstallment> byDate = new Comparator<LoanRepaymentScheduleInstallment>() {
@@ -1773,6 +1773,22 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
}
}
if (!loan.isInterestBearing() && loanCharge.isSpecifiedDueDate() && loanCharge.getDueDate().isAfter(loan.getMaturityDate())) {
LoanRepaymentScheduleInstallment latestRepaymentScheduleInstalment = loan.getRepaymentScheduleInstallments()
.get(loan.getLoanRepaymentScheduleInstallmentsSize() - 1);
if (loanCharge.getDueDate().isAfter(latestRepaymentScheduleInstalment.getDueDate())) {
if (latestRepaymentScheduleInstalment.isAdditional()) {
latestRepaymentScheduleInstalment.updateDueDate(loanCharge.getDueDate());
} else {
final LoanRepaymentScheduleInstallment installment = new LoanRepaymentScheduleInstallment(loan,
(loan.getLoanRepaymentScheduleInstallmentsSize() + 1), latestRepaymentScheduleInstalment.getDueDate(),
loanCharge.getDueDate(), BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO, false, null);
installment.markAsAdditional();
loan.addLoanRepaymentScheduleInstallment(installment);
}
}
}
loan.addLoanCharge(loanCharge);
this.loanChargeRepository.saveAndFlush(loanCharge);