FINERACT-2090: restructure loan disbursements

more refactors
This commit is contained in:
Kristof Jozsa
2024-07-12 11:43:49 +02:00
committed by Adam Saghy
parent 79f32d32a3
commit c9d1b81cc6
21 changed files with 379 additions and 385 deletions
@@ -158,15 +158,13 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
ExternalAssetOwnerTransferDetails details = new ExternalAssetOwnerTransferDetails(); ExternalAssetOwnerTransferDetails details = new ExternalAssetOwnerTransferDetails();
details.setExternalAssetOwnerTransfer(externalAssetOwnerTransfer); details.setExternalAssetOwnerTransfer(externalAssetOwnerTransfer);
details.setTotalOutstanding(Objects.requireNonNullElse(loan.getLoanSummary().getTotalOutstanding(), BigDecimal.ZERO)); details.setTotalOutstanding(Objects.requireNonNullElse(loan.getSummary().getTotalOutstanding(), BigDecimal.ZERO));
details.setTotalPrincipalOutstanding( details.setTotalPrincipalOutstanding(Objects.requireNonNullElse(loan.getSummary().getTotalPrincipalOutstanding(), BigDecimal.ZERO));
Objects.requireNonNullElse(loan.getLoanSummary().getTotalPrincipalOutstanding(), BigDecimal.ZERO)); details.setTotalInterestOutstanding(Objects.requireNonNullElse(loan.getSummary().getTotalInterestOutstanding(), BigDecimal.ZERO));
details.setTotalInterestOutstanding(
Objects.requireNonNullElse(loan.getLoanSummary().getTotalInterestOutstanding(), BigDecimal.ZERO));
details.setTotalFeeChargesOutstanding( details.setTotalFeeChargesOutstanding(
Objects.requireNonNullElse(loan.getLoanSummary().getTotalFeeChargesOutstanding(), BigDecimal.ZERO)); Objects.requireNonNullElse(loan.getSummary().getTotalFeeChargesOutstanding(), BigDecimal.ZERO));
details.setTotalPenaltyChargesOutstanding( details.setTotalPenaltyChargesOutstanding(
Objects.requireNonNullElse(loan.getLoanSummary().getTotalPenaltyChargesOutstanding(), BigDecimal.ZERO)); Objects.requireNonNullElse(loan.getSummary().getTotalPenaltyChargesOutstanding(), BigDecimal.ZERO));
details.setTotalOverpaid(Objects.requireNonNullElse(loan.getTotalOverpaid(), BigDecimal.ZERO)); details.setTotalOverpaid(Objects.requireNonNullElse(loan.getTotalOverpaid(), BigDecimal.ZERO));
return details; return details;
} }
@@ -179,7 +177,7 @@ public class LoanAccountOwnerTransferBusinessStep implements LoanCOBBusinessStep
} }
private boolean isTransferable(final Loan loan) { private boolean isTransferable(final Loan loan) {
return MathUtil.nullToDefault(loan.getLoanSummary().getTotalOutstanding(), BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0; return MathUtil.nullToDefault(loan.getSummary().getTotalOutstanding(), BigDecimal.ZERO).compareTo(BigDecimal.ZERO) > 0;
} }
private void handleSameDaySaleAndBuyback(final LocalDate settlementDate, final List<ExternalAssetOwnerTransfer> transferDataList, private void handleSameDaySaleAndBuyback(final LocalDate settlementDate, final List<ExternalAssetOwnerTransfer> transferDataList,
@@ -80,10 +80,10 @@ public class AccountingServiceImpl implements AccountingService {
// transaction properties // transaction properties
final Long transactionId = transfer.getId(); final Long transactionId = transfer.getId();
final LocalDate transactionDate = transfer.getSettlementDate(); final LocalDate transactionDate = transfer.getSettlementDate();
final BigDecimal principalAmount = loan.getLoanSummary().getTotalPrincipalOutstanding(); final BigDecimal principalAmount = loan.getSummary().getTotalPrincipalOutstanding();
final BigDecimal interestAmount = loan.getLoanSummary().getTotalInterestOutstanding(); final BigDecimal interestAmount = loan.getSummary().getTotalInterestOutstanding();
final BigDecimal feesAmount = loan.getLoanSummary().getTotalFeeChargesOutstanding(); final BigDecimal feesAmount = loan.getSummary().getTotalFeeChargesOutstanding();
final BigDecimal penaltiesAmount = loan.getLoanSummary().getTotalPenaltyChargesOutstanding(); final BigDecimal penaltiesAmount = loan.getSummary().getTotalPenaltyChargesOutstanding();
final BigDecimal overPaymentAmount = loan.getTotalOverpaid(); final BigDecimal overPaymentAmount = loan.getTotalOverpaid();
// Moving money to asset transfer account // Moving money to asset transfer account
@@ -165,15 +165,13 @@ public class LoanAccountOwnerTransferServiceImpl implements LoanAccountOwnerTran
ExternalAssetOwnerTransfer externalAssetOwnerTransfer) { ExternalAssetOwnerTransfer externalAssetOwnerTransfer) {
ExternalAssetOwnerTransferDetails details = new ExternalAssetOwnerTransferDetails(); ExternalAssetOwnerTransferDetails details = new ExternalAssetOwnerTransferDetails();
details.setExternalAssetOwnerTransfer(externalAssetOwnerTransfer); details.setExternalAssetOwnerTransfer(externalAssetOwnerTransfer);
details.setTotalOutstanding(Objects.requireNonNullElse(loan.getLoanSummary().getTotalOutstanding(), BigDecimal.ZERO)); details.setTotalOutstanding(Objects.requireNonNullElse(loan.getSummary().getTotalOutstanding(), BigDecimal.ZERO));
details.setTotalPrincipalOutstanding( details.setTotalPrincipalOutstanding(Objects.requireNonNullElse(loan.getSummary().getTotalPrincipalOutstanding(), BigDecimal.ZERO));
Objects.requireNonNullElse(loan.getLoanSummary().getTotalPrincipalOutstanding(), BigDecimal.ZERO)); details.setTotalInterestOutstanding(Objects.requireNonNullElse(loan.getSummary().getTotalInterestOutstanding(), BigDecimal.ZERO));
details.setTotalInterestOutstanding(
Objects.requireNonNullElse(loan.getLoanSummary().getTotalInterestOutstanding(), BigDecimal.ZERO));
details.setTotalFeeChargesOutstanding( details.setTotalFeeChargesOutstanding(
Objects.requireNonNullElse(loan.getLoanSummary().getTotalFeeChargesOutstanding(), BigDecimal.ZERO)); Objects.requireNonNullElse(loan.getSummary().getTotalFeeChargesOutstanding(), BigDecimal.ZERO));
details.setTotalPenaltyChargesOutstanding( details.setTotalPenaltyChargesOutstanding(
Objects.requireNonNullElse(loan.getLoanSummary().getTotalPenaltyChargesOutstanding(), BigDecimal.ZERO)); Objects.requireNonNullElse(loan.getSummary().getTotalPenaltyChargesOutstanding(), BigDecimal.ZERO));
details.setTotalOverpaid(Objects.requireNonNullElse(loan.getTotalOverpaid(), BigDecimal.ZERO)); details.setTotalOverpaid(Objects.requireNonNullElse(loan.getTotalOverpaid(), BigDecimal.ZERO));
return details; return details;
} }
@@ -203,7 +203,7 @@ public class LoanAccountOwnerTransferBusinessStepTest {
final Loan loanForProcessing = Mockito.mock(Loan.class); final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L); when(loanForProcessing.getId()).thenReturn(1L);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class); LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK); when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK);
@@ -248,7 +248,7 @@ public class LoanAccountOwnerTransferBusinessStepTest {
ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class); ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer); when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class); LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ONE); when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ONE);
when(newTransfer.getStatus()).thenReturn(ExternalTransferStatus.ACTIVE); when(newTransfer.getStatus()).thenReturn(ExternalTransferStatus.ACTIVE);
// when // when
@@ -296,7 +296,7 @@ public class LoanAccountOwnerTransferBusinessStepTest {
ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class); ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer); when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class); LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ZERO); when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ZERO);
when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ZERO); when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ZERO);
when(newTransfer.getStatus()).thenReturn(ExternalTransferStatus.DECLINED); when(newTransfer.getStatus()).thenReturn(ExternalTransferStatus.DECLINED);
@@ -340,7 +340,7 @@ public class LoanAccountOwnerTransferBusinessStepTest {
ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class); ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer); when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class); LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ONE.negate()); when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ONE.negate());
when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ONE.negate()); when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ONE.negate());
when(newTransfer.getStatus()).thenReturn(ExternalTransferStatus.DECLINED); when(newTransfer.getStatus()).thenReturn(ExternalTransferStatus.DECLINED);
@@ -389,7 +389,7 @@ public class LoanAccountOwnerTransferBusinessStepTest {
final Loan loanForProcessing = Mockito.mock(Loan.class); final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L); when(loanForProcessing.getId()).thenReturn(1L);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class); LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ZERO); when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ZERO);
when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ONE); when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ONE);
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class); ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
@@ -133,7 +133,7 @@ public class LoanAccountOwnerTransferServiceTest {
final Loan loanForProcessing = Mockito.mock(Loan.class); final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L); when(loanForProcessing.getId()).thenReturn(1L);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class); LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
ExternalAssetOwnerTransfer pendingBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class); ExternalAssetOwnerTransfer pendingBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(pendingBuybackTransfer.getStatus()).thenReturn(BUYBACK); when(pendingBuybackTransfer.getStatus()).thenReturn(BUYBACK);
@@ -85,7 +85,7 @@ public class DefaultLoanLifecycleStateMachine implements LoanLifecycleStateMachi
if (anyOfAllowedWhenComingFrom(from, LoanStatus.APPROVED, LoanStatus.CLOSED_OBLIGATIONS_MET)) { if (anyOfAllowedWhenComingFrom(from, LoanStatus.APPROVED, LoanStatus.CLOSED_OBLIGATIONS_MET)) {
newState = activeTransition(); newState = activeTransition();
} else if (from.isOverpaid() && loan.getTotalOverpaidAsMoney().isZero()) { } else if (from.isOverpaid() && loan.getTotalOverpaidAsMoney().isZero()) {
if (loan.getLoanSummary().getTotalOutstanding(loan.getCurrency()).isZero()) { if (loan.getSummary().getTotalOutstanding(loan.getCurrency()).isZero()) {
newState = closeObligationsMetTransition(); newState = closeObligationsMetTransition();
} else { } else {
newState = activeTransition(); newState = activeTransition();
@@ -613,7 +613,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return this.loanRepaymentScheduleDetail.getNumberOfRepayments(); return this.loanRepaymentScheduleDetail.getNumberOfRepayments();
} }
private LoanSummary updateSummaryWithTotalFeeChargesDueAtDisbursement(final BigDecimal feeChargesDueAtDisbursement) { public LoanSummary updateSummaryWithTotalFeeChargesDueAtDisbursement(final BigDecimal feeChargesDueAtDisbursement) {
if (this.summary == null) { if (this.summary == null) {
this.summary = LoanSummary.create(feeChargesDueAtDisbursement); this.summary = LoanSummary.create(feeChargesDueAtDisbursement);
} else { } else {
@@ -634,7 +634,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
this.summary.updateTotalWaived(this.summary.getTotalWaived().subtract(amountWaived)); this.summary.updateTotalWaived(this.summary.getTotalWaived().subtract(amountWaived));
} }
private BigDecimal deriveSumTotalOfChargesDueAtDisbursement() { public BigDecimal deriveSumTotalOfChargesDueAtDisbursement() {
return getActiveCharges().stream() // return getActiveCharges().stream() //
.filter(LoanCharge::isDueAtDisbursement) // .filter(LoanCharge::isDueAtDisbursement) //
.map(LoanCharge::amount) // .map(LoanCharge::amount) //
@@ -1721,108 +1721,12 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public ChangedTransactionDetail disburse(final AppUser currentUser, final JsonCommand command, final Map<String, Object> actualChanges, public List<LoanDisbursementDetails> getDisbursedLoanDisbursementDetails() {
final ScheduleGeneratorDTO scheduleGeneratorDTO, final PaymentDetail paymentDetail) {
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed(ACTUAL_DISBURSEMENT_DATE);
this.disbursedBy = currentUser;
updateLoanScheduleDependentDerivedFields();
actualChanges.put(LOCALE, command.locale());
actualChanges.put(DATE_FORMAT, command.dateFormat());
actualChanges.put(ACTUAL_DISBURSEMENT_DATE, command.stringValueOfParameterNamed(ACTUAL_DISBURSEMENT_DATE));
HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO();
// validate if disbursement date is a holiday or a non-working day
validateDisbursementDateIsOnNonWorkingDay(holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay());
validateDisbursementDateIsOnHoliday(holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays());
regenerateRepaymentScheduleWithInterestRecalculationIfNeeded(this.repaymentScheduleDetail().isInterestRecalculationEnabled(),
isDisbursementMissed(), scheduleGeneratorDTO);
updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement());
updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate);
handleDisbursementTransaction(actualDisbursementDate, paymentDetail);
updateLoanSummaryDerivedFields();
final Money interestApplied = Money.of(getCurrency(), this.summary.getTotalInterestCharged());
/*
* Add an interest applied transaction of the interest is accrued upfront (Up front accrual), no accounting or
* cash based accounting is selected
*/
if (((isMultiDisburmentLoan() && getDisbursedLoanDisbursementDetails().size() == 1) || !isMultiDisburmentLoan())
&& isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) {
ExternalId externalId = ExternalId.empty();
if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) {
externalId = ExternalId.generate();
}
final LoanTransaction interestAppliedTransaction = LoanTransaction.accrueInterest(getOffice(), this, interestApplied,
actualDisbursementDate, externalId);
addLoanTransaction(interestAppliedTransaction);
}
ChangedTransactionDetail result = reprocessTransactionForDisbursement();
this.loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, this);
actualChanges.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus));
return result;
}
private void regenerateRepaymentScheduleWithInterestRecalculationIfNeeded(boolean interestRecalculationEnabledParam,
boolean disbursementMissedParam, ScheduleGeneratorDTO scheduleGeneratorDTO) {
LocalDate firstInstallmentDueDate = fetchRepaymentScheduleInstallment(1).getDueDate();
if ((interestRecalculationEnabledParam && (DateUtils.isBeforeBusinessDate(firstInstallmentDueDate) || disbursementMissedParam))) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
}
}
private List<LoanDisbursementDetails> getDisbursedLoanDisbursementDetails() {
return getDisbursementDetails().stream() // return getDisbursementDetails().stream() //
.filter(it -> it.actualDisbursementDate() != null) // .filter(it -> it.actualDisbursementDate() != null) //
.collect(Collectors.toList()); .collect(Collectors.toList());
} }
public void regenerateScheduleOnDisbursement(final ScheduleGeneratorDTO scheduleGeneratorDTO, final boolean recalculateSchedule,
final LocalDate actualDisbursementDate, BigDecimal emiAmount, LocalDate nextPossibleRepaymentDate,
LocalDate rescheduledRepaymentDate) {
boolean isEmiAmountChanged = false;
if ((this.loanProduct.isMultiDisburseLoan() || this.loanProduct.isCanDefineInstallmentAmount()) && emiAmount != null
&& emiAmount.compareTo(retriveLastEmiAmount()) != 0) {
if (this.loanProduct.isMultiDisburseLoan()) {
final LocalDate dateValue = null;
final boolean isSpecificToInstallment = false;
final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled = scheduleGeneratorDTO
.isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled();
LocalDate effectiveDateFrom = actualDisbursementDate;
if (!isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled && actualDisbursementDate.equals(nextPossibleRepaymentDate)) {
effectiveDateFrom = nextPossibleRepaymentDate.plusDays(1);
}
LoanTermVariations loanVariationTerms = new LoanTermVariations(LoanTermVariationType.EMI_AMOUNT.getValue(),
effectiveDateFrom, emiAmount, dateValue, isSpecificToInstallment, this, LoanStatus.ACTIVE.getValue());
this.loanTermVariations.add(loanVariationTerms);
} else {
this.fixedEmiAmount = emiAmount;
}
isEmiAmountChanged = true;
}
if (rescheduledRepaymentDate != null && this.loanProduct.isMultiDisburseLoan()) {
final boolean isSpecificToInstallment = false;
LoanTermVariations loanVariationTerms = new LoanTermVariations(LoanTermVariationType.DUE_DATE.getValue(),
nextPossibleRepaymentDate, emiAmount, rescheduledRepaymentDate, isSpecificToInstallment, this,
LoanStatus.ACTIVE.getValue());
this.loanTermVariations.add(loanVariationTerms);
}
if (isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate) || recalculateSchedule || isEmiAmountChanged
|| rescheduledRepaymentDate != null) {
if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
} else {
regenerateRepaymentSchedule(scheduleGeneratorDTO);
}
}
}
public boolean canDisburse(final LocalDate actualDisbursementDate) { public boolean canDisburse(final LocalDate actualDisbursementDate) {
LocalDate loanSubmittedOnDate = this.submittedOnDate; LocalDate loanSubmittedOnDate = this.submittedOnDate;
final LoanStatus statusEnum = this.loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_DISBURSED, this); final LoanStatus statusEnum = this.loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_DISBURSED, this);
@@ -1918,25 +1822,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
} }
} }
private ChangedTransactionDetail reprocessTransactionForDisbursement() {
ChangedTransactionDetail changedTransactionDetail = null;
if (this.loanProduct.isMultiDisburseLoan()) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing();
if (!allNonContraTransactionsPostDisbursement.isEmpty()) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory
.determineProcessor(this.transactionProcessingStrategyCode);
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(getDisbursementDate(),
allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(this);
}
this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values());
}
updateLoanSummaryDerivedFields();
}
return changedTransactionDetail;
}
private Collection<LoanDisbursementDetails> fetchUndisbursedDetail() { private Collection<LoanDisbursementDetails> fetchUndisbursedDetail() {
Collection<LoanDisbursementDetails> disbursementDetails = new ArrayList<>(); Collection<LoanDisbursementDetails> disbursementDetails = new ArrayList<>();
LocalDate date = null; LocalDate date = null;
@@ -1993,15 +1878,19 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
private boolean isDisbursementAllowed() { private boolean isDisbursementAllowed() {
List<LoanDisbursementDetails> disbursementDetails = getDisbursementDetails(); List<LoanDisbursementDetails> disbursementDetails = getDisbursementDetails();
return disbursementDetails == null || disbursementDetails.isEmpty() boolean isSingleDisburseLoanDisbursementAllowed = disbursementDetails == null || disbursementDetails.isEmpty()
|| disbursementDetails.stream().anyMatch(it -> it.actualDisbursementDate() == null); || disbursementDetails.stream().anyMatch(it -> it.actualDisbursementDate() == null);
boolean isMultiDisburseLoanDisbursementAllowed = isMultiDisburmentLoan()
&& (disbursementDetails == null || disbursementDetails.stream().filter(it -> it.actualDisbursementDate() != null)
.count() < loanProduct.getLoanProductTrancheDetails().maxTrancheCount());
return isSingleDisburseLoanDisbursementAllowed || isMultiDisburseLoanDisbursementAllowed;
} }
private boolean atLeastOnceDisbursed() { private boolean atLeastOnceDisbursed() {
return getDisbursementDetails().stream().anyMatch(it -> it.actualDisbursementDate() != null); return getDisbursementDetails().stream().anyMatch(it -> it.actualDisbursementDate() != null);
} }
private void updateLoanRepaymentPeriodsDerivedFields(final LocalDate actualDisbursementDate) { public void updateLoanRepaymentPeriodsDerivedFields(final LocalDate actualDisbursementDate) {
List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments(); List<LoanRepaymentScheduleInstallment> installments = getRepaymentScheduleInstallments();
for (final LoanRepaymentScheduleInstallment repaymentPeriod : installments) { for (final LoanRepaymentScheduleInstallment repaymentPeriod : installments) {
repaymentPeriod.updateDerivedFields(loanCurrency(), actualDisbursementDate); repaymentPeriod.updateDerivedFields(loanCurrency(), actualDisbursementDate);
@@ -2075,7 +1964,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return interestRate; return interestRate;
} }
private void handleDisbursementTransaction(final LocalDate disbursedOn, final PaymentDetail paymentDetail) { public void handleDisbursementTransaction(final LocalDate disbursedOn, final PaymentDetail paymentDetail) {
// add repayment transaction to track incoming money from client to mfi // add repayment transaction to track incoming money from client to mfi
// for (charges due at time of disbursement) // for (charges due at time of disbursement)
@@ -2099,11 +1988,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
* create a Charge applied transaction if Up front Accrual, None or Cash based accounting is enabled * create a Charge applied transaction if Up front Accrual, None or Cash based accounting is enabled
*/ */
if ((charge.getCharge().getChargeTimeType().equals(ChargeTimeType.DISBURSEMENT.getValue()) if ((charge.getCharge().getChargeTimeType().equals(ChargeTimeType.DISBURSEMENT.getValue())
&& disbursedOn.equals(actualDisbursementDate) && (actualDisbursementDate != null) && !charge.isWaived() && disbursedOn.equals(actualDisbursementDate) && !charge.isWaived() && !charge.isFullyPaid())
&& !charge.isFullyPaid())
|| (charge.getCharge().getChargeTimeType().equals(ChargeTimeType.TRANCHE_DISBURSEMENT.getValue()) || (charge.getCharge().getChargeTimeType().equals(ChargeTimeType.TRANCHE_DISBURSEMENT.getValue())
&& disbursedOn.equals(actualDisbursementDate) && (actualDisbursementDate != null) && !charge.isWaived() && disbursedOn.equals(actualDisbursementDate) && !charge.isWaived() && !charge.isFullyPaid())) {
&& !charge.isFullyPaid())) {
if (totalFeeChargesDueAtDisbursement.isGreaterThanZero() && !charge.getChargePaymentMode().isPaymentModeAccountTransfer()) { if (totalFeeChargesDueAtDisbursement.isGreaterThanZero() && !charge.getChargePaymentMode().isPaymentModeAccountTransfer()) {
charge.markAsFullyPaid(); charge.markAsFullyPaid();
// Add "Loan Charge Paid By" details to this transaction // Add "Loan Charge Paid By" details to this transaction
@@ -2244,7 +2131,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
actualChanges.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus)); actualChanges.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus));
final LocalDate actualDisbursementDate = getDisbursementDate(); final LocalDate actualDisbursementDate = getDisbursementDate();
final boolean isScheduleRegenerateRequired = isRepaymentScheduleRegenerationRequiredForDisbursement(actualDisbursementDate); final boolean isScheduleRegenerateRequired = isActualDisbursedOnDateEarlierOrLaterThanExpected(actualDisbursementDate);
this.actualDisbursementDate = null; this.actualDisbursementDate = null;
this.disbursedBy = null; this.disbursedBy = null;
boolean isDisbursedAmountChanged = !MathUtil.isEqualTo(approvedPrincipal, boolean isDisbursedAmountChanged = !MathUtil.isEqualTo(approvedPrincipal,
@@ -2461,7 +2348,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
} }
if (loanTransaction.isRecoveryRepayment() if (loanTransaction.isRecoveryRepayment()
&& loanTransaction.getAmount(loanCurrency()).getAmount().compareTo(getLoanSummary().getTotalWrittenOff()) > 0) { && loanTransaction.getAmount(loanCurrency()).getAmount().compareTo(getSummary().getTotalWrittenOff()) > 0) {
final String errorMessage = "The transaction amount cannot greater than the remaining written off amount."; final String errorMessage = "The transaction amount cannot greater than the remaining written off amount.";
throw new InvalidLoanStateTransitionException("transaction", "cannot.be.greater.than.total.written.off", errorMessage); throw new InvalidLoanStateTransitionException("transaction", "cannot.be.greater.than.total.written.off", errorMessage);
} }
@@ -3210,8 +3097,9 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
public boolean isAllTranchesNotDisbursed() { public boolean isAllTranchesNotDisbursed() {
LoanStatus actualLoanStatus = LoanStatus.fromInt(this.loanStatus); LoanStatus actualLoanStatus = LoanStatus.fromInt(this.loanStatus);
return this.loanProduct.isMultiDisburseLoan() && (actualLoanStatus.isActive() || actualLoanStatus.isApproved() boolean isInRightStatus = actualLoanStatus.isActive() || actualLoanStatus.isApproved() || actualLoanStatus.isClosedObligationsMet()
|| actualLoanStatus.isClosedObligationsMet() || actualLoanStatus.isOverpaid()) && isDisbursementAllowed(); || actualLoanStatus.isOverpaid();
return this.loanProduct.isMultiDisburseLoan() && isInRightStatus && isDisbursementAllowed();
} }
private boolean hasDisbursementTransaction() { private boolean hasDisbursementTransaction() {
@@ -3263,7 +3151,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return principal; return principal;
} }
private boolean isActualDisbursedOnDateEarlierOrLaterThanExpected(final LocalDate actualDisbursedOnDate) { public boolean isActualDisbursedOnDateEarlierOrLaterThanExpected(final LocalDate actualDisbursedOnDate) {
boolean isRegenerationRequired = false; boolean isRegenerationRequired = false;
if (this.loanProduct.isMultiDisburseLoan()) { if (this.loanProduct.isMultiDisburseLoan()) {
LoanDisbursementDetails details = fetchLastDisburseDetail(); LoanDisbursementDetails details = fetchLastDisburseDetail();
@@ -3274,10 +3162,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return isRegenerationRequired || !DateUtils.isEqual(actualDisbursedOnDate, this.expectedDisbursementDate); return isRegenerationRequired || !DateUtils.isEqual(actualDisbursedOnDate, this.expectedDisbursementDate);
} }
private boolean isRepaymentScheduleRegenerationRequiredForDisbursement(final LocalDate actualDisbursementDate) {
return isActualDisbursedOnDateEarlierOrLaterThanExpected(actualDisbursementDate);
}
private Money getTotalPaidInRepayments() { private Money getTotalPaidInRepayments() {
Money cumulativePaid = Money.zero(loanCurrency()); Money cumulativePaid = Money.zero(loanCurrency());
@@ -3814,20 +3698,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
return dueRepaymentPeriodDate.minusDays(1);// get 2n-1 range date from startDate return dueRepaymentPeriodDate.minusDays(1);// get 2n-1 range date from startDate
} }
private void validateDisbursementDateIsOnNonWorkingDay(final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) {
if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, getDisbursementDate())) {
final String errorMessage = "Expected disbursement date cannot be on a non working day";
throw new LoanApplicationDateException("disbursement.date.on.non.working.day", errorMessage, getExpectedDisbursedOnLocalDate());
}
}
private void validateDisbursementDateIsOnHoliday(final boolean allowTransactionsOnHoliday, final List<Holiday> holidays) {
if (!allowTransactionsOnHoliday && HolidayUtil.isHoliday(getDisbursementDate(), holidays)) {
final String errorMessage = "Expected disbursement date cannot be on a holiday";
throw new LoanApplicationDateException("disbursement.date.on.holiday", errorMessage, getExpectedDisbursedOnLocalDate());
}
}
public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays, public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays,
final boolean allowTransactionsOnNonWorkingDay) { final boolean allowTransactionsOnNonWorkingDay) {
if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) { if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) {
@@ -4838,13 +4708,6 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
loanProduct.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()); loanProduct.getLoanProductRelatedDetail().isEnableAccrualActivityPosting());
} }
/**
* @return Loan summary embedded object
**/
public LoanSummary getLoanSummary() {
return this.summary;
}
public void updateRescheduledByUser(AppUser rescheduledByUser) { public void updateRescheduledByUser(AppUser rescheduledByUser) {
this.rescheduledByUser = rescheduledByUser; this.rescheduledByUser = rescheduledByUser;
} }
@@ -5245,7 +5108,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
public LoanRepaymentScheduleInstallment fetchLoanForeclosureDetail(final LocalDate closureDate) { public LoanRepaymentScheduleInstallment fetchLoanForeclosureDetail(final LocalDate closureDate) {
Money[] receivables = retriveIncomeOutstandingTillDate(closureDate); Money[] receivables = retriveIncomeOutstandingTillDate(closureDate);
Money totalPrincipal = Money.of(getCurrency(), this.getLoanSummary().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 Set<LoanInterestRecalcualtionAdditionalDetails> compoundingDetails = null;
final LocalDate currentDate = DateUtils.getBusinessLocalDate(); final LocalDate currentDate = DateUtils.getBusinessLocalDate();
@@ -262,25 +262,25 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
public static LoanTransaction initiateTransfer(final Office office, final Loan loan, final LocalDate transferDate, public static LoanTransaction initiateTransfer(final Office office, final Loan loan, final LocalDate transferDate,
final ExternalId externalId) { final ExternalId externalId) {
return new LoanTransaction(loan, office, LoanTransactionType.INITIATE_TRANSFER.getValue(), transferDate, return new LoanTransaction(loan, office, LoanTransactionType.INITIATE_TRANSFER.getValue(), transferDate,
loan.getLoanSummary().getTotalOutstanding(), loan.getLoanSummary().getTotalPrincipalOutstanding(), loan.getSummary().getTotalOutstanding(), loan.getSummary().getTotalPrincipalOutstanding(),
loan.getLoanSummary().getTotalInterestOutstanding(), loan.getLoanSummary().getTotalFeeChargesOutstanding(), loan.getSummary().getTotalInterestOutstanding(), loan.getSummary().getTotalFeeChargesOutstanding(),
loan.getLoanSummary().getTotalPenaltyChargesOutstanding(), null, false, null, externalId); loan.getSummary().getTotalPenaltyChargesOutstanding(), null, false, null, externalId);
} }
public static LoanTransaction approveTransfer(final Office office, final Loan loan, final LocalDate transferDate, public static LoanTransaction approveTransfer(final Office office, final Loan loan, final LocalDate transferDate,
final ExternalId externalId) { final ExternalId externalId) {
return new LoanTransaction(loan, office, LoanTransactionType.APPROVE_TRANSFER.getValue(), transferDate, return new LoanTransaction(loan, office, LoanTransactionType.APPROVE_TRANSFER.getValue(), transferDate,
loan.getLoanSummary().getTotalOutstanding(), loan.getLoanSummary().getTotalPrincipalOutstanding(), loan.getSummary().getTotalOutstanding(), loan.getSummary().getTotalPrincipalOutstanding(),
loan.getLoanSummary().getTotalInterestOutstanding(), loan.getLoanSummary().getTotalFeeChargesOutstanding(), loan.getSummary().getTotalInterestOutstanding(), loan.getSummary().getTotalFeeChargesOutstanding(),
loan.getLoanSummary().getTotalPenaltyChargesOutstanding(), null, false, null, externalId); loan.getSummary().getTotalPenaltyChargesOutstanding(), null, false, null, externalId);
} }
public static LoanTransaction withdrawTransfer(final Office office, final Loan loan, final LocalDate transferDate, public static LoanTransaction withdrawTransfer(final Office office, final Loan loan, final LocalDate transferDate,
final ExternalId externalId) { final ExternalId externalId) {
return new LoanTransaction(loan, office, LoanTransactionType.WITHDRAW_TRANSFER.getValue(), transferDate, return new LoanTransaction(loan, office, LoanTransactionType.WITHDRAW_TRANSFER.getValue(), transferDate,
loan.getLoanSummary().getTotalOutstanding(), loan.getLoanSummary().getTotalPrincipalOutstanding(), loan.getSummary().getTotalOutstanding(), loan.getSummary().getTotalPrincipalOutstanding(),
loan.getLoanSummary().getTotalInterestOutstanding(), loan.getLoanSummary().getTotalFeeChargesOutstanding(), loan.getSummary().getTotalInterestOutstanding(), loan.getSummary().getTotalFeeChargesOutstanding(),
loan.getLoanSummary().getTotalPenaltyChargesOutstanding(), null, false, null, externalId); loan.getSummary().getTotalPenaltyChargesOutstanding(), null, false, null, externalId);
} }
public static LoanTransaction refund(final Office office, final Money amount, final PaymentDetail paymentDetail, public static LoanTransaction refund(final Office office, final Money amount, final PaymentDetail paymentDetail,
@@ -369,19 +369,19 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
} }
public static LoanTransaction chargeOff(final Loan loan, final LocalDate chargeOffDate, final ExternalId externalId) { public static LoanTransaction chargeOff(final Loan loan, final LocalDate chargeOffDate, final ExternalId externalId) {
BigDecimal principalPortion = loan.getLoanSummary().getTotalPrincipalOutstanding().compareTo(BigDecimal.ZERO) != 0 BigDecimal principalPortion = loan.getSummary().getTotalPrincipalOutstanding().compareTo(BigDecimal.ZERO) != 0
? loan.getLoanSummary().getTotalPrincipalOutstanding() ? loan.getSummary().getTotalPrincipalOutstanding()
: null; : null;
BigDecimal interestPortion = loan.getLoanSummary().getTotalInterestOutstanding().compareTo(BigDecimal.ZERO) != 0 BigDecimal interestPortion = loan.getSummary().getTotalInterestOutstanding().compareTo(BigDecimal.ZERO) != 0
? loan.getLoanSummary().getTotalInterestOutstanding() ? loan.getSummary().getTotalInterestOutstanding()
: null; : null;
BigDecimal feePortion = loan.getLoanSummary().getTotalFeeChargesOutstanding().compareTo(BigDecimal.ZERO) != 0 BigDecimal feePortion = loan.getSummary().getTotalFeeChargesOutstanding().compareTo(BigDecimal.ZERO) != 0
? loan.getLoanSummary().getTotalFeeChargesOutstanding() ? loan.getSummary().getTotalFeeChargesOutstanding()
: null; : null;
BigDecimal penaltyPortion = loan.getLoanSummary().getTotalPenaltyChargesOutstanding().compareTo(BigDecimal.ZERO) != 0 BigDecimal penaltyPortion = loan.getSummary().getTotalPenaltyChargesOutstanding().compareTo(BigDecimal.ZERO) != 0
? loan.getLoanSummary().getTotalPenaltyChargesOutstanding() ? loan.getSummary().getTotalPenaltyChargesOutstanding()
: null; : null;
BigDecimal totalOutstanding = loan.getLoanSummary().getTotalOutstanding(); BigDecimal totalOutstanding = loan.getSummary().getTotalOutstanding();
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.CHARGE_OFF.getValue(), chargeOffDate, totalOutstanding, return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.CHARGE_OFF.getValue(), chargeOffDate, totalOutstanding,
principalPortion, interestPortion, feePortion, penaltyPortion, null, false, null, externalId); principalPortion, interestPortion, feePortion, penaltyPortion, null, false, null, externalId);
@@ -2494,7 +2494,7 @@ public abstract class AbstractCumulativeLoanScheduleGenerator implements LoanSch
currency, applyInterestRecalculation); currency, applyInterestRecalculation);
retainedInstallments.addAll(newRepaymentScheduleInstallments); retainedInstallments.addAll(newRepaymentScheduleInstallments);
loanScheduleParams.getCompoundingDateVariations().putAll(compoundingDateVariations); loanScheduleParams.getCompoundingDateVariations().putAll(compoundingDateVariations);
loanApplicationTerms.updateTotalInterestDue(Money.of(currency, loan.getLoanSummary().getTotalInterestCharged())); loanApplicationTerms.updateTotalInterestDue(Money.of(currency, loan.getSummary().getTotalInterestCharged()));
} else { } else {
loanApplicationTerms.getLoanTermVariations().resetVariations(); loanApplicationTerms.getLoanTermVariations().resetVariations();
periods.clear(); periods.clear();
@@ -105,7 +105,7 @@ public class LoanArrearsAgingServiceImpl implements LoanArrearsAgingService {
Map<Long, List<LoanSchedulePeriodData>> scheduleDate = this.jdbcTemplate.query(originalScheduleExtractor.schema, Map<Long, List<LoanSchedulePeriodData>> scheduleDate = this.jdbcTemplate.query(originalScheduleExtractor.schema,
originalScheduleExtractor); originalScheduleExtractor);
if (scheduleDate.size() > 0) { if (scheduleDate.size() > 0) {
List<Map<String, Object>> transactions = getLoanSummary(loan.getId(), loan.getLoanSummary()); List<Map<String, Object>> transactions = getLoanSummary(loan.getId(), loan.getSummary());
updateScheduleWithPaidDetail(scheduleDate, transactions); updateScheduleWithPaidDetail(scheduleDate, transactions);
createInsertStatements(updateStatement, scheduleDate, count == 0); createInsertStatements(updateStatement, scheduleDate, count == 0);
if (updateStatement.size() == 1) { if (updateStatement.size() == 1) {
@@ -80,7 +80,7 @@ public class CheckLoanRepaymentDueBusinessStep implements LoanCOBBusinessStep {
LoanRepaymentScheduleInstallment repaymentScheduleInstallment, LocalDate repaymentDate, List<LoanStatus> nonDisbursedStatuses) { LoanRepaymentScheduleInstallment repaymentScheduleInstallment, LocalDate repaymentDate, List<LoanStatus> nonDisbursedStatuses) {
return repaymentDate.minusDays(numberOfDaysBeforeDueDateToRaiseEvent).equals(currentDate) return repaymentDate.minusDays(numberOfDaysBeforeDueDateToRaiseEvent).equals(currentDate)
&& !nonDisbursedStatuses.contains(loan.getStatus()) && !nonDisbursedStatuses.contains(loan.getStatus())
&& loan.getLoanSummary().getTotalOutstanding().compareTo(BigDecimal.ZERO) > 0 && loan.getSummary().getTotalOutstanding().compareTo(BigDecimal.ZERO) > 0
&& repaymentScheduleInstallment.getTotalOutstanding(loan.getCurrency()).isGreaterThanZero(); && repaymentScheduleInstallment.getTotalOutstanding(loan.getCurrency()).isGreaterThanZero();
} }
} }
@@ -45,8 +45,7 @@ public class CheckLoanRepaymentOverdueBusinessStep implements LoanCOBBusinessSte
public Loan execute(Loan loan) { public Loan execute(Loan loan) {
List<LoanStatus> nonDisbursedStatuses = Arrays.asList(LoanStatus.INVALID, LoanStatus.SUBMITTED_AND_PENDING_APPROVAL, List<LoanStatus> nonDisbursedStatuses = Arrays.asList(LoanStatus.INVALID, LoanStatus.SUBMITTED_AND_PENDING_APPROVAL,
LoanStatus.APPROVED); LoanStatus.APPROVED);
if (!nonDisbursedStatuses.contains(loan.getStatus()) if (!nonDisbursedStatuses.contains(loan.getStatus()) && loan.getSummary().getTotalOutstanding().compareTo(BigDecimal.ZERO) > 0) {
&& loan.getLoanSummary().getTotalOutstanding().compareTo(BigDecimal.ZERO) > 0) {
log.debug("start processing loan repayment overdue business step for loan with Id [{}]", loan.getId()); log.debug("start processing loan repayment overdue business step for loan with Id [{}]", loan.getId());
Long numberOfDaysAfterDueDateToRaiseEvent = configurationDomainService.retrieveRepaymentOverdueDays(); Long numberOfDaysAfterDueDateToRaiseEvent = configurationDomainService.retrieveRepaymentOverdueDays();
if (loan.getLoanProduct().getOverDueDaysForRepaymentEvent() != null) { if (loan.getLoanProduct().getOverDueDaysForRepaymentEvent() != null) {
@@ -97,8 +97,8 @@ public class LoanScheduleCalculationPlatformServiceImpl implements LoanScheduleC
if (loan.loanProduct().isMultiDisburseLoan()) { if (loan.loanProduct().isMultiDisburseLoan()) {
BigDecimal disbursedAmount = loan.getDisbursedAmount(); BigDecimal disbursedAmount = loan.getDisbursedAmount();
BigDecimal principalRepaid = loan.getLoanSummary().getTotalPrincipalRepaid(); BigDecimal principalRepaid = loan.getSummary().getTotalPrincipalRepaid();
BigDecimal principalWrittenOff = loan.getLoanSummary().getTotalPrincipalWrittenOff(); BigDecimal principalWrittenOff = loan.getSummary().getTotalPrincipalWrittenOff();
if (disbursedAmount.subtract(principalWrittenOff).subtract(principalRepaid).compareTo(BigDecimal.ZERO) <= 0) { if (disbursedAmount.subtract(principalWrittenOff).subtract(principalRepaid).compareTo(BigDecimal.ZERO) <= 0) {
return; return;
} }
@@ -38,11 +38,15 @@ import org.apache.commons.lang3.StringUtils;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
import org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; import org.apache.fineract.infrastructure.core.exception.InvalidJsonException;
import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException; import org.apache.fineract.infrastructure.core.exception.PlatformDataIntegrityException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.DateUtils; import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.dataqueries.data.EntityTables;
import org.apache.fineract.infrastructure.dataqueries.data.StatusEnum;
import org.apache.fineract.infrastructure.dataqueries.service.EntityDatatableChecksWritePlatformService;
import org.apache.fineract.organisation.holiday.domain.Holiday; import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.holiday.service.HolidayUtil; import org.apache.fineract.organisation.holiday.service.HolidayUtil;
import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency; import org.apache.fineract.organisation.monetary.domain.ApplicationCurrency;
@@ -52,26 +56,35 @@ import org.apache.fineract.organisation.monetary.exception.CurrencyNotFoundExcep
import org.apache.fineract.organisation.workingdays.domain.WorkingDays; import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil; import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil;
import org.apache.fineract.portfolio.calendar.domain.Calendar; import org.apache.fineract.portfolio.calendar.domain.Calendar;
import org.apache.fineract.portfolio.calendar.domain.CalendarEntityType;
import org.apache.fineract.portfolio.calendar.domain.CalendarInstance; import org.apache.fineract.portfolio.calendar.domain.CalendarInstance;
import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository;
import org.apache.fineract.portfolio.calendar.exception.NotValidRecurringDateException; import org.apache.fineract.portfolio.calendar.exception.NotValidRecurringDateException;
import org.apache.fineract.portfolio.calendar.service.CalendarUtils; import org.apache.fineract.portfolio.calendar.service.CalendarUtils;
import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
import org.apache.fineract.portfolio.collateralmanagement.exception.LoanCollateralAmountNotSufficientException;
import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
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.LoanCollateralManagement;
import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
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.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException;
import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException; import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException; import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.apache.fineract.portfolio.loanaccount.exception.LoanRepaymentScheduleNotFoundException; import org.apache.fineract.portfolio.loanaccount.exception.LoanRepaymentScheduleNotFoundException;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.jetbrains.annotations.NotNull;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
@@ -81,8 +94,11 @@ public final class LoanTransactionValidator {
private final FromJsonHelper fromApiJsonHelper; private final FromJsonHelper fromApiJsonHelper;
private final LoanApplicationValidator fromApiJsonDeserializer; private final LoanApplicationValidator fromApiJsonDeserializer;
private final LoanRepository loanRepository; private final LoanRepository loanRepository;
private final LoanRepositoryWrapper loanRepositoryWrapper;
private final ApplicationCurrencyRepository applicationCurrencyRepository; private final ApplicationCurrencyRepository applicationCurrencyRepository;
private final LoanUtilService loanUtilService; private final LoanUtilService loanUtilService;
private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService;
private final CalendarInstanceRepository calendarInstanceRepository;
private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) { private void throwExceptionIfValidationWarningsExist(final List<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) { if (!dataValidationErrors.isEmpty()) {
@@ -91,13 +107,129 @@ public final class LoanTransactionValidator {
} }
} }
public void validateDisbursement(final String json, boolean isAccountTransfer) { public void validateDisbursement(JsonCommand command, boolean isAccountTransfer, Long loanId) {
String json = command.json();
if (StringUtils.isBlank(json)) { if (StringUtils.isBlank(json)) {
throw new InvalidJsonException(); throw new InvalidJsonException();
} }
Set<String> disbursementParameters = null; final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, getDisbursementParameters(isAccountTransfer));
LoanApplicationValidator.validateOrThrow("loan.disbursement", baseDataValidator -> {
final JsonElement element = this.fromApiJsonHelper.parse(json);
final LocalDate actualDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("actualDisbursementDate", element);
baseDataValidator.reset().parameter("actualDisbursementDate").value(actualDisbursementDate).notNull();
final String note = this.fromApiJsonHelper.extractStringNamed("note", element);
baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000);
final BigDecimal principal = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(LoanApiConstants.principalDisbursedParameterName, element);
baseDataValidator.reset().parameter(LoanApiConstants.principalDisbursedParameterName).value(principal).ignoreIfNull()
.positiveAmount();
final BigDecimal netDisbursalAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(LoanApiConstants.disbursementNetDisbursalAmountParameterName, element);
baseDataValidator.reset().parameter(LoanApiConstants.disbursementNetDisbursalAmountParameterName).value(netDisbursalAmount)
.ignoreIfNull().positiveAmount();
final BigDecimal emiAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedEmiAmountParameterName, element);
baseDataValidator.reset().parameter(LoanApiConstants.fixedEmiAmountParameterName).value(emiAmount).ignoreIfNull()
.positiveAmount().notGreaterThanMax(principal);
validatePaymentDetails(baseDataValidator, element);
if (command.parameterExists("postDatedChecks")) {
this.validateDisbursementWithPostDatedChecks(command.json(), loanId);
}
final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true);
validateLoanClientIsActive(loan);
validateLoanGroupIsActive(loan);
if (loan.isChargedOff() && DateUtils.isBefore(actualDisbursementDate, loan.getChargedOffOnDate())) {
throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: "
+ loan.getId()
+ " backdated transaction is not allowed. Transaction date cannot be earlier than the charge-off date of the loan",
loan.getId());
}
boolean isSingleDisburseLoan = !loan.getLoanProduct().isMultiDisburseLoan();
boolean isSingleDisburseNotApprovedOrDisbursedAlready = isSingleDisburseLoan && !(loan.isApproved() && loan.isNotDisbursed());
boolean isMultiDisburseLoanAndAllTranchesDisbursed = loan.getLoanProduct().isMultiDisburseLoan()
&& !loan.isAllTranchesNotDisbursed();
if (isSingleDisburseNotApprovedOrDisbursedAlready || isMultiDisburseLoanAndAllTranchesDisbursed) {
final String defaultUserMessage = "Loan Disbursal is not allowed. Loan Account is not in approved and not disbursed state.";
final ApiParameterError error = ApiParameterError
.generalError("error.msg.loan.disbursal.account.is.not.approve.not.disbursed.state", defaultUserMessage);
baseDataValidator.getDataValidationErrors().add(error);
}
final BigDecimal disbursedAmount = loan.getDisbursedAmount();
final Set<LoanCollateralManagement> loanCollateralManagements = loan.getLoanCollateralManagements();
if ((loanCollateralManagements != null && !loanCollateralManagements.isEmpty()) && loan.getLoanType().isIndividualAccount()) {
BigDecimal totalCollateral = collectTotalCollateral(loanCollateralManagements);
// Validate the loan collateral value against the disbursedAmount
if (disbursedAmount.compareTo(totalCollateral) > 0) {
throw new LoanCollateralAmountNotSufficientException(disbursedAmount);
}
}
// validate ActualDisbursement Date Against Expected Disbursement Date
LoanProduct loanProduct = loan.loanProduct();
if (loanProduct.isSyncExpectedWithDisbursementDate()) {
if (!loan.getExpectedDisbursedOnLocalDate().equals(actualDisbursementDate)) {
throw new DateMismatchException(actualDisbursementDate, loan.getExpectedDisbursedOnLocalDate());
}
}
entityDatatableChecksWritePlatformService.runTheCheckForProduct(loan.getId(), EntityTables.LOAN.getName(),
StatusEnum.DISBURSE.getValue(), EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId());
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null);
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(),
CalendarEntityType.LOANS.getValue());
if (loan.isSyncDisbursementWithMeeting()) {
validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance,
scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(), scheduleGeneratorDTO.getNumberOfdays());
}
// validate if disbursement date is a holiday or a non-working day
HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO();
if (!holidayDetailDTO.isAllowTransactionsOnNonWorkingDay()
&& !WorkingDaysUtil.isWorkingDay(holidayDetailDTO.getWorkingDays(), loan.getDisbursementDate())) {
final String errorMessage = "Expected disbursement date cannot be on a non working day";
throw new LoanApplicationDateException("disbursement.date.on.non.working.day", errorMessage,
loan.getExpectedDisbursedOnLocalDate());
}
if (!holidayDetailDTO.isAllowTransactionsOnHoliday()
&& HolidayUtil.isHoliday(loan.getDisbursementDate(), holidayDetailDTO.getHolidays())) {
final String errorMessage = "Expected disbursement date cannot be on a holiday";
throw new LoanApplicationDateException("disbursement.date.on.holiday", errorMessage,
loan.getExpectedDisbursedOnLocalDate());
}
});
}
private static @NotNull BigDecimal collectTotalCollateral(Set<LoanCollateralManagement> loanCollateralManagements) {
BigDecimal totalCollateral = BigDecimal.ZERO;
for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) {
BigDecimal quantity = loanCollateralManagement.getQuantity();
BigDecimal pctToBase = loanCollateralManagement.getClientCollateralManagement().getCollaterals().getPctToBase();
BigDecimal basePrice = loanCollateralManagement.getClientCollateralManagement().getCollaterals().getBasePrice();
totalCollateral = totalCollateral.add(quantity.multiply(basePrice).multiply(pctToBase).divide(BigDecimal.valueOf(100)));
}
return totalCollateral;
}
private static @NotNull Set<String> getDisbursementParameters(boolean isAccountTransfer) {
Set<String> disbursementParameters;
if (isAccountTransfer) { if (isAccountTransfer) {
disbursementParameters = new HashSet<>(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale", "dateFormat", disbursementParameters = new HashSet<>(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale", "dateFormat",
@@ -109,38 +241,7 @@ public final class LoanTransactionValidator {
LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.fixedEmiAmountParameterName, LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.fixedEmiAmountParameterName,
LoanApiConstants.postDatedChecks, LoanApiConstants.disbursementNetDisbursalAmountParameterName)); LoanApiConstants.postDatedChecks, LoanApiConstants.disbursementNetDisbursalAmountParameterName));
} }
return disbursementParameters;
final Type typeOfMap = new TypeToken<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, disbursementParameters);
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.disbursement");
final JsonElement element = this.fromApiJsonHelper.parse(json);
final LocalDate actualDisbursementDate = this.fromApiJsonHelper.extractLocalDateNamed("actualDisbursementDate", element);
baseDataValidator.reset().parameter("actualDisbursementDate").value(actualDisbursementDate).notNull();
final String note = this.fromApiJsonHelper.extractStringNamed("note", element);
baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000);
final BigDecimal principal = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(LoanApiConstants.principalDisbursedParameterName, element);
baseDataValidator.reset().parameter(LoanApiConstants.principalDisbursedParameterName).value(principal).ignoreIfNull()
.positiveAmount();
final BigDecimal netDisbursalAmount = this.fromApiJsonHelper
.extractBigDecimalWithLocaleNamed(LoanApiConstants.disbursementNetDisbursalAmountParameterName, element);
baseDataValidator.reset().parameter(LoanApiConstants.disbursementNetDisbursalAmountParameterName).value(netDisbursalAmount)
.ignoreIfNull().positiveAmount();
final BigDecimal emiAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed(LoanApiConstants.fixedEmiAmountParameterName,
element);
baseDataValidator.reset().parameter(LoanApiConstants.fixedEmiAmountParameterName).value(emiAmount).ignoreIfNull().positiveAmount()
.notGreaterThanMax(principal);
validatePaymentDetails(baseDataValidator, element);
throwExceptionIfValidationWarningsExist(dataValidationErrors);
} }
public void validateDisbursementWithPostDatedChecks(final String json, final Long loanId) { public void validateDisbursementWithPostDatedChecks(final String json, final Long loanId) {
@@ -46,6 +46,7 @@ import org.apache.fineract.cob.service.LoanAccountLockService;
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;
import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService; import org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer;
import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.data.ApiParameterError;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
@@ -135,7 +136,6 @@ import org.apache.fineract.portfolio.calendar.exception.CalendarParameterUpdateN
import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.domain.Client;
import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException;
import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagement; import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagement;
import org.apache.fineract.portfolio.collateralmanagement.exception.LoanCollateralAmountNotSufficientException;
import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkDisbursalCommand; import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkDisbursalCommand;
import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand; import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand;
import org.apache.fineract.portfolio.collectionsheet.command.SingleDisbursalCommand; import org.apache.fineract.portfolio.collectionsheet.command.SingleDisbursalCommand;
@@ -164,6 +164,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus; import org.apache.fineract.portfolio.loanaccount.domain.LoanSubStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationRepository;
@@ -194,6 +196,7 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionVa
import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer; import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException; import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException;
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.paymentdetail.domain.PaymentDetail; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
@@ -213,6 +216,7 @@ import org.springframework.transaction.annotation.Transactional;
@RequiredArgsConstructor @RequiredArgsConstructor
public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService { public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatformService {
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
private final PlatformSecurityContext context; private final PlatformSecurityContext context;
private final LoanTransactionValidator loanTransactionValidator; private final LoanTransactionValidator loanTransactionValidator;
private final LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer; private final LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer;
@@ -251,7 +255,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
private final RepaymentWithPostDatedChecksAssembler repaymentWithPostDatedChecksAssembler; private final RepaymentWithPostDatedChecksAssembler repaymentWithPostDatedChecksAssembler;
private final PostDatedChecksRepository postDatedChecksRepository; private final PostDatedChecksRepository postDatedChecksRepository;
private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository; private final LoanRepaymentScheduleInstallmentRepository loanRepaymentScheduleInstallmentRepository;
private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final LoanLifecycleStateMachine loanLifecycleStateMachine;
private final LoanAccountLockService loanAccountLockService; private final LoanAccountLockService loanAccountLockService;
private final ExternalIdFactory externalIdFactory; private final ExternalIdFactory externalIdFactory;
private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService; private final ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService;
@@ -294,28 +298,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
@Override @Override
public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand command, Boolean isAccountTransfer, public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand command, Boolean isAccountTransfer,
Boolean isWithoutAutoPayment) { Boolean isWithoutAutoPayment) {
loanTransactionValidator.validateDisbursement(command, isAccountTransfer, loanId);
final AppUser currentUser = getAppUserIfPresent(); Loan loan = loanAssembler.assembleFrom(loanId);
this.loanTransactionValidator.validateDisbursement(command.json(), isAccountTransfer);
if (command.parameterExists("postDatedChecks")) {
// validate with post dated checks for the disbursement
this.loanTransactionValidator.validateDisbursementWithPostDatedChecks(command.json(), loanId);
}
Loan loan = this.loanAssembler.assembleFrom(loanId);
// Fail fast if client/group is not active or actual loan status disallows disbursal
checkClientOrGroupActive(loan);
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
if (loan.isChargedOff() && DateUtils.isBefore(actualDisbursementDate, loan.getChargedOffOnDate())) {
throw new GeneralPlatformDomainRuleException("error.msg.transaction.date.cannot.be.earlier.than.charge.off.date", "Loan: "
+ loanId
+ " backdated transaction is not allowed. Transaction date cannot be earlier than the charge-off date of the loan",
loanId);
}
if (loan.loanProduct().isDisallowExpectedDisbursements()) { if (loan.loanProduct().isDisallowExpectedDisbursements()) {
List<LoanDisbursementDetails> filteredList = loan.getDisbursementDetails().stream() List<LoanDisbursementDetails> filteredList = loan.getDisbursementDetails().stream()
@@ -323,8 +308,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
// Check whether a new LoanDisbursementDetails is required // Check whether a new LoanDisbursementDetails is required
if (filteredList.isEmpty()) { if (filteredList.isEmpty()) {
// create artificial 'tranche/expected disbursal' as current disburse code expects it for // create artificial 'tranche/expected disbursal' as current disburse code expects it for
// multi-disbursal // multi-disbursal products
// products
final LocalDate artificialExpectedDate = loan.getExpectedDisbursedOnLocalDate(); final LocalDate artificialExpectedDate = loan.getExpectedDisbursedOnLocalDate();
LoanDisbursementDetails disbursementDetail = new LoanDisbursementDetails(artificialExpectedDate, null, LoanDisbursementDetails disbursementDetail = new LoanDisbursementDetails(artificialExpectedDate, null,
loan.getDisbursedAmount(), null, false); loan.getDisbursedAmount(), null, false);
@@ -332,61 +316,23 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.getAllDisbursementDetails().add(disbursementDetail); loan.getAllDisbursementDetails().add(disbursementDetail);
} }
} }
loan.validateAccountStatus(LoanEvent.LOAN_DISBURSED);
// Get disbursedAmount
final BigDecimal disbursedAmount = loan.getDisbursedAmount();
final Set<LoanCollateralManagement> loanCollateralManagements = loan.getLoanCollateralManagements();
// Get relevant loan collateral modules
if ((loanCollateralManagements != null && !loanCollateralManagements.isEmpty()) && loan.getLoanType().isIndividualAccount()) {
BigDecimal totalCollateral = BigDecimal.valueOf(0);
for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) {
BigDecimal quantity = loanCollateralManagement.getQuantity();
BigDecimal pctToBase = loanCollateralManagement.getClientCollateralManagement().getCollaterals().getPctToBase();
BigDecimal basePrice = loanCollateralManagement.getClientCollateralManagement().getCollaterals().getBasePrice();
totalCollateral = totalCollateral.add(quantity.multiply(basePrice).multiply(pctToBase).divide(BigDecimal.valueOf(100)));
}
// Validate the loan collateral value against the disbursedAmount
if (disbursedAmount.compareTo(totalCollateral) > 0) {
throw new LoanCollateralAmountNotSufficientException(disbursedAmount);
}
}
// validate ActualDisbursement Date Against Expected Disbursement Date
LoanProduct loanProduct = loan.loanProduct();
if (loanProduct.isSyncExpectedWithDisbursementDate()) {
syncExpectedDateWithActualDisbursementDate(loan, actualDisbursementDate);
}
final LocalDate nextPossibleRepaymentDate = loan.getNextPossibleRepaymentDateForRescheduling(); final LocalDate nextPossibleRepaymentDate = loan.getNextPossibleRepaymentDateForRescheduling();
final LocalDate rescheduledRepaymentDate = command.localDateValueOfParameterNamed("adjustRepaymentDate"); final LocalDate rescheduledRepaymentDate = command.localDateValueOfParameterNamed("adjustRepaymentDate");
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
entityDatatableChecksWritePlatformService.runTheCheckForProduct(loanId, EntityTables.LOAN.getName(), StatusEnum.DISBURSE.getValue(),
EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId());
LocalDate recalculateFrom = null;
if (!loan.isMultiDisburmentLoan()) { if (!loan.isMultiDisburmentLoan()) {
loan.setActualDisbursementDate(actualDisbursementDate); loan.setActualDisbursementDate(actualDisbursementDate);
} }
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
// validate actual disbursement date against meeting date // validate actual disbursement date against meeting date
final CalendarInstance calendarInstance = this.calendarInstanceRepository.findCalendarInstaneByEntityId(loan.getId(), ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, null);
CalendarEntityType.LOANS.getValue());
if (loan.isSyncDisbursementWithMeeting()) {
this.loanTransactionValidator.validateDisbursementDateWithMeetingDate(actualDisbursementDate, calendarInstance,
scheduleGeneratorDTO.isSkipRepaymentOnFirstDayofMonth(), scheduleGeneratorDTO.getNumberOfdays());
}
businessEventNotifierService.notifyPreBusinessEvent(new LoanDisbursalBusinessEvent(loan)); businessEventNotifierService.notifyPreBusinessEvent(new LoanDisbursalBusinessEvent(loan));
final List<Long> existingTransactionIds = new ArrayList<>(); final List<Long> existingTransactionIds = new ArrayList<>();
final List<Long> existingReversedTransactionIds = new ArrayList<>(); final List<Long> existingReversedTransactionIds = new ArrayList<>();
final AppUser currentUser = getAppUserIfPresent();
final Map<String, Object> changes = new LinkedHashMap<>(); final Map<String, Object> changes = new LinkedHashMap<>();
final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes);
@@ -400,12 +346,10 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
// Recalculate first repayment date based in actual disbursement date. // Recalculate first repayment date based in actual disbursement date.
updateLoanCounters(loan, actualDisbursementDate); updateLoanCounters(loan, actualDisbursementDate);
Money amountBeforeAdjust = loan.getPrincipal(); Money amountBeforeAdjust = loan.getPrincipal();
boolean canDisburse = loan.canDisburse(actualDisbursementDate);
ChangedTransactionDetail changedTransactionDetail = null;
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 (canDisburse) {
if (loan.canDisburse(actualDisbursementDate)) {
// 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);
@@ -441,9 +385,9 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
amountToDisburse = disburseAmount.minus(loanOutstanding); amountToDisburse = disburseAmount.minus(loanOutstanding);
disburseLoanToLoan(loan, command, loanOutstanding); disburseLoanToLoan(loan, command, loanOutstanding);
} }
LoanTransaction disbursementTransaction = null; LoanTransaction disbursementTransaction = null;
if (isAccountTransfer) { if (isAccountTransfer) {
disburseLoanToSavings(loan, command, amountToDisburse, paymentDetail); disburseLoanToSavings(loan, command, amountToDisburse, paymentDetail);
@@ -463,17 +407,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
*/ */
recalculateSchedule = true; recalculateSchedule = true;
} }
regenerateScheduleOnDisbursement(command, loan, recalculateSchedule, scheduleGeneratorDTO, nextPossibleRepaymentDate, regenerateScheduleOnDisbursement(command, loan, recalculateSchedule, scheduleGeneratorDTO, nextPossibleRepaymentDate,
rescheduledRepaymentDate); rescheduledRepaymentDate);
boolean downPaymentEnabled = loan.repaymentScheduleDetail().isEnableDownPayment(); boolean downPaymentEnabled = loan.repaymentScheduleDetail().isEnableDownPayment();
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() || downPaymentEnabled) { if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() || downPaymentEnabled) {
createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO); createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO);
} }
if (isPaymentTypeApplicableForDisbursementCharge) { ChangedTransactionDetail changedTransactionDetail = disburseLoan(command, isPaymentTypeApplicableForDisbursementCharge,
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail); paymentDetail, loan, currentUser, changes, scheduleGeneratorDTO);
} else {
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null);
}
loan.adjustNetDisbursalAmount(amountToDisburse.getAmount()); loan.adjustNetDisbursalAmount(amountToDisburse.getAmount());
loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan);
@@ -612,6 +554,69 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
.build(); .build();
} }
private ChangedTransactionDetail disburseLoan(JsonCommand command, boolean isPaymentTypeApplicableForDisbursementCharge,
PaymentDetail paymentDetail, Loan loan, AppUser currentUser, Map<String, Object> changes,
ScheduleGeneratorDTO scheduleGeneratorDTO) {
final PaymentDetail paymentDetail1 = isPaymentTypeApplicableForDisbursementCharge ? paymentDetail : null;
final LocalDate actualDisbursementDate1 = command.localDateValueOfParameterNamed(Loan.ACTUAL_DISBURSEMENT_DATE);
loan.setDisbursedBy(currentUser);
loan.updateLoanScheduleDependentDerivedFields();
changes.put(Loan.LOCALE, command.locale());
changes.put(Loan.DATE_FORMAT, command.dateFormat());
changes.put(Loan.ACTUAL_DISBURSEMENT_DATE, command.stringValueOfParameterNamed(Loan.ACTUAL_DISBURSEMENT_DATE));
boolean disbursementMissedParam = loan.isDisbursementMissed();
LocalDate firstInstallmentDueDate = loan.fetchRepaymentScheduleInstallment(1).getDueDate();
if ((loan.repaymentScheduleDetail().isInterestRecalculationEnabled()
&& (DateUtils.isBeforeBusinessDate(firstInstallmentDueDate) || disbursementMissedParam))) {
loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
}
loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement());
loan.updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate1);
loan.handleDisbursementTransaction(actualDisbursementDate1, paymentDetail1);
loan.updateLoanSummaryDerivedFields();
final Money interestApplied = Money.of(loan.getCurrency(), loan.getSummary().getTotalInterestCharged());
/*
* Add an interest applied transaction of the interest is accrued upfront (Up front accrual), no accounting or
* cash based accounting is selected
*/
if (((loan.isMultiDisburmentLoan() && loan.getDisbursedLoanDisbursementDetails().size() == 1) || !loan.isMultiDisburmentLoan())
&& loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) {
ExternalId externalId = ExternalId.empty();
if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) {
externalId = ExternalId.generate();
}
final LoanTransaction interestAppliedTransaction = LoanTransaction.accrueInterest(loan.getOffice(), loan, interestApplied,
actualDisbursementDate1, externalId);
loan.addLoanTransaction(interestAppliedTransaction);
}
ChangedTransactionDetail changedTransactionDetail = null;
if (loan.getLoanProduct().isMultiDisburseLoan()) {
final List<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing();
if (!allNonContraTransactionsPostDisbursement.isEmpty()) {
final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = transactionProcessorFactory
.determineProcessor(loan.getTransactionProcessingStrategyCode());
changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(loan.getDisbursementDate(),
allNonContraTransactionsPostDisbursement, loan.getCurrency(), loan.getRepaymentScheduleInstallments(),
loan.getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(loan);
}
loan.getLoanTransactions().addAll(changedTransactionDetail.getNewTransactionMappings().values());
}
loan.updateLoanSummaryDerivedFields();
}
loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, loan);
changes.put(Loan.PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus()));
return changedTransactionDetail;
}
private void updatePostDatedChecks(Set<PostDatedChecks> postDatedChecks) { private void updatePostDatedChecks(Set<PostDatedChecks> postDatedChecks) {
this.postDatedChecksRepository.saveAll(postDatedChecks); this.postDatedChecksRepository.saveAll(postDatedChecks);
} }
@@ -799,11 +804,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() || downPaymentEnabled) { if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() || downPaymentEnabled) {
createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO); createAndSaveLoanScheduleArchive(loan, scheduleGeneratorDTO);
} }
if (configurationDomainService.isPaymentTypeApplicableForDisbursementCharge()) { changedTransactionDetail = disburseLoan(command, configurationDomainService.isPaymentTypeApplicableForDisbursementCharge(),
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, paymentDetail); paymentDetail, loan, currentUser, changes, scheduleGeneratorDTO);
} else {
changedTransactionDetail = loan.disburse(currentUser, command, changes, scheduleGeneratorDTO, null);
}
loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan);
@@ -1132,7 +1134,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
loan.updateLoanSummaryDerivedFields(); loan.updateLoanSummaryDerivedFields();
doPostLoanTransactionChecks(loan, newInterestPaymentWaiverTransaction.getTransactionDate(), defaultLoanLifecycleStateMachine); doPostLoanTransactionChecks(loan, newInterestPaymentWaiverTransaction.getTransactionDate(), loanLifecycleStateMachine);
return changedTransactionDetail; return changedTransactionDetail;
} }
@@ -1519,7 +1521,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
final ChangedTransactionDetail changedTransactionDetail = loan.adjustExistingTransaction(newTransactionDetail, final ChangedTransactionDetail changedTransactionDetail = loan.adjustExistingTransaction(newTransactionDetail,
defaultLoanLifecycleStateMachine, transactionToAdjust, existingTransactionIds, existingReversedTransactionIds, loanLifecycleStateMachine, transactionToAdjust, existingTransactionIds, existingReversedTransactionIds,
scheduleGeneratorDTO, reversalTxnExternalId); scheduleGeneratorDTO, reversalTxnExternalId);
loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan);
@@ -1681,7 +1683,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
newTransaction = this.loanTransactionRepository.saveAndFlush(newTransaction); newTransaction = this.loanTransactionRepository.saveAndFlush(newTransaction);
loan.handleChargebackTransaction(newTransaction, defaultLoanLifecycleStateMachine); loan.handleChargebackTransaction(newTransaction, loanLifecycleStateMachine);
loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan);
@@ -1765,8 +1767,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
final ChangedTransactionDetail changedTransactionDetail = loan.waiveInterest(waiveInterestTransaction, final ChangedTransactionDetail changedTransactionDetail = loan.waiveInterest(waiveInterestTransaction, loanLifecycleStateMachine,
defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan);
@@ -1863,7 +1865,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
final ChangedTransactionDetail changedTransactionDetail = loan.closeAsWrittenOff(command, defaultLoanLifecycleStateMachine, changes, final ChangedTransactionDetail changedTransactionDetail = loan.closeAsWrittenOff(command, loanLifecycleStateMachine, changes,
existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO); existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan);
@@ -1937,8 +1939,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
ChangedTransactionDetail changedTransactionDetail = loan.close(command, defaultLoanLifecycleStateMachine, changes, ChangedTransactionDetail changedTransactionDetail = loan.close(command, loanLifecycleStateMachine, changes, existingTransactionIds,
existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); existingReversedTransactionIds, scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
@@ -2039,7 +2041,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());
loan.closeAsMarkedForReschedule(command, defaultLoanLifecycleStateMachine, changes); loan.closeAsMarkedForReschedule(command, loanLifecycleStateMachine, changes);
saveLoanWithDataIntegrityViolationChecks(loan); saveLoanWithDataIntegrityViolationChecks(loan);
@@ -2083,7 +2085,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
private void disburseLoanToLoan(final Loan loan, final JsonCommand command, final BigDecimal amount) { private void disburseLoanToLoan(final Loan loan, final JsonCommand command, final BigDecimal amount) {
final LocalDate transactionDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); final LocalDate transactionDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
@@ -2098,7 +2099,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
} }
private void disburseLoanToSavings(final Loan loan, final JsonCommand command, final Money amount, final PaymentDetail paymentDetail) { private void disburseLoanToSavings(final Loan loan, final JsonCommand command, final Money amount, final PaymentDetail paymentDetail) {
final LocalDate transactionDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); final LocalDate transactionDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName);
@@ -2118,7 +2118,6 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
LoanTransactionType.DISBURSEMENT.getValue(), null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null, LoanTransactionType.DISBURSEMENT.getValue(), null, null, null, AccountTransferType.ACCOUNT_TRANSFER.getValue(), null, null,
txnExternalId, loan, null, fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck); txnExternalId, loan, null, fromSavingsAccount, isRegularTransaction, isExceptionForBalanceCheck);
this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO); this.accountTransfersWritePlatformService.transferFunds(accountTransferDTO);
} }
@Transactional @Transactional
@@ -2136,7 +2135,7 @@ 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 = defaultLoanLifecycleStateMachine; 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);
@@ -2159,7 +2158,7 @@ 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 = defaultLoanLifecycleStateMachine; 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 {
@@ -2192,7 +2191,7 @@ 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 = defaultLoanLifecycleStateMachine; 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);
@@ -2209,7 +2208,7 @@ 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));
LoanLifecycleStateMachine loanLifecycleStateMachine = defaultLoanLifecycleStateMachine; 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));
@@ -2610,7 +2609,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom);
ChangedTransactionDetail changedTransactionDetail = loan.undoWrittenOff(defaultLoanLifecycleStateMachine, existingTransactionIds, ChangedTransactionDetail changedTransactionDetail = loan.undoWrittenOff(loanLifecycleStateMachine, existingTransactionIds,
existingReversedTransactionIds, scheduleGeneratorDTO); existingReversedTransactionIds, scheduleGeneratorDTO);
loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
@@ -2885,8 +2884,44 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf
final LocalDate rescheduledRepaymentDate) { final LocalDate rescheduledRepaymentDate) {
final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate"); final LocalDate actualDisbursementDate = command.localDateValueOfParameterNamed("actualDisbursementDate");
BigDecimal emiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.fixedEmiAmountParameterName); BigDecimal emiAmount = command.bigDecimalValueOfParameterNamed(LoanApiConstants.fixedEmiAmountParameterName);
loan.regenerateScheduleOnDisbursement(scheduleGeneratorDTO, recalculateSchedule, actualDisbursementDate, emiAmount,
nextPossibleRepaymentDate, rescheduledRepaymentDate); boolean isEmiAmountChanged = false;
LoanProduct loanProduct = loan.getLoanProduct();
if ((loanProduct.isMultiDisburseLoan() || loanProduct.isCanDefineInstallmentAmount()) && emiAmount != null
&& emiAmount.compareTo(loan.retriveLastEmiAmount()) != 0) {
if (loanProduct.isMultiDisburseLoan()) {
final LocalDate dateValue = null;
final boolean isSpecificToInstallment = false;
final Boolean isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled = scheduleGeneratorDTO
.isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled();
LocalDate effectiveDateFrom = actualDisbursementDate;
if (!isChangeEmiIfRepaymentDateSameAsDisbursementDateEnabled && actualDisbursementDate.equals(nextPossibleRepaymentDate)) {
effectiveDateFrom = nextPossibleRepaymentDate.plusDays(1);
}
LoanTermVariations loanVariationTerms = new LoanTermVariations(LoanTermVariationType.EMI_AMOUNT.getValue(),
effectiveDateFrom, emiAmount, dateValue, isSpecificToInstallment, loan, LoanStatus.ACTIVE.getValue());
loan.getLoanTermVariations().add(loanVariationTerms);
} else {
loan.setFixedEmiAmount(emiAmount);
}
isEmiAmountChanged = true;
}
if (rescheduledRepaymentDate != null && loanProduct.isMultiDisburseLoan()) {
final boolean isSpecificToInstallment = false;
LoanTermVariations loanVariationTerms = new LoanTermVariations(LoanTermVariationType.DUE_DATE.getValue(),
nextPossibleRepaymentDate, emiAmount, rescheduledRepaymentDate, isSpecificToInstallment, loan,
LoanStatus.ACTIVE.getValue());
loan.getLoanTermVariations().add(loanVariationTerms);
}
if (loan.isActualDisbursedOnDateEarlierOrLaterThanExpected(actualDisbursementDate) || recalculateSchedule || isEmiAmountChanged
|| rescheduledRepaymentDate != null) {
if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) {
loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO);
} else {
loan.regenerateRepaymentSchedule(scheduleGeneratorDTO);
}
}
loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan);
if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) {
@@ -346,8 +346,8 @@ public class LoanAccountConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(LoanWritePlatformService.class) @ConditionalOnMissingBean(LoanWritePlatformService.class)
public LoanWritePlatformService loanWritePlatformService(PlatformSecurityContext context, public LoanWritePlatformService loanWritePlatformService(LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory,
LoanTransactionValidator loanTransactionValidator, PlatformSecurityContext context, LoanTransactionValidator loanTransactionValidator,
LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer, LoanRepositoryWrapper loanRepositoryWrapper, LoanUpdateCommandFromApiJsonDeserializer loanUpdateCommandFromApiJsonDeserializer, LoanRepositoryWrapper loanRepositoryWrapper,
LoanAccountDomainService loanAccountDomainService, NoteRepository noteRepository, LoanAccountDomainService loanAccountDomainService, NoteRepository noteRepository,
LoanTransactionRepository loanTransactionRepository, LoanTransactionRelationRepository loanTransactionRelationRepository, LoanTransactionRepository loanTransactionRepository, LoanTransactionRelationRepository loanTransactionRelationRepository,
@@ -373,17 +373,18 @@ public class LoanAccountConfiguration {
LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService, ErrorHandler errorHandler, LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService, ErrorHandler errorHandler,
LoanDownPaymentHandlerService loanDownPaymentHandlerService, AccountTransferRepository accountTransferRepository, LoanDownPaymentHandlerService loanDownPaymentHandlerService, AccountTransferRepository accountTransferRepository,
LoanTransactionAssembler loanTransactionAssembler, LoanAccrualsProcessingService loanAccrualsProcessingService) { LoanTransactionAssembler loanTransactionAssembler, LoanAccrualsProcessingService loanAccrualsProcessingService) {
return new LoanWritePlatformServiceJpaRepositoryImpl(context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer, return new LoanWritePlatformServiceJpaRepositoryImpl(transactionProcessorFactory, context, loanTransactionValidator,
loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository, loanUpdateCommandFromApiJsonDeserializer, loanRepositoryWrapper, loanAccountDomainService, noteRepository,
loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, calendarInstanceRepository, loanTransactionRepository, loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService,
paymentDetailWritePlatformService, holidayRepository, configurationDomainService, workingDaysRepository, calendarInstanceRepository, paymentDetailWritePlatformService, holidayRepository, configurationDomainService,
accountTransfersWritePlatformService, accountTransfersReadPlatformService, accountAssociationsReadPlatformService, workingDaysRepository, accountTransfersWritePlatformService, accountTransfersReadPlatformService,
loanReadPlatformService, fromApiJsonHelper, calendarRepository, loanScheduleHistoryWritePlatformService, accountAssociationsReadPlatformService, loanReadPlatformService, fromApiJsonHelper, calendarRepository,
loanApplicationValidator, accountAssociationRepository, accountTransferDetailRepository, businessEventNotifierService, loanScheduleHistoryWritePlatformService, loanApplicationValidator, accountAssociationRepository,
guarantorDomainService, loanUtilService, loanSummaryWrapper, entityDatatableChecksWritePlatformService, accountTransferDetailRepository, businessEventNotifierService, guarantorDomainService, loanUtilService, loanSummaryWrapper,
transactionProcessingStrategy, codeValueRepository, cashierTransactionDataValidator, glimRepository, loanRepository, entityDatatableChecksWritePlatformService, transactionProcessingStrategy, codeValueRepository,
repaymentWithPostDatedChecksAssembler, postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository, cashierTransactionDataValidator, glimRepository, loanRepository, repaymentWithPostDatedChecksAssembler,
defaultLoanLifecycleStateMachine, loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService, postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository, defaultLoanLifecycleStateMachine,
loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService,
loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService, accountTransferRepository, loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService, accountTransferRepository,
loanTransactionAssembler, loanAccrualsProcessingService); loanTransactionAssembler, loanAccrualsProcessingService);
} }
@@ -95,8 +95,8 @@ public class CheckLoanRepaymentDueBusinessStepTest {
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(loanProduct.getDueDaysForRepaymentEvent()).thenReturn(null); when(loanProduct.getDueDaysForRepaymentEvent()).thenReturn(null);
when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments); when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments);
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanForProcessing.getLoanSummary().getTotalOutstanding()).thenReturn(BigDecimal.ONE); when(loanForProcessing.getSummary().getTotalOutstanding()).thenReturn(BigDecimal.ONE);
when(loanForProcessing.getCurrency()).thenReturn(currency); when(loanForProcessing.getCurrency()).thenReturn(currency);
when(repaymentInstallment.getTotalOutstanding(currency)).thenReturn(money); when(repaymentInstallment.getTotalOutstanding(currency)).thenReturn(money);
when(money.isGreaterThanZero()).thenReturn(true); when(money.isGreaterThanZero()).thenReturn(true);
@@ -153,8 +153,8 @@ public class CheckLoanRepaymentDueBusinessStepTest {
// Loan Product setting overrides global settings // Loan Product setting overrides global settings
when(loanProduct.getDueDaysForRepaymentEvent()).thenReturn(1); when(loanProduct.getDueDaysForRepaymentEvent()).thenReturn(1);
when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments); when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments);
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanForProcessing.getLoanSummary().getTotalOutstanding()).thenReturn(BigDecimal.ONE); when(loanForProcessing.getSummary().getTotalOutstanding()).thenReturn(BigDecimal.ONE);
when(loanForProcessing.getCurrency()).thenReturn(currency); when(loanForProcessing.getCurrency()).thenReturn(currency);
when(repaymentInstallment.getTotalOutstanding(currency)).thenReturn(money); when(repaymentInstallment.getTotalOutstanding(currency)).thenReturn(money);
when(money.isGreaterThanZero()).thenReturn(true); when(money.isGreaterThanZero()).thenReturn(true);
@@ -111,8 +111,8 @@ public class CheckLoanRepaymentOverdueBusinessStepTest {
List<LoanRepaymentScheduleInstallment> loanRepaymentScheduleInstallments = Arrays.asList(repaymentInstallment); List<LoanRepaymentScheduleInstallment> loanRepaymentScheduleInstallments = Arrays.asList(repaymentInstallment);
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(null); when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(null);
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanForProcessing.getLoanSummary().getTotalOutstanding()).thenReturn(BigDecimal.valueOf(100)); when(loanForProcessing.getSummary().getTotalOutstanding()).thenReturn(BigDecimal.valueOf(100));
when(loanForProcessing.getCurrency()).thenReturn(currency); when(loanForProcessing.getCurrency()).thenReturn(currency);
when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments); when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments);
@@ -138,8 +138,8 @@ public class CheckLoanRepaymentOverdueBusinessStepTest {
loanInstallmentRepaymentDueDateBefore5Days, BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), loanInstallmentRepaymentDueDateBefore5Days, BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0),
BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), false, new HashSet<>(), BigDecimal.valueOf(0.0))); BigDecimal.valueOf(0.0), BigDecimal.valueOf(0.0), false, new HashSet<>(), BigDecimal.valueOf(0.0)));
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanForProcessing.getLoanSummary().getTotalOutstanding()).thenReturn(BigDecimal.valueOf(100)); when(loanForProcessing.getSummary().getTotalOutstanding()).thenReturn(BigDecimal.valueOf(100));
when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(null); when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(null);
when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments); when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments);
// when // when
@@ -166,8 +166,8 @@ public class CheckLoanRepaymentOverdueBusinessStepTest {
List<LoanRepaymentScheduleInstallment> loanRepaymentScheduleInstallments = Arrays.asList(repaymentInstallmentPaidOff); List<LoanRepaymentScheduleInstallment> loanRepaymentScheduleInstallments = Arrays.asList(repaymentInstallmentPaidOff);
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanForProcessing.getLoanSummary().getTotalOutstanding()).thenReturn(BigDecimal.valueOf(100)); when(loanForProcessing.getSummary().getTotalOutstanding()).thenReturn(BigDecimal.valueOf(100));
when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(null); when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(null);
when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments); when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments);
@@ -196,8 +196,8 @@ public class CheckLoanRepaymentOverdueBusinessStepTest {
when(loanForProcessing.getStatus()).thenReturn(LoanStatus.ACTIVE); when(loanForProcessing.getStatus()).thenReturn(LoanStatus.ACTIVE);
// product configuration overrides global configuration // product configuration overrides global configuration
when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(1); when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(1);
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanForProcessing.getLoanSummary().getTotalOutstanding()).thenReturn(BigDecimal.valueOf(100)); when(loanForProcessing.getSummary().getTotalOutstanding()).thenReturn(BigDecimal.valueOf(100));
when(loanForProcessing.getCurrency()).thenReturn(currency); when(loanForProcessing.getCurrency()).thenReturn(currency);
when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments); when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments);
@@ -216,8 +216,8 @@ public class CheckLoanRepaymentOverdueBusinessStepTest {
Loan loanForProcessing = Mockito.mock(Loan.class); Loan loanForProcessing = Mockito.mock(Loan.class);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class); LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanForProcessing.getStatus()).thenReturn(LoanStatus.ACTIVE); when(loanForProcessing.getStatus()).thenReturn(LoanStatus.ACTIVE);
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanForProcessing.getLoanSummary().getTotalOutstanding()).thenReturn(BigDecimal.ZERO); when(loanForProcessing.getSummary().getTotalOutstanding()).thenReturn(BigDecimal.ZERO);
// when // when
Loan processedLoan = underTest.execute(loanForProcessing); Loan processedLoan = underTest.execute(loanForProcessing);
// then - No Business Event raised // then - No Business Event raised
@@ -240,8 +240,8 @@ public class CheckLoanRepaymentOverdueBusinessStepTest {
BigDecimal.valueOf(1.0), BigDecimal.valueOf(0.0), false, new HashSet<>(), BigDecimal.valueOf(0.0))); BigDecimal.valueOf(1.0), BigDecimal.valueOf(0.0), false, new HashSet<>(), BigDecimal.valueOf(0.0)));
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(1); when(loanProduct.getOverDueDaysForRepaymentEvent()).thenReturn(1);
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanForProcessing.getLoanSummary().getTotalOutstanding()).thenReturn(BigDecimal.ONE); when(loanForProcessing.getSummary().getTotalOutstanding()).thenReturn(BigDecimal.ONE);
when(loanForProcessing.getCurrency()).thenReturn(currency); when(loanForProcessing.getCurrency()).thenReturn(currency);
when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments); when(loanForProcessing.getRepaymentScheduleInstallments()).thenReturn(loanRepaymentScheduleInstallments);
// when // when
@@ -117,7 +117,7 @@ public class LoanRepaymentBusinessEventSerializerTest {
when(loanForProcessing.getId()).thenReturn(1L); when(loanForProcessing.getId()).thenReturn(1L);
when(loanForProcessing.getAccountNumber()).thenReturn("0001"); when(loanForProcessing.getAccountNumber()).thenReturn("0001");
when(loanForProcessing.getExternalId()).thenReturn(ExternalIdFactory.produce("externalId")); when(loanForProcessing.getExternalId()).thenReturn(ExternalIdFactory.produce("externalId"));
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.valueOf(0.0)); when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.valueOf(0.0));
when(loanForProcessing.getCurrency()).thenReturn(loanCurrency); when(loanForProcessing.getCurrency()).thenReturn(loanCurrency);
when(loanCurrency.getCode()).thenReturn("CODE"); when(loanCurrency.getCode()).thenReturn("CODE");
@@ -168,7 +168,7 @@ public class LoanRepaymentBusinessEventSerializerTest {
when(loanForProcessing.getId()).thenReturn(null); when(loanForProcessing.getId()).thenReturn(null);
when(loanForProcessing.getAccountNumber()).thenReturn("0001"); when(loanForProcessing.getAccountNumber()).thenReturn("0001");
when(loanForProcessing.getExternalId()).thenReturn(ExternalIdFactory.produce("externalId")); when(loanForProcessing.getExternalId()).thenReturn(ExternalIdFactory.produce("externalId"));
when(loanForProcessing.getLoanSummary()).thenReturn(loanSummary); when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.valueOf(0.0)); when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.valueOf(0.0));
when(loanForProcessing.getCurrency()).thenReturn(loanCurrency); when(loanForProcessing.getCurrency()).thenReturn(loanCurrency);
when(loanCurrency.getCode()).thenReturn("CODE"); when(loanCurrency.getCode()).thenReturn("CODE");
@@ -143,7 +143,7 @@ class DefaultLoanLifecycleStateMachineTest {
Mockito.when(loan.getPlainStatus()).thenReturn(LoanStatus.OVERPAID.getValue()); Mockito.when(loan.getPlainStatus()).thenReturn(LoanStatus.OVERPAID.getValue());
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.OVERPAID); Mockito.when(loan.getStatus()).thenReturn(LoanStatus.OVERPAID);
Mockito.when(loan.getTotalOverpaidAsMoney()).thenReturn(zero); Mockito.when(loan.getTotalOverpaidAsMoney()).thenReturn(zero);
Mockito.when(loan.getLoanSummary()).thenReturn(loanSummary); Mockito.when(loan.getSummary()).thenReturn(loanSummary);
Mockito.when(loanSummary.getTotalOutstanding(eq(currency))).thenReturn(one); Mockito.when(loanSummary.getTotalOutstanding(eq(currency))).thenReturn(one);
// when // when
underTest.transition(LoanEvent.LOAN_DISBURSED, loan); underTest.transition(LoanEvent.LOAN_DISBURSED, loan);
@@ -163,7 +163,7 @@ class DefaultLoanLifecycleStateMachineTest {
Mockito.when(loan.getPlainStatus()).thenReturn(LoanStatus.OVERPAID.getValue()); Mockito.when(loan.getPlainStatus()).thenReturn(LoanStatus.OVERPAID.getValue());
Mockito.when(loan.getStatus()).thenReturn(LoanStatus.OVERPAID); Mockito.when(loan.getStatus()).thenReturn(LoanStatus.OVERPAID);
Mockito.when(loan.getTotalOverpaidAsMoney()).thenReturn(zero); Mockito.when(loan.getTotalOverpaidAsMoney()).thenReturn(zero);
Mockito.when(loan.getLoanSummary()).thenReturn(loanSummary); Mockito.when(loan.getSummary()).thenReturn(loanSummary);
Mockito.when(loanSummary.getTotalOutstanding(currency)).thenReturn(zero); Mockito.when(loanSummary.getTotalOutstanding(currency)).thenReturn(zero);
// when // when
underTest.transition(LoanEvent.LOAN_DISBURSED, loan); underTest.transition(LoanEvent.LOAN_DISBURSED, loan);
@@ -130,22 +130,21 @@ public class LoanRepaymentRescheduleAtDisbursementTest {
this.loanTransactionHelper.disburseLoanWithRepaymentReschedule(disbursementDate, loanID, adjustRepaymentDate); this.loanTransactionHelper.disburseLoanWithRepaymentReschedule(disbursementDate, loanID, adjustRepaymentDate);
loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID); loanStatusHashMap = LoanStatusChecker.getStatusOfLoan(this.requestSpec, this.responseSpec, loanID);
ArrayList<HashMap> loanRepaymnetSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, generalResponseSpec, ArrayList<HashMap> loanRepaymentSchedule = this.loanTransactionHelper.getLoanRepaymentSchedule(requestSpec, generalResponseSpec,
loanID); loanID);
HashMap firstInstallement = loanRepaymnetSchedule.get(1); HashMap firstInstallment = loanRepaymentSchedule.get(1);
Map<String, Object> expectedvalues = new HashMap<>(3); Map<String, Object> expectedValues = new HashMap<>(3);
Calendar date = Calendar.getInstance(Utils.getTimeZoneOfTenant()); Calendar date = Calendar.getInstance(Utils.getTimeZoneOfTenant());
date.set(2015, Calendar.MARCH, 16); date.set(2015, Calendar.MARCH, 16);
expectedvalues.put("dueDate", getDateAsArray(date, 0)); expectedValues.put("dueDate", getDateAsArray(date, 0));
expectedvalues.put("principalDue", "834.71"); expectedValues.put("principalDue", "834.71");
expectedvalues.put("interestDue", "49.32"); expectedValues.put("interestDue", "49.32");
expectedvalues.put("feeChargesDue", "0"); expectedValues.put("feeChargesDue", "0");
expectedvalues.put("penaltyChargesDue", "0"); expectedValues.put("penaltyChargesDue", "0");
expectedvalues.put("totalDueForPeriod", "884.03"); expectedValues.put("totalDueForPeriod", "884.03");
// VALIDATE REPAYMENT SCHEDULE // VALIDATE REPAYMENT SCHEDULE
verifyLoanRepaymentSchedule(firstInstallement, expectedvalues); verifyLoanRepaymentSchedule(firstInstallment, expectedValues);
} }
private void addCollaterals(List<HashMap> collaterals, Integer collateralId, BigDecimal quantity) { private void addCollaterals(List<HashMap> collaterals, Integer collateralId, BigDecimal quantity) {