From a2c01d25e5ce95a9a9147b1ebf29afa23276aacb Mon Sep 17 00:00:00 2001 From: Oleksii Novikov Date: Mon, 18 Nov 2024 15:40:06 +0200 Subject: [PATCH] FINERACT-2080: Move loan validation from entity to validator classes --- .../portfolio/loanaccount/domain/Loan.java | 1993 +---------------- .../loanaccount/domain/LoanTransaction.java | 1 - ...RepaymentScheduleTransactionProcessor.java | 3 + .../serialization/LoanChargeValidator.java | 101 + .../LoanDownPaymentTransactionValidator.java | 224 ++ .../serialization/LoanRefundValidator.java | 114 + .../service/LoanChargeService.java | 84 + .../LoanDownPaymentHandlerService.java | 6 + .../LoanDownPaymentHandlerServiceImpl.java | 189 +- .../service/LoanRefundService.java | 123 + .../service/LoanScheduleService.java | 121 + ...edPaymentScheduleTransactionProcessor.java | 1 + ...WritePlatformServiceJpaRepositoryImpl.java | 4 +- .../group/starter/GroupConfiguration.java | 5 +- .../domain/LoanAccountDomainServiceJpa.java | 174 +- .../service/LoanScheduleAssembler.java | 14 +- .../LoanScheduleWritePlatformServiceImpl.java | 4 +- ...heduleRequestWritePlatformServiceImpl.java | 4 +- .../LoanApplicationValidator.java | 2 +- .../LoanDisbursementValidator.java | 83 + .../LoanForeclosureValidator.java | 48 + .../serialization/LoanOfficerValidator.java | 81 + .../LoanTransactionValidator.java | 284 ++- ...nAccrualActivityProcessingServiceImpl.java | 3 + .../LoanAccrualsProcessingServiceImpl.java | 20 + ...WritePlatformServiceJpaRepositoryImpl.java | 6 +- .../service/LoanAssemblerImpl.java | 11 +- .../LoanChargeWritePlatformServiceImpl.java | 114 +- .../service/LoanDisbursementService.java | 300 +++ .../service/LoanOfficerService.java | 71 + .../service/LoanReadPlatformServiceImpl.java | 4 +- ...WritePlatformServiceJpaRepositoryImpl.java | 564 ++++- .../reaging/LoanReAgingServiceImpl.java | 4 + .../LoanReAmortizationServiceImpl.java | 4 + .../starter/LoanAccountConfiguration.java | 83 +- ...WritePlatformServiceJpaRepositoryImpl.java | 6 +- .../starter/TransferConfiguration.java | 6 +- ...oanChargeWritePlatformServiceImplTest.java | 4 + ...LoanDownPaymentHandlerServiceImplTest.java | 145 +- ...ePlatformServiceJpaRepositoryImplTest.java | 7 +- 40 files changed, 2868 insertions(+), 2147 deletions(-) create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeValidator.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDownPaymentTransactionValidator.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanRefundValidator.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeService.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanRefundService.java create mode 100644 fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDisbursementValidator.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanForeclosureValidator.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanOfficerValidator.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java create mode 100644 fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanOfficerService.java diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java index be4c52a47d3..ebc7d95528a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/Loan.java @@ -18,8 +18,6 @@ */ package org.apache.fineract.portfolio.loanaccount.domain; -import com.google.common.base.Splitter; -import com.google.gson.JsonArray; import com.google.gson.JsonElement; import com.google.gson.JsonObject; import com.google.gson.JsonPrimitive; @@ -66,13 +64,10 @@ import org.apache.fineract.infrastructure.codes.domain.CodeValue; import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; import org.apache.fineract.infrastructure.core.api.JsonCommand; -import org.apache.fineract.infrastructure.core.data.ApiParameterError; import org.apache.fineract.infrastructure.core.domain.AbstractAuditableWithUTCDateTimeCustom; import org.apache.fineract.infrastructure.core.domain.ExternalId; -import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper; import org.apache.fineract.infrastructure.core.service.DateUtils; -import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.security.service.RandomPasswordGenerator; import org.apache.fineract.organisation.holiday.domain.Holiday; @@ -84,7 +79,6 @@ import org.apache.fineract.organisation.office.domain.Office; import org.apache.fineract.organisation.staff.domain.Staff; import org.apache.fineract.organisation.workingdays.domain.WorkingDays; -import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil; import org.apache.fineract.portfolio.accountdetails.domain.AccountType; import org.apache.fineract.portfolio.calendar.data.CalendarHistoryDataWrapper; import org.apache.fineract.portfolio.calendar.domain.Calendar; @@ -95,7 +89,6 @@ import org.apache.fineract.portfolio.charge.domain.Charge; import org.apache.fineract.portfolio.charge.domain.ChargeCalculationType; import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; -import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeAddedException; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.collateral.domain.LoanCollateral; import org.apache.fineract.portfolio.common.domain.DayOfWeekType; @@ -114,25 +107,11 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; -import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; -import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; -import org.apache.fineract.portfolio.loanaccount.exception.InvalidRefundDateException; -import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException; -import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException; -import org.apache.fineract.portfolio.loanaccount.exception.LoanDisbursalException; -import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException; -import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException; -import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException; -import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerUnassignmentDateException; -import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataNotAllowedException; -import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException; -import org.apache.fineract.portfolio.loanaccount.exception.UndoLastTrancheDisbursementException; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; -import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; import org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType; import org.apache.fineract.portfolio.loanproduct.domain.InterestCalculationPeriodMethod; @@ -145,7 +124,6 @@ import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType; import org.apache.fineract.portfolio.loanproduct.domain.RepaymentStartDateType; import org.apache.fineract.portfolio.loanproduct.service.LoanEnumerations; -import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; import org.apache.fineract.portfolio.rate.domain.Rate; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks; import org.apache.fineract.useradministration.domain.AppUser; @@ -212,6 +190,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @JoinColumn(name = "fund_id") private Fund fund; + @Setter @ManyToOne(fetch = FetchType.EAGER) @JoinColumn(name = "loan_officer_id") private Staff loanOfficer; @@ -309,9 +288,11 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @JoinColumn(name = "closedon_userid") private AppUser closedBy; + @Setter @Column(name = "writtenoffon_date") private LocalDate writtenOffOnDate; + @Setter @Column(name = "rescheduledon_date") private LocalDate rescheduledOnDate; @@ -434,6 +415,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @Column(name = "guarantee_amount_derived", scale = 6, precision = 19) private BigDecimal guaranteeAmountDerived; + @Setter @Column(name = "interest_recalcualated_on") private LocalDate interestRecalculatedOn; @@ -449,6 +431,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { @JoinColumn(name = "writeoff_reason_cv_id") private CodeValue writeOffReason; + @Setter @Column(name = "loan_sub_status_id") private Integer loanSubStatus; @@ -696,25 +679,6 @@ private Set associateWithThisLoan(final Set installments = getRepaymentScheduleInstallments(); for (LoanRepaymentScheduleInstallment installment : installments) { @@ -881,13 +820,6 @@ private LocalDate getLastRepaymentPeriodDueDate(final boolean includeRecalculate } public void removeLoanCharge(final LoanCharge loanCharge) { - validateLoanIsNotClosed(loanCharge); - - // NOTE: to remove this constraint requires that loan transactions - // that represent the waive of charges also be removed (or reversed)M - // if you want ability to remove loan charges that are waived. - validateLoanChargeIsNotWaived(loanCharge); - final boolean removed = loanCharge.isActive(); if (removed) { loanCharge.setActive(false); @@ -940,14 +872,25 @@ && doesLoanChargePaidByContainLoanCharge(transaction.getLoanChargesPaid(), loanC } } + public void removeDisbursementDetails(final long id) { + this.disbursementDetails.remove(fetchLoanDisbursementsById(id)); + } + + public LoanDisbursementDetails addLoanDisbursementDetails(final LocalDate expectedDisbursementDate, final BigDecimal principal) { + final LocalDate actualDisbursementDate = null; + final LoanDisbursementDetails disbursementDetails = new LoanDisbursementDetails(expectedDisbursementDate, actualDisbursementDate, + principal, this.netDisbursalAmount, false); + disbursementDetails.updateLoan(this); + this.disbursementDetails.add(disbursementDetails); + return disbursementDetails; + } + private boolean doesLoanChargePaidByContainLoanCharge(Set loanChargePaidBys, LoanCharge loanCharge) { return loanChargePaidBys.stream() // .anyMatch(loanChargePaidBy -> loanChargePaidBy.getLoanCharge().equals(loanCharge)); } public Map updateLoanCharge(final LoanCharge loanCharge, final JsonCommand command) { - validateLoanIsNotClosed(loanCharge); - final Map actualChanges = new LinkedHashMap<>(3); if (getActiveCharges().contains(loanCharge)) { @@ -978,7 +921,7 @@ public Map updateLoanCharge(final LoanCharge loanCharge, final J return actualChanges; } - private BigDecimal calculateAmountPercentageAppliedTo(final LoanCharge loanCharge) { + public BigDecimal calculateAmountPercentageAppliedTo(final LoanCharge loanCharge) { if (loanCharge.isOverdueInstallmentCharge()) { return loanCharge.getAmountPercentageAppliedTo(); } @@ -1019,7 +962,7 @@ public BigDecimal getTotalInterest() { return this.loanSummaryWrapper.calculateTotalInterestCharged(getRepaymentScheduleInstallments(), getCurrency()).getAmount(); } - private BigDecimal calculatePerInstallmentChargeAmount(final LoanCharge loanCharge) { + public BigDecimal calculatePerInstallmentChargeAmount(final LoanCharge loanCharge) { return calculatePerInstallmentChargeAmount(loanCharge.getChargeCalculation(), loanCharge.getPercentage()); } @@ -1056,99 +999,6 @@ private Money calculateInstallmentChargeAmount(final ChargeCalculationType calcu .plus(LoanCharge.percentageOf(percentOf.getAmount(), percentage)); } - public LoanTransaction waiveLoanCharge(final LoanCharge loanCharge, final LoanLifecycleStateMachine loanLifecycleStateMachine, - final Map changes, final List existingTransactionIds, final List existingReversedTransactionIds, - final Integer loanInstallmentNumber, final ScheduleGeneratorDTO scheduleGeneratorDTO, final Money accruedCharge, - final ExternalId externalId) { - validateLoanIsNotClosed(loanCharge); - - final Money amountWaived = loanCharge.waive(getCurrency(), loanInstallmentNumber); - changes.put("amount", amountWaived.getAmount()); - - Money unrecognizedIncome = amountWaived.zero(); - Money chargeComponent = amountWaived; - if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) { - Money receivableCharge; - if (loanInstallmentNumber != null) { - receivableCharge = accruedCharge - .minus(loanCharge.getInstallmentLoanCharge(loanInstallmentNumber).getAmountPaid(getCurrency())); - } else { - receivableCharge = accruedCharge.minus(loanCharge.getAmountPaid(getCurrency())); - } - if (receivableCharge.isLessThanZero()) { - receivableCharge = amountWaived.zero(); - } - if (amountWaived.isGreaterThan(receivableCharge)) { - chargeComponent = receivableCharge; - unrecognizedIncome = amountWaived.minus(receivableCharge); - } - } - Money feeChargesWaived = chargeComponent; - Money penaltyChargesWaived = Money.zero(getCurrency()); - if (loanCharge.isPenaltyCharge()) { - penaltyChargesWaived = chargeComponent; - feeChargesWaived = Money.zero(getCurrency()); - } - - LocalDate transactionDate = getDisbursementDate(); - LocalDate businessDate = DateUtils.getBusinessLocalDate(); - if (loanCharge.isDueDateCharge()) { - if (DateUtils.isAfter(loanCharge.getDueLocalDate(), businessDate)) { - transactionDate = businessDate; - } else { - transactionDate = loanCharge.getDueLocalDate(); - } - } else if (loanCharge.isInstalmentFee()) { - LocalDate repaymentDueDate = loanCharge.getInstallmentLoanCharge(loanInstallmentNumber).getRepaymentInstallment().getDueDate(); - if (DateUtils.isAfter(repaymentDueDate, businessDate)) { - transactionDate = businessDate; - } else { - transactionDate = repaymentDueDate; - } - } - - scheduleGeneratorDTO.setRecalculateFrom(transactionDate); - - updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); - - existingTransactionIds.addAll(findExistingTransactionIds()); - existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); - - final LoanTransaction waiveLoanChargeTransaction = LoanTransaction.waiveLoanCharge(this, getOffice(), amountWaived, transactionDate, - feeChargesWaived, penaltyChargesWaived, unrecognizedIncome, externalId); - final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(waiveLoanChargeTransaction, loanCharge, - waiveLoanChargeTransaction.getAmount(getCurrency()).getAmount(), loanInstallmentNumber); - waiveLoanChargeTransaction.getLoanChargesPaid().add(loanChargePaidBy); - addLoanTransaction(waiveLoanChargeTransaction); - if (this.repaymentScheduleDetail().isInterestRecalculationEnabled() - && DateUtils.isBefore(loanCharge.getDueLocalDate(), businessDate)) { - regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); - } - // Waive of charges whose due date falls after latest 'repayment' transaction don't require entire loan schedule - // to be reprocessed. - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(); - if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(getCurrency())) { - /* - * TODO Vishwas Currently we do not allow waiving fully paid loan charge and waiving partially paid loan - * charges only waives the remaining amount. - * - * Consider removing this block of code or logically completing it for the future by getting the list of - * affected Transactions - */ - reprocessTransactions(); - } else { - // reprocess loan schedule based on charge been waived. - final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper(); - wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), getActiveCharges()); - } - - updateLoanSummaryDerivedFields(); - - doPostLoanTransactionChecks(waiveLoanChargeTransaction.getTransactionDate(), loanLifecycleStateMachine); - - return waiveLoanChargeTransaction; - } - public Client client() { return this.client; } @@ -1182,17 +1032,6 @@ public void updateLoanPurpose(final CodeValue loanPurpose) { this.loanPurpose = loanPurpose; } - public void updateLoanOfficerOnLoanApplication(final Staff newLoanOfficer) { - if (!isSubmittedAndPendingApproval()) { - Long loanOfficerId = null; - if (this.loanOfficer != null) { - loanOfficerId = this.loanOfficer.getId(); - } - throw new LoanOfficerAssignmentException(getId(), loanOfficerId); - } - this.loanOfficer = newLoanOfficer; - } - public void updateTransactionProcessingStrategy(final String transactionProcessingStrategyCode, final String transactionProcessingStrategyName) { this.transactionProcessingStrategyCode = transactionProcessingStrategyCode; @@ -1348,15 +1187,6 @@ public void updateLoanSummaryAndStatus() { doPostLoanTransactionChecks(getLastUserTransactionDate(), loanLifecycleStateMachine); } - public void recalculateAllCharges() { - Set charges = this.getActiveCharges(); - int penaltyWaitPeriod = 0; - for (final LoanCharge loanCharge : charges) { - recalculateLoanCharge(loanCharge, penaltyWaitPeriod); - } - updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); - } - public boolean isInterestRecalculationEnabledForProduct() { return this.loanProduct.isInterestRecalculationEnabled(); } @@ -1368,7 +1198,7 @@ public boolean isMultiDisburmentLoan() { /** * Update interest recalculation settings if product configuration changes */ - private void updateOverdueScheduleInstallment(final LoanCharge loanCharge) { + public void updateOverdueScheduleInstallment(final LoanCharge loanCharge) { if (loanCharge.isOverdueInstallmentCharge() && loanCharge.isActive()) { LoanOverdueInstallmentCharge overdueInstallmentCharge = loanCharge.getOverdueInstallmentCharge(); if (overdueInstallmentCharge != null) { @@ -1379,32 +1209,7 @@ private void updateOverdueScheduleInstallment(final LoanCharge loanCharge) { } } - private void recalculateLoanCharge(final LoanCharge loanCharge, final int penaltyWaitPeriod) { - BigDecimal amount = BigDecimal.ZERO; - BigDecimal chargeAmt; - BigDecimal totalChargeAmt = BigDecimal.ZERO; - if (loanCharge.getChargeCalculation().isPercentageBased()) { - if (loanCharge.isOverdueInstallmentCharge()) { - amount = calculateOverdueAmountPercentageAppliedTo(loanCharge, penaltyWaitPeriod); - } else { - amount = calculateAmountPercentageAppliedTo(loanCharge); - } - chargeAmt = loanCharge.getPercentage(); - if (loanCharge.isInstalmentFee()) { - totalChargeAmt = calculatePerInstallmentChargeAmount(loanCharge); - } - } else { - chargeAmt = loanCharge.amountOrPercentage(); - } - if (loanCharge.isActive()) { - clearLoanInstallmentChargesBeforeRegeneration(loanCharge); - loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, fetchNumberOfInstallmensAfterExceptions(), totalChargeAmt); - validateChargeHasValidSpecifiedDateIfApplicable(loanCharge, getDisbursementDate()); - } - - } - - private void clearLoanInstallmentChargesBeforeRegeneration(final LoanCharge loanCharge) { + public void clearLoanInstallmentChargesBeforeRegeneration(final LoanCharge loanCharge) { /* * JW https://issues.apache.org/jira/browse/FINERACT-1557 For loan installment charges only : Clear down * installment charges from the loanCharge and from each of the repayment installments and allow them to be @@ -1433,7 +1238,7 @@ private void clearLoanInstallmentChargesBeforeRegeneration(final LoanCharge loan } } - private BigDecimal calculateOverdueAmountPercentageAppliedTo(final LoanCharge loanCharge, final int penaltyWaitPeriod) { + public BigDecimal calculateOverdueAmountPercentageAppliedTo(final LoanCharge loanCharge, final int penaltyWaitPeriod) { LoanRepaymentScheduleInstallment installment = loanCharge.getOverdueInstallmentCharge().getInstallment(); LocalDate graceDate = DateUtils.getBusinessLocalDate().minusDays(penaltyWaitPeriod); Money amount = Money.zero(getCurrency()); @@ -1461,7 +1266,7 @@ private Money calculateOverdueAmountPercentageAppliedTo(LoanRepaymentScheduleIns } // This method returns date format and locale if present in the JsonCommand - private Map getDateFormatAndLocale(final JsonCommand jsonCommand) { + public Map getDateFormatAndLocale(final JsonCommand jsonCommand) { Map returnObject = new HashMap<>(); JsonElement jsonElement = jsonCommand.parsedJson(); if (jsonElement.isJsonObject()) { @@ -1481,7 +1286,7 @@ private Map getDateFormatAndLocale(final JsonCommand jsonCommand return returnObject; } - private Map parseDisbursementDetails(final JsonObject jsonObject, String dateFormat, Locale locale) { + public Map parseDisbursementDetails(final JsonObject jsonObject, String dateFormat, Locale locale) { Map returnObject = new HashMap<>(); if (jsonObject.get(LoanApiConstants.expectedDisbursementDateParameterName) != null && jsonObject.get(LoanApiConstants.expectedDisbursementDateParameterName).isJsonPrimitive()) { @@ -1518,128 +1323,7 @@ private Map parseDisbursementDetails(final JsonObject jsonObject return returnObject; } - public void updateDisbursementDetails(final JsonCommand jsonCommand, final Map actualChanges) { - List disbursementList = fetchDisbursementIds(); - List loanChargeIds = fetchLoanTrancheChargeIds(); - int chargeIdLength = loanChargeIds.size(); - String chargeIds; - // From modify application page, if user removes all charges, we should - // get empty array. - // So we need to remove all charges applied for this loan - boolean removeAllCharges = jsonCommand.parameterExists(LoanApiConstants.chargesParameterName) - && jsonCommand.arrayOfParameterNamed(LoanApiConstants.chargesParameterName).isEmpty(); - - if (jsonCommand.parameterExists(LoanApiConstants.disbursementDataParameterName)) { - final JsonArray disbursementDataArray = jsonCommand.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName); - if (disbursementDataArray != null && disbursementDataArray.size() > 0) { - String dateFormat = null; - Locale locale = null; - Map dateAndLocale = getDateFormatAndLocale(jsonCommand); - dateFormat = dateAndLocale.get(LoanApiConstants.dateFormatParameterName); - if (dateAndLocale.containsKey(LoanApiConstants.localeParameterName)) { - locale = JsonParserHelper.localeFromString(dateAndLocale.get(LoanApiConstants.localeParameterName)); - } - for (JsonElement jsonElement : disbursementDataArray) { - final JsonObject jsonObject = jsonElement.getAsJsonObject(); - Map parsedDisbursementData = parseDisbursementDetails(jsonObject, dateFormat, locale); - LocalDate expectedDisbursementDate = (LocalDate) parsedDisbursementData - .get(LoanApiConstants.expectedDisbursementDateParameterName); - BigDecimal principal = (BigDecimal) parsedDisbursementData.get(LoanApiConstants.disbursementPrincipalParameterName); - Long disbursementID = (Long) parsedDisbursementData.get(LoanApiConstants.disbursementIdParameterName); - chargeIds = (String) parsedDisbursementData.get(LoanApiConstants.loanChargeIdParameterName); - if (chargeIds != null) { - if (chargeIds.contains(",")) { - Iterable chargeId = Splitter.on(',').split(chargeIds); - for (String loanChargeId : chargeId) { - loanChargeIds.remove(Long.parseLong(loanChargeId)); - } - } else { - loanChargeIds.remove(Long.parseLong(chargeIds)); - } - } - createOrUpdateDisbursementDetails(disbursementID, actualChanges, expectedDisbursementDate, principal, disbursementList); - } - removeDisbursementAndAssociatedCharges(actualChanges, disbursementList, loanChargeIds, chargeIdLength, removeAllCharges); - } - } - } - - private void removeDisbursementAndAssociatedCharges(final Map actualChanges, List disbursementList, - List loanChargeIds, int chargeIdLength, boolean removeAllChages) { - if (removeAllChages) { - LoanCharge[] tempCharges = new LoanCharge[this.charges.size()]; - this.charges.toArray(tempCharges); - for (LoanCharge loanCharge : tempCharges) { - removeLoanCharge(loanCharge); - } - this.trancheCharges.clear(); - } else { - if (!loanChargeIds.isEmpty() && loanChargeIds.size() != chargeIdLength) { - for (Long chargeId : loanChargeIds) { - LoanCharge deleteCharge = fetchLoanChargesById(chargeId); - if (this.charges.contains(deleteCharge)) { - removeLoanCharge(deleteCharge); - } - } - } - } - for (Long id : disbursementList) { - removeChargesByDisbursementID(id); - this.disbursementDetails.remove(fetchLoanDisbursementsById(id)); - actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true); - } - } - - private void createOrUpdateDisbursementDetails(Long disbursementID, final Map actualChanges, - LocalDate expectedDisbursementDate, BigDecimal principal, List existingDisbursementList) { - if (disbursementID != null) { - LoanDisbursementDetails loanDisbursementDetail = fetchLoanDisbursementsById(disbursementID); - existingDisbursementList.remove(disbursementID); - if (loanDisbursementDetail.actualDisbursementDate() == null) { - LocalDate actualDisbursementDate = null; - LoanDisbursementDetails disbursementDetails = new LoanDisbursementDetails(expectedDisbursementDate, actualDisbursementDate, - principal, this.netDisbursalAmount, false); - disbursementDetails.updateLoan(this); - if (!loanDisbursementDetail.equals(disbursementDetails)) { - loanDisbursementDetail.copy(disbursementDetails); - actualChanges.put("disbursementDetailId", disbursementID); - actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true); - } - } - } else { - LocalDate actualDisbursementDate = null; - LoanDisbursementDetails disbursementDetails = new LoanDisbursementDetails(expectedDisbursementDate, actualDisbursementDate, - principal, this.netDisbursalAmount, false); - disbursementDetails.updateLoan(this); - this.disbursementDetails.add(disbursementDetails); - for (LoanTrancheCharge trancheCharge : trancheCharges) { - Charge chargeDefinition = trancheCharge.getCharge(); - ExternalId externalId = ExternalId.empty(); - if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { - externalId = ExternalId.generate(); - } - final LoanCharge loanCharge = new LoanCharge(this, chargeDefinition, principal, null, null, null, expectedDisbursementDate, - null, null, BigDecimal.ZERO, externalId); - LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, - disbursementDetails); - loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge); - addLoanCharge(loanCharge); - } - actualChanges.put(LoanApiConstants.disbursementDataParameterName, expectedDisbursementDate + "-" + principal); - actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true); - } - } - - private void removeChargesByDisbursementID(Long id) { - getCharges().stream() // - .filter(charge -> { // - LoanTrancheDisbursementCharge transCharge = charge.getTrancheDisbursementCharge(); // - return transCharge != null && id.equals(transCharge.getloanDisbursementDetails().getId()); // - }) // - .forEach(this::removeLoanCharge); - } - - private List fetchLoanTrancheChargeIds() { + public List fetchLoanTrancheChargeIds() { return getCharges().stream()// .filter(charge -> charge.isTrancheDisbursementCharge() && charge.isActive()) // .map(LoanCharge::getId) // @@ -1653,7 +1337,7 @@ public LoanDisbursementDetails fetchLoanDisbursementsById(Long id) { .orElse(null); } - private List fetchDisbursementIds() { + public List fetchDisbursementIds() { return getDisbursementDetails().stream() // .map(LoanDisbursementDetails::getId) // .collect(Collectors.toList()); @@ -1675,35 +1359,7 @@ private LocalDate determineExpectedMaturityDate() { return maturityDate; } - public List getLoanDisbursementDetails() { - List currentDisbursementDetails = getDisbursementDetails(); - if (loanProduct.isDisallowExpectedDisbursements()) { - if (!currentDisbursementDetails.isEmpty()) { - final String errorMessage = "For this loan product, disbursement details are not allowed"; - throw new MultiDisbursementDataNotAllowedException(LoanApiConstants.disbursementDataParameterName, errorMessage); - } - } else { - if (currentDisbursementDetails.isEmpty()) { - final String errorMessage = "For this loan product, disbursement details must be provided"; - throw new MultiDisbursementDataRequiredException(LoanApiConstants.disbursementDataParameterName, errorMessage); - } - } - return currentDisbursementDetails; - } - - @Deprecated // moved to LoanApplicationValidator - private BigDecimal getOverAppliedMax() { - if ("percentage".equals(getLoanProduct().getOverAppliedCalculationType())) { - BigDecimal overAppliedNumber = BigDecimal.valueOf(getLoanProduct().getOverAppliedNumber()); - BigDecimal totalPercentage = BigDecimal.valueOf(1).add(overAppliedNumber.divide(BigDecimal.valueOf(100))); - return proposedPrincipal.multiply(totalPercentage); - } else { - return proposedPrincipal.add(BigDecimal.valueOf(getLoanProduct().getOverAppliedNumber())); - } - } - public Map undoApproval(final LoanLifecycleStateMachine loanLifecycleStateMachine) { - validateAccountStatus(LoanEvent.LOAN_APPROVAL_UNDO); final Map actualChanges = new LinkedHashMap<>(); final LoanStatus currentStatus = getStatus(); @@ -1765,84 +1421,7 @@ && isAllTranchesNotDisbursed()) { return !statusEnum.hasStateOf(actualLoanStatus) || isMultiTrancheDisburse; } - public Money adjustDisburseAmount(@NotNull JsonCommand command, @NotNull LocalDate actualDisbursementDate) { - Money disburseAmount = this.loanRepaymentScheduleDetail.getPrincipal().zero(); - BigDecimal principalDisbursed = command.bigDecimalValueOfParameterNamed(LoanApiConstants.principalDisbursedParameterName); - if (this.actualDisbursementDate == null || DateUtils.isBefore(actualDisbursementDate, this.actualDisbursementDate)) { - this.actualDisbursementDate = actualDisbursementDate; - } - BigDecimal diff = BigDecimal.ZERO; - Collection details = fetchUndisbursedDetail(); - if (principalDisbursed == null) { - disburseAmount = this.loanRepaymentScheduleDetail.getPrincipal(); - if (!details.isEmpty()) { - disburseAmount = disburseAmount.zero(); - for (LoanDisbursementDetails disbursementDetails : details) { - disbursementDetails.updateActualDisbursementDate(actualDisbursementDate); - disburseAmount = disburseAmount.plus(disbursementDetails.principal()); - } - } - } else { - if (this.loanProduct.isMultiDisburseLoan()) { - disburseAmount = Money.of(getCurrency(), principalDisbursed); - } else { - disburseAmount = disburseAmount.plus(principalDisbursed); - } - - if (details.isEmpty()) { - diff = this.loanRepaymentScheduleDetail.getPrincipal().minus(principalDisbursed).getAmount(); - } else { - for (LoanDisbursementDetails disbursementDetails : details) { - disbursementDetails.updateActualDisbursementDate(actualDisbursementDate); - disbursementDetails.updatePrincipal(principalDisbursed); - } - } - if (this.loanProduct().isMultiDisburseLoan()) { - Collection loanDisburseDetails = this.getDisbursementDetails(); - BigDecimal setPrincipalAmount = BigDecimal.ZERO; - BigDecimal totalAmount = BigDecimal.ZERO; - for (LoanDisbursementDetails disbursementDetails : loanDisburseDetails) { - if (disbursementDetails.actualDisbursementDate() != null) { - setPrincipalAmount = setPrincipalAmount.add(disbursementDetails.principal()); - } - totalAmount = totalAmount.add(disbursementDetails.principal()); - } - this.loanRepaymentScheduleDetail.setPrincipal(setPrincipalAmount); - compareDisbursedToApprovedOrProposedPrincipal(disburseAmount.getAmount(), totalAmount); - } else { - this.loanRepaymentScheduleDetail.setPrincipal(this.loanRepaymentScheduleDetail.getPrincipal().minus(diff).getAmount()); - } - if (!this.loanProduct().isMultiDisburseLoan() && diff.compareTo(BigDecimal.ZERO) < 0) { - final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved amount "; - throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.amount", principalDisbursed, - this.loanRepaymentScheduleDetail.getPrincipal().getAmount()); - } - } - return disburseAmount; - } - - private void compareDisbursedToApprovedOrProposedPrincipal(BigDecimal disbursedAmount, BigDecimal totalDisbursed) { - if (this.loanProduct().isDisallowExpectedDisbursements() && this.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { - BigDecimal maxDisbursedAmount = getOverAppliedMax(); - if (totalDisbursed.compareTo(maxDisbursedAmount) > 0) { - final String errorMessage = String.format( - "Loan disbursal amount can't be greater than maximum applied loan amount calculation. " - + "Total disbursed amount: %s Maximum disbursal amount: %s", - totalDisbursed.stripTrailingZeros().toPlainString(), maxDisbursedAmount.stripTrailingZeros().toPlainString()); - throw new InvalidLoanStateTransitionException("disbursal", - "amount.can't.be.greater.than.maximum.applied.loan.amount.calculation", errorMessage, disbursedAmount, - maxDisbursedAmount); - } - } else { - if (totalDisbursed.compareTo(this.approvedPrincipal) > 0) { - final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved principal "; - throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.principal", totalDisbursed, - this.approvedPrincipal); - } - } - } - - private Collection fetchUndisbursedDetail() { + public Collection fetchUndisbursedDetail() { Collection disbursementDetails = new ArrayList<>(); LocalDate date = null; for (LoanDisbursementDetails disbursementDetail : getDisbursementDetails()) { @@ -1892,11 +1471,11 @@ public BigDecimal getDisbursedAmount() { return principal; } - private void removeDisbursementDetail() { + public void removeDisbursementDetail() { getDisbursementDetails().removeIf(it -> it.actualDisbursementDate() == null); } - private boolean isDisbursementAllowed() { + public boolean isDisbursementAllowed() { List disbursementDetails = getDisbursementDetails(); boolean isSingleDisburseLoanDisbursementAllowed = disbursementDetails == null || disbursementDetails.isEmpty() || disbursementDetails.stream().anyMatch(it -> it.actualDisbursementDate() == null); @@ -1906,7 +1485,7 @@ private boolean isDisbursementAllowed() { return isSingleDisburseLoanDisbursementAllowed || isMultiDisburseLoanDisbursementAllowed; } - private boolean atLeastOnceDisbursed() { + public boolean atLeastOnceDisbursed() { return getDisbursementDetails().stream().anyMatch(it -> it.actualDisbursementDate() != null); } @@ -1917,23 +1496,6 @@ public void updateLoanRepaymentPeriodsDerivedFields(final LocalDate actualDisbur } } - /** - * Ability to regenerate the repayment schedule based on the loans current details/state. - */ - public void regenerateRepaymentSchedule(final ScheduleGeneratorDTO scheduleGeneratorDTO) { - final LoanScheduleModel loanSchedule = regenerateScheduleModel(scheduleGeneratorDTO); - if (loanSchedule == null) { - return; - } - updateLoanSchedule(loanSchedule); - final Set charges = this.getActiveCharges(); - for (final LoanCharge loanCharge : charges) { - if (!loanCharge.isWaived()) { - recalculateLoanCharge(loanCharge, scheduleGeneratorDTO.getPenaltyWaitPeriod()); - } - } - } - public LoanScheduleModel regenerateScheduleModel(final ScheduleGeneratorDTO scheduleGeneratorDTO) { final MathContext mc = MoneyHelper.getMathContext(); @@ -1984,121 +1546,6 @@ private BigDecimal constructFloatingInterestRates(final BigDecimal annualNominal return interestRate; } - public void handleDisbursementTransaction(final LocalDate disbursedOn, final PaymentDetail paymentDetail) { - // add repayment transaction to track incoming money from client to mfi - // for (charges due at time of disbursement) - - /* - * TODO Vishwas: do we need to be able to pass in payment type details for repayments at disbursements too? - */ - - final Money totalFeeChargesDueAtDisbursement = this.summary.getTotalFeeChargesDueAtDisbursement(getCurrency()); - /* - * all Charges repaid at disbursal is marked as repaid and "APPLY Charge" transactions are created for all other - * fees ( which are created during disbursal but not repaid) - */ - - Money disbursentMoney = Money.zero(getCurrency()); - final LoanTransaction chargesPayment = LoanTransaction.repaymentAtDisbursement(getOffice(), disbursentMoney, paymentDetail, - disbursedOn, null); - final Integer installmentNumber = null; - for (final LoanCharge charge : getActiveCharges()) { - LocalDate actualDisbursementDate = getActualDisbursementDate(charge); - /* - * create a Charge applied transaction if Up front Accrual, None or Cash based accounting is enabled - */ - if ((charge.getCharge().getChargeTimeType().equals(ChargeTimeType.DISBURSEMENT.getValue()) - && disbursedOn.equals(actualDisbursementDate) && !charge.isWaived() && !charge.isFullyPaid()) - || (charge.getCharge().getChargeTimeType().equals(ChargeTimeType.TRANCHE_DISBURSEMENT.getValue()) - && disbursedOn.equals(actualDisbursementDate) && !charge.isWaived() && !charge.isFullyPaid())) { - if (totalFeeChargesDueAtDisbursement.isGreaterThanZero() && !charge.getChargePaymentMode().isPaymentModeAccountTransfer()) { - charge.markAsFullyPaid(); - // Add "Loan Charge Paid By" details to this transaction - final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, charge.amount(), - installmentNumber); - chargesPayment.getLoanChargesPaid().add(loanChargePaidBy); - disbursentMoney = disbursentMoney.plus(charge.amount()); - } - } else if (disbursedOn.equals(this.actualDisbursementDate) && isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { - handleChargeAppliedTransaction(charge, disbursedOn); - } - } - - if (disbursentMoney.isGreaterThanZero()) { - final Money zero = Money.zero(getCurrency()); - chargesPayment.updateComponentsAndTotal(zero, zero, disbursentMoney, zero); - chargesPayment.updateLoan(this); - addLoanTransaction(chargesPayment); - updateLoanOutstandingBalances(); - } - - LocalDate expectedDate = getExpectedFirstRepaymentOnDate(); - if (expectedDate != null && (DateUtils.isAfter(disbursedOn, this.fetchRepaymentScheduleInstallment(1).getDueDate()) - || DateUtils.isAfter(disbursedOn, expectedDate)) && DateUtils.isEqual(disbursedOn, this.actualDisbursementDate)) { - final String errorMessage = "submittedOnDate cannot be after the loans expectedFirstRepaymentOnDate: " + expectedDate; - throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.after.expected.first.repayment.date", errorMessage, - disbursedOn, expectedDate); - } - - validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_DISBURSED, disbursedOn); - - if (DateUtils.isDateInTheFuture(disbursedOn)) { - final String errorMessage = "The date on which a loan with identifier : " + this.accountNumber - + " is disbursed cannot be in the future."; - throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.a.future.date", errorMessage, disbursedOn); - } - } - - public LoanTransaction handleDownPayment(final LoanTransaction disbursementTransaction, final JsonCommand command, - final ScheduleGeneratorDTO scheduleGeneratorDTO) { - LocalDate disbursedOn = command.localDateValueOfParameterNamed(ACTUAL_DISBURSEMENT_DATE); - BigDecimal disbursedAmountPercentageForDownPayment = this.loanRepaymentScheduleDetail.getDisbursedAmountPercentageForDownPayment(); - ExternalId externalId = ExternalId.empty(); - if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { - externalId = ExternalId.generate(); - } - Money downPaymentMoney = Money.of(getCurrency(), - MathUtil.percentageOf(disbursementTransaction.getAmount(), disbursedAmountPercentageForDownPayment, 19)); - if (getLoanProduct().getInstallmentAmountInMultiplesOf() != null) { - downPaymentMoney = Money.roundToMultiplesOf(downPaymentMoney, getLoanProduct().getInstallmentAmountInMultiplesOf()); - } - Money adjustedDownPaymentMoney = switch (getLoanProductRelatedDetail().getLoanScheduleType()) { - // For Cumulative loan: To check whether the loan was overpaid when the disbursement happened and to get the - // proper amount after the disbursement we are using two balances: - // 1. Whether the loan is still overpaid after the disbursement, - // 2. if the loan is not overpaid anymore after the disbursement, but was it more overpaid than the - // calculated down-payment amount? - case CUMULATIVE -> { - if (getTotalOverpaidAsMoney().isGreaterThanZero()) { - yield Money.zero(getCurrency()); - } - yield MathUtil.negativeToZero(downPaymentMoney.minus(MathUtil.negativeToZero(disbursementTransaction - .getAmount(getCurrency()).minus(disbursementTransaction.getOutstandingLoanBalanceMoney(getCurrency()))))); - } - // For Progressive loan: Disbursement transaction portion balances are enough to see whether the overpayment - // amount was more than the calculated down-payment amount - case PROGRESSIVE -> - MathUtil.negativeToZero(downPaymentMoney.minus(disbursementTransaction.getOverPaymentPortion(getCurrency()))); - }; - - if (adjustedDownPaymentMoney.isGreaterThanZero()) { - LoanTransaction downPaymentTransaction = LoanTransaction.downPayment(getOffice(), adjustedDownPaymentMoney, null, disbursedOn, - externalId); - LoanEvent event = LoanEvent.LOAN_REPAYMENT_OR_WAIVER; - validateRepaymentTypeAccountStatus(downPaymentTransaction, event); - HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO(); - validateRepaymentDateIsOnHoliday(downPaymentTransaction.getTransactionDate(), holidayDetailDTO.isAllowTransactionsOnHoliday(), - holidayDetailDTO.getHolidays()); - validateRepaymentDateIsOnNonWorkingDay(downPaymentTransaction.getTransactionDate(), holidayDetailDTO.getWorkingDays(), - holidayDetailDTO.isAllowTransactionsOnNonWorkingDay()); - - handleRepaymentOrRecoveryOrWaiverTransaction(downPaymentTransaction, loanLifecycleStateMachine, null, scheduleGeneratorDTO); - return downPaymentTransaction; - } else { - return null; - } - } - public boolean isAutoRepaymentForDownPaymentEnabled() { return this.loanRepaymentScheduleDetail.isEnableDownPayment() && this.loanRepaymentScheduleDetail.isEnableAutoRepaymentForDownPayment(); @@ -2128,355 +1575,6 @@ public void removePostDatedChecks() { this.postDatedChecks = new ArrayList<>(); } - public Map undoDisbursal(final ScheduleGeneratorDTO scheduleGeneratorDTO, final List existingTransactionIds, - final List existingReversedTransactionIds) { - validateAccountStatus(LoanEvent.LOAN_DISBURSAL_UNDO); - - final Map actualChanges = new LinkedHashMap<>(); - final LoanStatus currentStatus = getStatus(); - final LoanStatus statusEnum = this.loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_DISBURSAL_UNDO, this); - validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_DISBURSAL_UNDO, getDisbursementDate()); - existingTransactionIds.addAll(findExistingTransactionIds()); - existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); - if (!statusEnum.hasStateOf(currentStatus)) { - this.loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSAL_UNDO, this); - actualChanges.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus)); - - final LocalDate actualDisbursementDate = getDisbursementDate(); - final boolean isScheduleRegenerateRequired = isActualDisbursedOnDateEarlierOrLaterThanExpected(actualDisbursementDate); - this.actualDisbursementDate = null; - this.disbursedBy = null; - boolean isDisbursedAmountChanged = !MathUtil.isEqualTo(approvedPrincipal, - this.loanRepaymentScheduleDetail.getPrincipal().getAmount()); - this.loanRepaymentScheduleDetail.setPrincipal(this.approvedPrincipal); - // Remove All the Disbursement Details If the Loan Product is disabled and exists one - if (this.loanProduct().isDisallowExpectedDisbursements() && !getDisbursementDetails().isEmpty()) { - for (LoanDisbursementDetails disbursementDetail : getAllDisbursementDetails()) { - disbursementDetail.reverse(); - } - } else { - for (final LoanDisbursementDetails details : getDisbursementDetails()) { - details.updateActualDisbursementDate(null); - } - } - boolean isEmiAmountChanged = !this.loanTermVariations.isEmpty(); - - updateLoanToPreDisbursalState(); - if (isScheduleRegenerateRequired || isDisbursedAmountChanged || isEmiAmountChanged - || this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { - // clear off actual disbusrement date so schedule regeneration - // uses expected date. - - regenerateRepaymentSchedule(scheduleGeneratorDTO); - if (isDisbursedAmountChanged) { - updateSummaryWithTotalFeeChargesDueAtDisbursement(deriveSumTotalOfChargesDueAtDisbursement()); - } - } else if (isPeriodicAccrualAccountingEnabledOnLoanProduct()) { - for (final LoanRepaymentScheduleInstallment period : getRepaymentScheduleInstallments()) { - period.resetAccrualComponents(); - } - } - - if (this.isTopup) { - this.loanTopupDetails.setAccountTransferDetails(null); - this.loanTopupDetails.setTopupAmount(null); - } - - this.adjustNetDisbursalAmount(this.approvedPrincipal); - actualChanges.put(ACTUAL_DISBURSEMENT_DATE, ""); - updateLoanSummaryDerivedFields(); - } - - return actualChanges; - } - - private void reverseExistingTransactions() { - Collection retainTransactions = new ArrayList<>(); - for (final LoanTransaction transaction : this.loanTransactions) { - transaction.reverse(); - if (transaction.getId() != null) { - retainTransactions.add(transaction); - } - } - this.loanTransactions.retainAll(retainTransactions); - } - - private void updateLoanToPreDisbursalState() { - this.actualDisbursementDate = null; - - this.accruedTill = null; - reverseExistingTransactions(); - - for (final LoanCharge charge : getActiveCharges()) { - if (charge.isOverdueInstallmentCharge()) { - charge.setActive(false); - } else { - charge.resetToOriginal(getCurrency()); - } - } - List installments = getRepaymentScheduleInstallments(); - for (final LoanRepaymentScheduleInstallment currentInstallment : installments) { - currentInstallment.resetDerivedComponents(); - } - for (LoanTermVariations variations : this.loanTermVariations) { - if (variations.getOnLoanStatus().equals(LoanStatus.ACTIVE.getValue())) { - variations.markAsInactive(); - } - } - final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper(); - wrapper.reprocess(getCurrency(), getDisbursementDate(), getRepaymentScheduleInstallments(), getActiveCharges()); - - updateLoanSummaryDerivedFields(); - } - - public ChangedTransactionDetail waiveInterest(final LoanTransaction waiveInterestTransaction, - final LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, - final List existingReversedTransactionIds, final ScheduleGeneratorDTO scheduleGeneratorDTO) { - validateAccountStatus(LoanEvent.LOAN_REPAYMENT_OR_WAIVER); - validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, - waiveInterestTransaction.getTransactionDate()); - validateActivityNotBeforeLastTransactionDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, waiveInterestTransaction.getTransactionDate()); - - existingTransactionIds.addAll(findExistingTransactionIds()); - existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); - - return handleRepaymentOrRecoveryOrWaiverTransaction(waiveInterestTransaction, loanLifecycleStateMachine, null, - scheduleGeneratorDTO); - } - - @SuppressWarnings("null") - public ChangedTransactionDetail makeRepayment(final LoanTransaction repaymentTransaction, - final LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, - final List existingReversedTransactionIds, boolean isRecoveryRepayment, final ScheduleGeneratorDTO scheduleGeneratorDTO, - Boolean isHolidayValidationDone) { - LoanEvent event = isRecoveryRepayment ? LoanEvent.LOAN_RECOVERY_PAYMENT : LoanEvent.LOAN_REPAYMENT_OR_WAIVER; - - HolidayDetailDTO holidayDetailDTO = null; - if (!isHolidayValidationDone) { - holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO(); - } - validateRepaymentTypeAccountStatus(repaymentTransaction, event); - validateActivityNotBeforeClientOrGroupTransferDate(event, repaymentTransaction.getTransactionDate()); - validateRepaymentTypeTransactionNotBeforeAChargeRefund(repaymentTransaction, "created"); - validateActivityNotBeforeLastTransactionDate(event, repaymentTransaction.getTransactionDate()); - if (!isHolidayValidationDone) { - validateRepaymentDateIsOnHoliday(repaymentTransaction.getTransactionDate(), holidayDetailDTO.isAllowTransactionsOnHoliday(), - holidayDetailDTO.getHolidays()); - validateRepaymentDateIsOnNonWorkingDay(repaymentTransaction.getTransactionDate(), holidayDetailDTO.getWorkingDays(), - holidayDetailDTO.isAllowTransactionsOnNonWorkingDay()); - } - existingTransactionIds.addAll(findExistingTransactionIds()); - existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); - - return handleRepaymentOrRecoveryOrWaiverTransaction(repaymentTransaction, loanLifecycleStateMachine, null, scheduleGeneratorDTO); - } - - private void validateRepaymentTypeAccountStatus(LoanTransaction repaymentTransaction, LoanEvent event) { - if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isInterestPaymentWaiver() - || repaymentTransaction.isMerchantIssuedRefund() || repaymentTransaction.isPayoutRefund() - || repaymentTransaction.isChargeRefund() || repaymentTransaction.isRepayment() || repaymentTransaction.isDownPayment() - || repaymentTransaction.isInterestRefund()) { - - if (!(isOpen() || isClosedObligationsMet() || isOverPaid())) { - final List dataValidationErrors = new ArrayList<>(); - final String defaultUserMessage = "Loan must be Active, Fully Paid or Overpaid"; - final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.must.be.active.fully.paid.or.overpaid", - defaultUserMessage); - dataValidationErrors.add(error); - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } else { - validateAccountStatus(event); - } - - } - - public void makeChargePayment(final Long chargeId, final LoanLifecycleStateMachine loanLifecycleStateMachine, - final List existingTransactionIds, final List existingReversedTransactionIds, - final HolidayDetailDTO holidayDetailDTO, final LoanTransaction paymentTransaction, final Integer installmentNumber) { - validateAccountStatus(LoanEvent.LOAN_CHARGE_PAYMENT); - validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_CHARGE_PAYMENT, paymentTransaction.getTransactionDate()); - validateActivityNotBeforeLastTransactionDate(LoanEvent.LOAN_CHARGE_PAYMENT, paymentTransaction.getTransactionDate()); - validateRepaymentDateIsOnHoliday(paymentTransaction.getTransactionDate(), holidayDetailDTO.isAllowTransactionsOnHoliday(), - holidayDetailDTO.getHolidays()); - validateRepaymentDateIsOnNonWorkingDay(paymentTransaction.getTransactionDate(), holidayDetailDTO.getWorkingDays(), - holidayDetailDTO.isAllowTransactionsOnNonWorkingDay()); - - if (DateUtils.isDateInTheFuture(paymentTransaction.getTransactionDate())) { - final String errorMessage = "The date on which a loan charge paid cannot be in the future."; - throw new InvalidLoanStateTransitionException("charge.payment", "cannot.be.a.future.date", errorMessage, - paymentTransaction.getTransactionDate()); - } - existingTransactionIds.addAll(findExistingTransactionIds()); - existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); - LoanCharge charge = null; - for (final LoanCharge loanCharge : this.charges) { - if (loanCharge.isActive() && chargeId.equals(loanCharge.getId())) { - charge = loanCharge; - } - } - handleChargePaidTransaction(charge, paymentTransaction, loanLifecycleStateMachine, installmentNumber); - } - - public void makeRefund(final LoanTransaction loanTransaction, final LoanLifecycleStateMachine loanLifecycleStateMachine, - final List existingTransactionIds, final List existingReversedTransactionIds, - final boolean allowTransactionsOnHoliday, final List holidays, final WorkingDays workingDays, - final boolean allowTransactionsOnNonWorkingDay) { - validateRepaymentDateIsOnHoliday(loanTransaction.getTransactionDate(), allowTransactionsOnHoliday, holidays); - validateRepaymentDateIsOnNonWorkingDay(loanTransaction.getTransactionDate(), workingDays, allowTransactionsOnNonWorkingDay); - - existingTransactionIds.addAll(findExistingTransactionIds()); - existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); - - if (getStatus().isOverpaid()) { - if (this.totalOverpaid.compareTo(loanTransaction.getAmount(getCurrency()).getAmount()) < 0) { - final String errorMessage = "The refund amount must be less than or equal to overpaid amount "; - throw new InvalidLoanStateTransitionException("transaction", "is.exceeding.overpaid.amount", errorMessage, - this.totalOverpaid, loanTransaction.getAmount(getCurrency()).getAmount()); - } else if (!isAfterLastRepayment(loanTransaction, getLoanTransactions())) { - final String errorMessage = "Transfer funds is allowed only after last repayment date"; - throw new InvalidLoanStateTransitionException("transaction", "is.not.after.repayment.date", errorMessage); - } - } else { - final String errorMessage = "Transfer funds is allowed only for loan accounts with overpaid status "; - throw new InvalidLoanStateTransitionException("transaction", "is.not.a.overpaid.loan", errorMessage); - } - - loanTransaction.updateLoan(this); - - if (loanTransaction.isNotZero()) { - addLoanTransaction(loanTransaction); - } - updateLoanSummaryDerivedFields(); - doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine); - } - - public ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(final LoanTransaction loanTransaction, - final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction, - final ScheduleGeneratorDTO scheduleGeneratorDTO) { - ChangedTransactionDetail changedTransactionDetail = null; - - if (loanTransaction.isRecoveryRepayment()) { - loanLifecycleStateMachine.transition(LoanEvent.LOAN_RECOVERY_PAYMENT, this); - } - - if (loanTransaction.isRecoveryRepayment() - && loanTransaction.getAmount(getCurrency()).getAmount().compareTo(getSummary().getTotalWrittenOff()) > 0) { - 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); - } - - loanTransaction.updateLoan(this); - - final boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(loanTransaction); - - if (loanTransaction.isNotZero()) { - addLoanTransaction(loanTransaction); - } - - if (loanTransaction.isNotRepaymentLikeType() && loanTransaction.isNotWaiver() && loanTransaction.isNotRecoveryRepayment()) { - final String errorMessage = "A transaction of type repayment or recovery repayment or waiver was expected but not received."; - throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.repayment.or.waiver.or.recovery.transaction", - errorMessage); - } - - final LocalDate loanTransactionDate = extractTransactionDate(loanTransaction); - - if (DateUtils.isDateInTheFuture(loanTransactionDate)) { - final String errorMessage = "The transaction date cannot be in the future."; - throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, loanTransactionDate); - } - - if (loanTransaction.isInterestWaiver()) { - Money totalInterestOutstandingOnLoan = getTotalInterestOutstandingOnLoan(); - if (adjustedTransaction != null) { - totalInterestOutstandingOnLoan = totalInterestOutstandingOnLoan.plus(adjustedTransaction.getAmount(getCurrency())); - } - if (loanTransaction.getAmount(getCurrency()).isGreaterThan(totalInterestOutstandingOnLoan)) { - final String errorMessage = "The amount of interest to waive cannot be greater than total interest outstanding on loan."; - throw new InvalidLoanStateTransitionException("waive.interest", "amount.exceeds.total.outstanding.interest", errorMessage, - loanTransaction.getAmount(getCurrency()), totalInterestOutstandingOnLoan.getAmount()); - } - } - - if (this.loanProduct.isMultiDisburseLoan() && adjustedTransaction == null) { - BigDecimal totalDisbursed = getDisbursedAmount(); - BigDecimal totalPrincipalAdjusted = this.summary.getTotalPrincipalAdjustments(); - BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted); - if (totalPrincipalCredited.compareTo(this.summary.getTotalPrincipalRepaid()) < 0) { - final String errorMessage = "The transaction amount cannot exceed threshold."; - throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage); - } - } - - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(); - - final LoanRepaymentScheduleInstallment currentInstallment = fetchLoanRepaymentScheduleInstallmentByDueDate( - loanTransaction.getTransactionDate()); - - boolean reprocess = isForeclosure() || !isTransactionChronologicallyLatest || adjustedTransaction != null - || !DateUtils.isEqualBusinessDate(loanTransaction.getTransactionDate()) || currentInstallment == null - || !currentInstallment.getTotalOutstanding(getCurrency()).isEqualTo(loanTransaction.getAmount(getCurrency())); - - if (isTransactionChronologicallyLatest && adjustedTransaction == null - && (!reprocess || !this.repaymentScheduleDetail().isInterestRecalculationEnabled()) && !isForeclosure()) { - loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(), - getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null)); - reprocess = false; - if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { - if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) { - reprocess = true; - } else { - final LoanRepaymentScheduleInstallment nextInstallment = fetchRepaymentScheduleInstallment( - currentInstallment.getInstallmentNumber() + 1); - if (nextInstallment != null && nextInstallment.getTotalPaidInAdvance(getCurrency()).isGreaterThanZero()) { - reprocess = true; - } - } - } - } - if (reprocess) { - if (this.repaymentScheduleDetail().isInterestRecalculationEnabled() - && !getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)) { - regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); - } - changedTransactionDetail = reprocessTransactions(); - } - - updateLoanSummaryDerivedFields(); - - /** - * FIXME: Vishwas, skipping post loan transaction checks for Loan recoveries - **/ - if (loanTransaction.isNotRecoveryRepayment()) { - doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine); - } - - if (this.loanProduct.isMultiDisburseLoan()) { - BigDecimal totalDisbursed = getDisbursedAmount(); - BigDecimal totalPrincipalAdjusted = this.summary.getTotalPrincipalAdjustments(); - BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted); - if (totalPrincipalCredited.compareTo(this.summary.getTotalPrincipalRepaid()) < 0 - && this.repaymentScheduleDetail().getPrincipal().minus(totalDisbursed).isGreaterThanZero()) { - final String errorMessage = "The transaction amount cannot exceed threshold."; - throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage); - } - } - - return changedTransactionDetail; - } - - private LocalDate extractTransactionDate(LoanTransaction loanTransaction) { - final LocalDate loanTransactionDate = loanTransaction.getTransactionDate(); - if (DateUtils.isBefore(loanTransactionDate, getDisbursementDate())) { - final String errorMessage = "The transaction date cannot be before the loan disbursement date: " - + getDisbursementDate().toString(); - throw new InvalidLoanStateTransitionException("transaction", "cannot.be.before.disbursement.date", errorMessage, - loanTransactionDate, getDisbursementDate()); - } - return loanTransactionDate; - } - public List retrieveListOfTransactionsForReprocessing() { return getLoanTransactions().stream().filter(loanTransactionForReprocessingPredicate()).sorted(LoanTransactionComparator.INSTANCE) .collect(Collectors.toList()); @@ -2487,7 +1585,7 @@ private static Predicate loanTransactionForReprocessingPredicat || transaction.isAccrualActivity() || transaction.isReAmortize() || !transaction.isNonMonetaryTransaction()); } - private List retrieveListOfTransactionsExcludeAccruals() { + public List retrieveListOfTransactionsExcludeAccruals() { final List repaymentsOrWaivers = new ArrayList<>(); for (final LoanTransaction transaction : this.loanTransactions) { if (transaction.isNotReversed() && !transaction.isNonMonetaryTransaction()) { @@ -2566,14 +1664,13 @@ public boolean isChronologicallyLatestRepaymentOrWaiver(final LoanTransaction lo return isChronologicallyLatestRepaymentOrWaiver; } - private boolean isAfterLastRepayment(final LoanTransaction loanTransaction, final List loanTransactions) { + public boolean isAfterLastRepayment(final LoanTransaction loanTransaction, final List loanTransactions) { return loanTransactions.stream() // .filter(t -> t.isRepaymentLikeType() && t.isNotReversed()) // .noneMatch(t -> DateUtils.isBefore(loanTransaction.getTransactionDate(), t.getTransactionDate())); } - private boolean isChronologicallyLatestTransaction(final LoanTransaction loanTransaction, - final List loanTransactions) { + public boolean isChronologicallyLatestTransaction(final LoanTransaction loanTransaction, final List loanTransactions) { return loanTransactions.stream() // .filter(LoanTransaction::isNotReversed) // .allMatch(t -> DateUtils.isAfter(loanTransaction.getTransactionDate(), t.getTransactionDate())); @@ -2663,18 +1760,6 @@ public LoanTransaction deriveDefaultInterestWaiverTransaction() { possibleInterestToWaive.zero(), ExternalId.empty()); } - public ChangedTransactionDetail undoWrittenOff(LoanLifecycleStateMachine loanLifecycleStateMachine, - final List existingTransactionIds, final List existingReversedTransactionIds, - final ScheduleGeneratorDTO scheduleGeneratorDTO) { - validateAccountStatus(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO); - existingTransactionIds.addAll(findExistingTransactionIds()); - existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); - final LoanTransaction writeOffTransaction = findWriteOffTransaction(); - writeOffTransaction.reverse(); - loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO, this); - return reprocessTransactions(); - } - public LoanTransaction findWriteOffTransaction() { return this.loanTransactions.stream() // .filter(transaction -> !transaction.isReversed() && transaction.isWriteOff()) // @@ -2686,7 +1771,7 @@ public boolean isOverPaid() { return calculateTotalOverpayment().isGreaterThanZero(); } - private Money calculateTotalOverpayment() { + public Money calculateTotalOverpayment() { Money totalPaidInRepayments = getTotalPaidInRepayments(); final MonetaryCurrency currency = getCurrency(); @@ -2730,92 +1815,8 @@ public Money calculateTotalRecoveredPayments() { return getTotalRecoveredPayments(); } - public ChangedTransactionDetail closeAsWrittenOff(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine, - final Map changes, final List existingTransactionIds, final List existingReversedTransactionIds, - final AppUser currentUser, final ScheduleGeneratorDTO scheduleGeneratorDTO) { - - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(); - ChangedTransactionDetail changedTransactionDetail = closeDisbursements(scheduleGeneratorDTO, - loanRepaymentScheduleTransactionProcessor); - - validateAccountStatus(LoanEvent.WRITE_OFF_OUTSTANDING); - - final LocalDate writtenOffOnLocalDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE); - this.closedOnDate = writtenOffOnLocalDate; - this.writtenOffOnDate = writtenOffOnLocalDate; - this.closedBy = currentUser; - final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.WRITE_OFF_OUTSTANDING, this); - - LoanTransaction loanTransaction = null; - if (!statusEnum.hasStateOf(getStatus())) { - loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING, this); - changes.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus)); - - existingTransactionIds.addAll(findExistingTransactionIds()); - existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); - - final String txnExternalId = command.stringValueOfParameterNamedAllowingNull(EXTERNAL_ID); - - ExternalId externalId = ExternalIdFactory.produce(txnExternalId); - - if (externalId.isEmpty() && TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { - externalId = ExternalId.generate(); - } - - changes.put(CLOSED_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE)); - changes.put(WRITTEN_OFF_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE)); - changes.put("externalId", externalId); - - if (DateUtils.isBefore(writtenOffOnLocalDate, getDisbursementDate())) { - final String errorMessage = "The date on which a loan is written off cannot be before the loan disbursement date: " - + getDisbursementDate().toString(); - throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.before.submittal.date", errorMessage, - writtenOffOnLocalDate, getDisbursementDate()); - } - - validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.WRITE_OFF_OUTSTANDING, writtenOffOnLocalDate); - - if (DateUtils.isDateInTheFuture(writtenOffOnLocalDate)) { - final String errorMessage = "The date on which a loan is written off cannot be in the future."; - throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.a.future.date", errorMessage, writtenOffOnLocalDate); - } - - loanTransaction = LoanTransaction.writeoff(this, getOffice(), writtenOffOnLocalDate, externalId); - LocalDate lastTransactionDate = getLastUserTransactionDate(); - if (DateUtils.isAfter(lastTransactionDate, writtenOffOnLocalDate)) { - final String errorMessage = "The date of the writeoff transaction must occur on or before previous transactions."; - throw new InvalidLoanStateTransitionException("writeoff", "must.occur.on.or.after.other.transaction.dates", errorMessage, - writtenOffOnLocalDate); - } - - addLoanTransaction(loanTransaction); - loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(), - getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null)); - - updateLoanSummaryDerivedFields(); - } - if (changedTransactionDetail == null) { - changedTransactionDetail = new ChangedTransactionDetail(); - } - changedTransactionDetail.getNewTransactionMappings().put(0L, loanTransaction); - return changedTransactionDetail; - } - - private ChangedTransactionDetail closeDisbursements(final ScheduleGeneratorDTO scheduleGeneratorDTO, - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor) { - ChangedTransactionDetail changedTransactionDetail = null; - if (isDisbursementAllowed() && atLeastOnceDisbursed()) { - this.loanRepaymentScheduleDetail.setPrincipal(getDisbursedAmount()); - removeDisbursementDetail(); - regenerateRepaymentSchedule(scheduleGeneratorDTO); - if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { - regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); - } - changedTransactionDetail = reprocessTransactions(); - LocalDate lastLoanTransactionDate = getLatestTransactionDate(); - doPostLoanTransactionChecks(lastLoanTransactionDate, loanLifecycleStateMachine); - } - return changedTransactionDetail; + public MonetaryCurrency loanCurrency() { + return this.loanRepaymentScheduleDetail.getCurrency(); } public LocalDate getLatestTransactionDate() { @@ -2826,131 +1827,6 @@ public LocalDate getLatestTransactionDate() { return oneOfTheLatestTxn != null ? oneOfTheLatestTxn.getTransactionDate() : null; } - public ChangedTransactionDetail close(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine, - final Map changes, final List existingTransactionIds, final List existingReversedTransactionIds, - final ScheduleGeneratorDTO scheduleGeneratorDTO) { - - validateAccountStatus(LoanEvent.LOAN_CLOSED); - - existingTransactionIds.addAll(findExistingTransactionIds()); - existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); - - final LocalDate closureDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE); - final String txnExternalId = command.stringValueOfParameterNamedAllowingNull(EXTERNAL_ID); - - ExternalId externalId = ExternalIdFactory.produce(txnExternalId); - if (externalId.isEmpty() && TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { - externalId = ExternalId.generate(); - } - - this.closedOnDate = closureDate; - changes.put(CLOSED_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE)); - - validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.REPAID_IN_FULL, closureDate); - if (DateUtils.isBefore(closureDate, getDisbursementDate())) { - final String errorMessage = "The date on which a loan is closed cannot be before the loan disbursement date: " - + getDisbursementDate().toString(); - throw new InvalidLoanStateTransitionException("close", "cannot.be.before.submittal.date", errorMessage, closureDate, - getDisbursementDate()); - } - - if (DateUtils.isDateInTheFuture(closureDate)) { - final String errorMessage = "The date on which a loan is closed cannot be in the future."; - throw new InvalidLoanStateTransitionException("close", "cannot.be.a.future.date", errorMessage, closureDate); - } - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(); - ChangedTransactionDetail changedTransactionDetail = closeDisbursements(scheduleGeneratorDTO, - loanRepaymentScheduleTransactionProcessor); - - LoanTransaction loanTransaction = null; - if (isOpen()) { - final Money totalOutstanding = this.summary.getTotalOutstanding(getCurrency()); - if (totalOutstanding.isGreaterThanZero() && getInArrearsTolerance().isGreaterThanOrEqualTo(totalOutstanding)) { - - this.closedOnDate = closureDate; - final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.REPAID_IN_FULL, this); - if (!statusEnum.hasStateOf(getStatus())) { - loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this); - changes.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus)); - } - changes.put("externalId", externalId); - loanTransaction = LoanTransaction.writeoff(this, getOffice(), closureDate, externalId); - final boolean isLastTransaction = isChronologicallyLatestTransaction(loanTransaction, getLoanTransactions()); - if (!isLastTransaction) { - final String errorMessage = "The closing date of the loan must be on or after latest transaction date."; - throw new InvalidLoanStateTransitionException("close.loan", "must.occur.on.or.after.latest.transaction.date", - errorMessage, closureDate); - } - - addLoanTransaction(loanTransaction); - loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(), - getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null)); - - updateLoanSummaryDerivedFields(); - } else if (totalOutstanding.isGreaterThanZero()) { - final String errorMessage = "A loan with money outstanding cannot be closed"; - throw new InvalidLoanStateTransitionException("close", "loan.has.money.outstanding", errorMessage, - totalOutstanding.toString()); - } - } - - if (isOverPaid()) { - final Money totalLoanOverpayment = calculateTotalOverpayment(); - if (totalLoanOverpayment.isGreaterThanZero() && getInArrearsTolerance().isGreaterThanOrEqualTo(totalLoanOverpayment)) { - // TODO - KW - technically should set somewhere that this loan - // has 'overpaid' amount - this.closedOnDate = closureDate; - final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.REPAID_IN_FULL, this); - if (!statusEnum.hasStateOf(getStatus())) { - loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, this); - changes.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus)); - } - } else if (totalLoanOverpayment.isGreaterThanZero()) { - final String errorMessage = "The loan is marked as 'Overpaid' and cannot be moved to 'Closed (obligations met)."; - throw new InvalidLoanStateTransitionException("close", "loan.is.overpaid", errorMessage, totalLoanOverpayment.toString()); - } - } - - if (changedTransactionDetail == null) { - changedTransactionDetail = new ChangedTransactionDetail(); - } - changedTransactionDetail.getNewTransactionMappings().put(0L, loanTransaction); - return changedTransactionDetail; - } - - /** - * Behaviour added to comply with capability of previous mifos product to support easier transition to fineract - * platform. - */ - public void closeAsMarkedForReschedule(final JsonCommand command, final LoanLifecycleStateMachine loanLifecycleStateMachine, - final Map changes) { - final LocalDate rescheduledOn = command.localDateValueOfParameterNamed(TRANSACTION_DATE); - - this.closedOnDate = rescheduledOn; - final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_RESCHEDULE, this); - if (!statusEnum.hasStateOf(getStatus())) { - loanLifecycleStateMachine.transition(LoanEvent.LOAN_RESCHEDULE, this); - changes.put(PARAM_STATUS, LoanEnumerations.status(this.loanStatus)); - } - - this.rescheduledOnDate = rescheduledOn; - changes.put(CLOSED_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE)); - changes.put("rescheduledOnDate", command.stringValueOfParameterNamed(TRANSACTION_DATE)); - - if (DateUtils.isBefore(this.rescheduledOnDate, getDisbursementDate())) { - final String errorMessage = "The date on which a loan is rescheduled cannot be before the loan disbursement date: " - + getDisbursementDate().toString(); - throw new InvalidLoanStateTransitionException("close.reschedule", "cannot.be.before.submittal.date", errorMessage, - this.rescheduledOnDate, getDisbursementDate()); - } - - if (DateUtils.isDateInTheFuture(this.rescheduledOnDate)) { - final String errorMessage = "The date on which a loan is rescheduled cannot be in the future."; - throw new InvalidLoanStateTransitionException("close.reschedule", "cannot.be.a.future.date", errorMessage, - this.rescheduledOnDate); - } - } - public boolean isNotSubmittedAndPendingApproval() { return !isSubmittedAndPendingApproval(); } @@ -3082,7 +1958,7 @@ public boolean isActualDisbursedOnDateEarlierOrLaterThanExpected(final LocalDate return isRegenerationRequired || !DateUtils.isEqual(actualDisbursedOnDate, this.expectedDisbursementDate); } - private Money getTotalPaidInRepayments() { + public Money getTotalPaidInRepayments() { Money cumulativePaid = Money.zero(getCurrency()); for (final LoanTransaction repayment : this.loanTransactions) { @@ -3112,7 +1988,7 @@ public Money getTotalPrincipalOutstandingUntil(LocalDate date) { } - private Money getTotalInterestOutstandingOnLoan() { + public Money getTotalInterestOutstandingOnLoan() { Money cumulativeInterest = Money.zero(getCurrency()); List installments = getRepaymentScheduleInstallments(); @@ -3138,7 +2014,7 @@ private Money getTotalInterestOverdueOnLoan() { return cumulativeInterestOverdue; } - private Money getInArrearsTolerance() { + public Money getInArrearsTolerance() { return this.loanRepaymentScheduleDetail.getInArrearsTolerance(); } @@ -3170,82 +2046,18 @@ public MonetaryCurrency getCurrency() { return this.loanRepaymentScheduleDetail.getCurrency(); } - public void reassignLoanOfficer(final Staff newLoanOfficer, final LocalDate assignmentDate) { - final LoanOfficerAssignmentHistory latestHistoryRecord = findLatestIncompleteHistoryRecord(); - final LoanOfficerAssignmentHistory lastAssignmentRecord = findLastAssignmentHistoryRecord(newLoanOfficer); - - // assignment date should not be less than loan submitted date - if (isSubmittedOnDateAfter(assignmentDate)) { - final String errorMessage = "The Loan Officer assignment date (" + assignmentDate.toString() - + ") cannot be before loan submitted date (" + getSubmittedOnDate().toString() + ")."; - throw new LoanOfficerAssignmentDateException("cannot.be.before.loan.submittal.date", errorMessage, assignmentDate, - getSubmittedOnDate()); - } else if (lastAssignmentRecord != null && lastAssignmentRecord.isEndDateAfter(assignmentDate)) { - final String errorMessage = "The Loan Officer assignment date (" + assignmentDate - + ") cannot be before previous Loan Officer unassigned date (" + lastAssignmentRecord.getEndDate() + ")."; - throw new LoanOfficerAssignmentDateException("cannot.be.before.previous.unassignement.date", errorMessage, assignmentDate, - lastAssignmentRecord.getEndDate()); - } else if (DateUtils.isDateInTheFuture(assignmentDate)) { - final String errorMessage = "The Loan Officer assignment date (" + assignmentDate + ") cannot be in the future."; - throw new LoanOfficerAssignmentDateException("cannot.be.a.future.date", errorMessage, assignmentDate); - } else if (latestHistoryRecord != null && this.loanOfficer.identifiedBy(newLoanOfficer)) { - latestHistoryRecord.updateStartDate(assignmentDate); - } else if (latestHistoryRecord != null && latestHistoryRecord.matchesStartDateOf(assignmentDate)) { - latestHistoryRecord.updateLoanOfficer(newLoanOfficer); - this.loanOfficer = newLoanOfficer; - } else if (latestHistoryRecord != null && latestHistoryRecord.isBeforeStartDate(assignmentDate)) { - final String errorMessage = "Loan with identifier " + getId() + " was already assigned before date " + assignmentDate; - throw new LoanOfficerAssignmentDateException("is.before.last.assignment.date", errorMessage, getId(), assignmentDate); - } else { - if (latestHistoryRecord != null) { - // loan officer correctly changed from previous loan officer to new loan officer - latestHistoryRecord.updateEndDate(assignmentDate); - } - - this.loanOfficer = newLoanOfficer; - if (isNotSubmittedAndPendingApproval()) { - final LoanOfficerAssignmentHistory loanOfficerAssignmentHistory = LoanOfficerAssignmentHistory.createNew(this, - this.loanOfficer, assignmentDate); - this.loanOfficerHistory.add(loanOfficerAssignmentHistory); - } - } - } - public void removeLoanOfficer(final LocalDate unassignDate) { - final LoanOfficerAssignmentHistory latestHistoryRecord = findLatestIncompleteHistoryRecord(); - - if (latestHistoryRecord != null) { - validateUnassignDate(latestHistoryRecord, unassignDate); - latestHistoryRecord.updateEndDate(unassignDate); - } + findLatestIncompleteHistoryRecord() + .ifPresent(loanOfficerAssignmentHistory -> loanOfficerAssignmentHistory.updateEndDate(unassignDate)); this.loanOfficer = null; } - - private void validateUnassignDate(final LoanOfficerAssignmentHistory latestHistoryRecord, final LocalDate unassignDate) { - if (DateUtils.isAfter(latestHistoryRecord.getStartDate(), unassignDate)) { - final String errorMessage = "The Loan officer Unassign date(" + unassignDate + ") cannot be before its assignment date (" - + latestHistoryRecord.getStartDate() + ")."; - throw new LoanOfficerUnassignmentDateException("cannot.be.before.assignment.date", errorMessage, getId(), - getLoanOfficer().getId(), latestHistoryRecord.getStartDate(), unassignDate); - } else if (DateUtils.isDateInTheFuture(unassignDate)) { - final String errorMessage = "The Loan Officer Unassign date (" + unassignDate + ") cannot be in the future."; - throw new LoanOfficerUnassignmentDateException("cannot.be.a.future.date", errorMessage, unassignDate); - } - } - - private LoanOfficerAssignmentHistory findLatestIncompleteHistoryRecord() { - LoanOfficerAssignmentHistory latestRecordWithNoEndDate = null; - for (final LoanOfficerAssignmentHistory historyRecord : this.loanOfficerHistory) { - if (historyRecord.isCurrentRecord()) { - latestRecordWithNoEndDate = historyRecord; - break; - } - } - return latestRecordWithNoEndDate; + + public Optional findLatestIncompleteHistoryRecord() { + return this.loanOfficerHistory.stream().filter(LoanOfficerAssignmentHistory::isCurrentRecord).findFirst(); } - private LoanOfficerAssignmentHistory findLastAssignmentHistoryRecord(final Staff newLoanOfficer) { + public LoanOfficerAssignmentHistory findLastAssignmentHistoryRecord(final Staff newLoanOfficer) { LoanOfficerAssignmentHistory lastAssignmentRecordLatestEndDate = null; for (final LoanOfficerAssignmentHistory historyRecord : this.loanOfficerHistory) { if (historyRecord.isCurrentRecord() && !historyRecord.isSameLoanOfficer(newLoanOfficer)) { @@ -3618,22 +2430,6 @@ private LocalDate getMaxDateLimitForNewRepayment(final PeriodFrequencyType perio return dueRepaymentPeriodDate.minusDays(1);// get 2n-1 range date from startDate } - public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays, - final boolean allowTransactionsOnNonWorkingDay) { - if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) { - final String errorMessage = "Repayment date cannot be on a non working day"; - throw new LoanApplicationDateException("repayment.date.on.non.working.day", errorMessage, repaymentDate); - } - } - - public void validateRepaymentDateIsOnHoliday(final LocalDate repaymentDate, final boolean allowTransactionsOnHoliday, - final List holidays) { - if (!allowTransactionsOnHoliday && HolidayUtil.isHoliday(repaymentDate, holidays)) { - final String errorMessage = "Repayment date cannot be on a holiday"; - throw new LoanApplicationDateException("repayment.date.on.holiday", errorMessage, repaymentDate); - } - } - public Group group() { return this.group; } @@ -3673,131 +2469,10 @@ public void updateInterestRateFrequencyType() { this.loanRepaymentScheduleDetail.setInterestPeriodFrequencyType(this.loanProduct.getInterestPeriodFrequencyType()); } - public void updateInterestRateFrequencyType(PeriodFrequencyType periodFrequencyType) { - this.loanRepaymentScheduleDetail.setInterestPeriodFrequencyType(periodFrequencyType); - } - public void addLoanTransaction(final LoanTransaction loanTransaction) { this.loanTransactions.add(loanTransaction); } - public void removeLoanTransaction(final LoanTransaction loanTransaction) { - this.loanTransactions.remove(loanTransaction); - } - - public void validateActivityNotBeforeClientOrGroupTransferDate(final LoanEvent event, final LocalDate activityDate) { - if (this.client != null && this.client.getOfficeJoiningDate() != null) { - final LocalDate clientOfficeJoiningDate = this.client.getOfficeJoiningDate(); - if (DateUtils.isBefore(activityDate, clientOfficeJoiningDate)) { - String errorMessage = null; - String action = null; - String postfix = null; - switch (event) { - case LOAN_APPROVED -> { - errorMessage = "The date on which a loan is approved cannot be earlier than client's transfer date to this office"; - action = "approval"; - postfix = "cannot.be.before.client.transfer.date"; - } - case LOAN_APPROVAL_UNDO -> { - errorMessage = "The date on which a loan is approved cannot be earlier than client's transfer date to this office"; - action = "approval"; - postfix = "cannot.be.undone.before.client.transfer.date"; - } - case LOAN_DISBURSED -> { - errorMessage = "The date on which a loan is disbursed cannot be earlier than client's transfer date to this office"; - action = "disbursal"; - postfix = "cannot.be.before.client.transfer.date"; - } - case LOAN_DISBURSAL_UNDO -> { - errorMessage = "Cannot undo a disbursal done in another branch"; - action = "disbursal"; - postfix = "cannot.be.undone.before.client.transfer.date"; - } - case LOAN_REPAYMENT_OR_WAIVER -> { - errorMessage = "The date on which a repayment or waiver is made cannot be earlier than client's transfer date to this office"; - action = "repayment.or.waiver"; - postfix = "cannot.be.made.before.client.transfer.date"; - } - case WRITE_OFF_OUTSTANDING -> { - errorMessage = "The date on which a write off is made cannot be earlier than client's transfer date to this office"; - action = "writeoff"; - postfix = "cannot.be.undone.before.client.transfer.date"; - } - case REPAID_IN_FULL -> { - errorMessage = "The date on which the loan is repaid in full cannot be earlier than client's transfer date to this office"; - action = "close"; - postfix = "cannot.be.undone.before.client.transfer.date"; - } - case LOAN_CHARGE_PAYMENT -> { - errorMessage = "The date on which a charge payment is made cannot be earlier than client's transfer date to this office"; - action = "charge.payment"; - postfix = "cannot.be.made.before.client.transfer.date"; - } - case LOAN_REFUND -> { - errorMessage = "The date on which a refund is made cannot be earlier than client's transfer date to this office"; - action = "refund"; - postfix = "cannot.be.made.before.client.transfer.date"; - } - case LOAN_DISBURSAL_UNDO_LAST -> { - errorMessage = "Cannot undo a last disbursal in another branch"; - action = "disbursal"; - postfix = "cannot.be.undone.before.client.transfer.date"; - } - default -> { - } - } - throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, clientOfficeJoiningDate); - } - } - } - - private void validateActivityNotBeforeLastTransactionDate(final LoanEvent event, final LocalDate activityDate) { - if (!(this.repaymentScheduleDetail().isInterestRecalculationEnabled() || this.loanProduct().isHoldGuaranteeFunds()) - || !this.getLoanRepaymentScheduleDetail().getLoanScheduleType().equals(LoanScheduleType.CUMULATIVE)) { - return; - } - LocalDate lastTransactionDate = getLastUserTransactionDate(); - if (DateUtils.isAfter(lastTransactionDate, activityDate)) { - String errorMessage = null; - String action = null; - String postfix = null; - switch (event) { - case LOAN_REPAYMENT_OR_WAIVER -> { - errorMessage = "The date on which a repayment or waiver is made cannot be earlier than last transaction date"; - action = "repayment.or.waiver"; - postfix = "cannot.be.made.before.last.transaction.date"; - } - case WRITE_OFF_OUTSTANDING -> { - errorMessage = "The date on which a write off is made cannot be earlier than last transaction date"; - action = "writeoff"; - postfix = "cannot.be.made.before.last.transaction.date"; - } - case LOAN_CHARGE_PAYMENT -> { - errorMessage = "The date on which a charge payment is made cannot be earlier than last transaction date"; - action = "charge.payment"; - postfix = "cannot.be.made.before.last.transaction.date"; - } - default -> { - } - } - throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, lastTransactionDate); - } - } - - public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final LoanTransaction repaymentTransaction, - final String reversedOrCreated) { - if (repaymentTransaction.isRepaymentLikeType() && !repaymentTransaction.isChargeRefund()) { - for (LoanTransaction txn : this.getLoanTransactions()) { - if (txn.isChargeRefund() && DateUtils.isBefore(repaymentTransaction.getTransactionDate(), txn.getTransactionDate())) { - final String errorMessage = "loan.transaction.cant.be." + reversedOrCreated + ".because.later.charge.refund.exists"; - final String details = "Loan Transaction: " + this.getId() + " Can't be " + reversedOrCreated - + " because a Later Charge Refund Exists."; - throw new LoanChargeRefundException(errorMessage, details); - } - } - } - } - public LocalDate getLastUserTransactionDate() { LocalDate currentTransactionDate = getDisbursementDate(); for (final LoanTransaction previousTransaction : this.loanTransactions) { @@ -3870,155 +2545,6 @@ public List generateInstallmentLoanCharges(final LoanChar return loanChargePerInstallments; } - public void validateAccountStatus(final LoanEvent event) { - final List dataValidationErrors = new ArrayList<>(); - - switch (event) { - case LOAN_APPROVED -> { - if (!isSubmittedAndPendingApproval()) { - final String defaultUserMessage = "Loan Account Approval is not allowed. Loan Account is not in submitted and pending approval state."; - final ApiParameterError error = ApiParameterError - .generalError("error.msg.loan.approve.account.is.not.submitted.and.pending.state", defaultUserMessage); - dataValidationErrors.add(error); - } - } - case LOAN_APPROVAL_UNDO -> { - if (!isApproved()) { - final String defaultUserMessage = "Loan Account Undo Approval is not allowed. Loan Account is not in approved state."; - final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.undo.approval.account.is.not.approved", - defaultUserMessage); - dataValidationErrors.add(error); - } - } - case LOAN_DISBURSED -> { - if ((!(isApproved() && isNotDisbursed()) && !this.loanProduct.isMultiDisburseLoan()) - || (this.loanProduct.isMultiDisburseLoan() && !isAllTranchesNotDisbursed())) { - 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); - dataValidationErrors.add(error); - } - } - case LOAN_DISBURSAL_UNDO -> { - if (!isOpen()) { - final String defaultUserMessage = "Loan Undo disbursal is not allowed. Loan Account is not active."; - final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.undo.disbursal.account.is.not.active", - defaultUserMessage); - dataValidationErrors.add(error); - } - if (isOpen() && this.isTopup()) { - final String defaultUserMessage = "Loan Undo disbursal is not allowed on Topup Loans"; - final ApiParameterError error = ApiParameterError - .generalError("error.msg.loan.undo.disbursal.not.allowed.on.topup.loan", defaultUserMessage); - dataValidationErrors.add(error); - } - } - case LOAN_REPAYMENT_OR_WAIVER -> { - if (!isOpen()) { - final String defaultUserMessage = "Loan Repayment (or its types) or Waiver is not allowed. Loan Account is not active."; - final ApiParameterError error = ApiParameterError - .generalError("error.msg.loan.repayment.or.waiver.account.is.not.active", defaultUserMessage); - dataValidationErrors.add(error); - } - } - case WRITE_OFF_OUTSTANDING -> { - if (!isOpen()) { - final String defaultUserMessage = "Loan Written off is not allowed. Loan Account is not active."; - final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.writtenoff.account.is.not.active", - defaultUserMessage); - dataValidationErrors.add(error); - } - } - case WRITE_OFF_OUTSTANDING_UNDO -> { - if (!isClosedWrittenOff()) { - final String defaultUserMessage = "Loan Undo Written off is not allowed. Loan Account is not Written off."; - final ApiParameterError error = ApiParameterError - .generalError("error.msg.loan.undo.writtenoff.account.is.not.written.off", defaultUserMessage); - dataValidationErrors.add(error); - } - } - case LOAN_CHARGE_PAYMENT -> { - if (!isOpen()) { - final String defaultUserMessage = "Charge payment is not allowed. Loan Account is not Active."; - final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.charge.payment.account.is.not.active", - defaultUserMessage); - dataValidationErrors.add(error); - } - } - case LOAN_CLOSED -> { - if (!isOpen()) { - final String defaultUserMessage = "Closing Loan Account is not allowed. Loan Account is not Active."; - final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.close.account.is.not.active", - defaultUserMessage); - dataValidationErrors.add(error); - } - } - case LOAN_EDIT_MULTI_DISBURSE_DATE -> { - if (isClosed()) { - final String defaultUserMessage = "Edit disbursement is not allowed. Loan Account is not active."; - final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.edit.disbursement.account.is.not.active", - defaultUserMessage); - dataValidationErrors.add(error); - } - } - case LOAN_RECOVERY_PAYMENT -> { - if (!isClosedWrittenOff()) { - final String defaultUserMessage = "Recovery repayments may only be made on loans which are written off"; - final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.account.is.not.written.off", - defaultUserMessage); - dataValidationErrors.add(error); - } - } - case LOAN_REFUND -> { - if (!isOpen()) { - final String defaultUserMessage = "Loan Refund is not allowed. Loan Account is not active."; - final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.refund.account.is.not.active", - defaultUserMessage); - dataValidationErrors.add(error); - } - } - case LOAN_DISBURSAL_UNDO_LAST -> { - if (!isOpen()) { - final String defaultUserMessage = "Loan Undo last disbursal is not allowed. Loan Account is not active."; - final ApiParameterError error = ApiParameterError - .generalError("error.msg.loan.undo.last.disbursal.account.is.not.active", defaultUserMessage); - dataValidationErrors.add(error); - } - } - case LOAN_FORECLOSURE -> { - if (!isOpen()) { - final String defaultUserMessage = "Loan foreclosure is not allowed. Loan Account is not active."; - final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.foreclosure.account.is.not.active", - defaultUserMessage); - dataValidationErrors.add(error); - } - } - case LOAN_CREDIT_BALANCE_REFUND -> { - if (!getStatus().isOverpaid()) { - final String defaultUserMessage = "Loan Credit Balance Refund is not allowed. Loan Account is not Overpaid."; - final ApiParameterError error = ApiParameterError - .generalError("error.msg.loan.credit.balance.refund.account.is.not.overpaid", defaultUserMessage); - dataValidationErrors.add(error); - } - } - case LOAN_CHARGE_ADJUSTMENT -> { - if (!(getStatus().isActive() || getStatus().isClosedObligationsMet() || getStatus().isOverpaid())) { - final String defaultUserMessage = "Loan Charge Adjustment is not allowed. Loan Account must be either Active, Fully repaid or Overpaid."; - final ApiParameterError error = ApiParameterError - .generalError("error.msg.loan.charge.adjustment.account.is.not.in.valid.state", defaultUserMessage); - dataValidationErrors.add(error); - } - } - default -> { - } - } - - if (!dataValidationErrors.isEmpty()) { - throw new PlatformApiDataValidationException(dataValidationErrors); - } - - } - public LoanCharge fetchLoanChargesById(Long id) { LoanCharge charge = null; for (LoanCharge loanCharge : this.charges) { @@ -4066,33 +2592,6 @@ public LoanDisbursementDetails getDisbursementDetails(final LocalDate transactio return null; } - public ChangedTransactionDetail updateDisbursementDateAndAmountForTranche(final LoanDisbursementDetails disbursementDetails, - final JsonCommand command, final Map actualChanges, final ScheduleGeneratorDTO scheduleGeneratorDTO) { - final Locale locale = command.extractLocale(); - validateAccountStatus(LoanEvent.LOAN_EDIT_MULTI_DISBURSE_DATE); - final BigDecimal principal = command.bigDecimalValueOfParameterNamed(LoanApiConstants.updatedDisbursementPrincipalParameterName, - locale); - final LocalDate expectedDisbursementDate = command - .localDateValueOfParameterNamed(LoanApiConstants.updatedDisbursementDateParameterName); - disbursementDetails.updateExpectedDisbursementDateAndAmount(expectedDisbursementDate, principal); - actualChanges.put(LoanApiConstants.expectedDisbursementDateParameterName, - command.stringValueOfParameterNamed(LoanApiConstants.expectedDisbursementDateParameterName)); - actualChanges.put(LoanApiConstants.disbursementIdParameterName, - command.stringValueOfParameterNamed(LoanApiConstants.disbursementIdParameterName)); - actualChanges.put(LoanApiConstants.disbursementPrincipalParameterName, - command.bigDecimalValueOfParameterNamed(LoanApiConstants.disbursementPrincipalParameterName, locale)); - - this.loanRepaymentScheduleDetail.setPrincipal(getPrincipalAmountForRepaymentSchedule()); - - if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { - regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); - } else { - regenerateRepaymentSchedule(scheduleGeneratorDTO); - } - - return reprocessTransactions(); - } - public BigDecimal getPrincipalAmountForRepaymentSchedule() { BigDecimal principalAmount = BigDecimal.ZERO; @@ -4152,72 +2651,23 @@ public LocalDate getMaturityDate() { return this.actualMaturityDate; } - public ChangedTransactionDetail recalculateScheduleFromLastTransaction(final ScheduleGeneratorDTO generatorDTO, - final List existingTransactionIds, final List existingReversedTransactionIds) { - existingTransactionIds.addAll(findExistingTransactionIds()); - existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); + public ChangedTransactionDetail processTransactions() { + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(); + final List allNonContraTransactionsPostDisbursement = retrieveListOfTransactionsForReprocessing(); + ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( + getDisbursementDate(), allNonContraTransactionsPostDisbursement, getCurrency(), getRepaymentScheduleInstallments(), + getActiveCharges()); + for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + mapEntry.getValue().updateLoan(this); + } /* - * LocalDate recalculateFrom = null; List loanTransactions = - * this.retrieveListOfTransactionsPostDisbursementExcludeAccruals(); for (LoanTransaction loanTransaction : - * loanTransactions) { if (recalculateFrom == null || - * loanTransaction.getTransactionDate().isAfter(recalculateFrom)) { recalculateFrom = - * loanTransaction.getTransactionDate(); } } generatorDTO.setRecalculateFrom(recalculateFrom); + * Commented since throwing exception if external id present for one of the transactions. for this need to save + * the reversed transactions first and then new transactions. */ - if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { - regenerateRepaymentScheduleWithInterestRecalculation(generatorDTO); - } else { - regenerateRepaymentSchedule(generatorDTO); - } - return reprocessTransactions(); - - } - - public ChangedTransactionDetail recalculateScheduleFromLastTransaction(final ScheduleGeneratorDTO generatorDTO) { - if (this.repaymentScheduleDetail().isInterestRecalculationEnabled()) { - regenerateRepaymentScheduleWithInterestRecalculation(generatorDTO); - } else { - regenerateRepaymentSchedule(generatorDTO); - } - return reprocessTransactions(); - - } - - public ChangedTransactionDetail handleRegenerateRepaymentScheduleWithInterestRecalculation(final ScheduleGeneratorDTO generatorDTO) { - regenerateRepaymentScheduleWithInterestRecalculation(generatorDTO); - return reprocessTransactions(); - } - - public void regenerateRepaymentScheduleWithInterestRecalculation(final ScheduleGeneratorDTO generatorDTO) { - LocalDate lastTransactionDate = getLastUserTransactionDate(); - final LoanScheduleDTO loanSchedule = getRecalculatedSchedule(generatorDTO); - if (loanSchedule == null) { - return; - } - // Either the installments got recalculated or the model - if (loanSchedule.getInstallments() != null) { - updateLoanSchedule(loanSchedule.getInstallments()); - } else { - updateLoanSchedule(loanSchedule.getLoanScheduleModel()); - } - this.interestRecalculatedOn = DateUtils.getBusinessLocalDate(); - LocalDate lastRepaymentDate = this.getLastRepaymentPeriodDueDate(true); - Set charges = this.getActiveCharges(); - for (final LoanCharge loanCharge : charges) { - if (!loanCharge.isDueAtDisbursement()) { - updateOverdueScheduleInstallment(loanCharge); - if (loanCharge.getDueLocalDate() == null || !DateUtils.isBefore(lastRepaymentDate, loanCharge.getDueLocalDate())) { - if ((loanCharge.isInstalmentFee() || !loanCharge.isWaived()) && (loanCharge.getDueLocalDate() == null - || !DateUtils.isAfter(lastTransactionDate, loanCharge.getDueLocalDate()))) { - recalculateLoanCharge(loanCharge, generatorDTO.getPenaltyWaitPeriod()); - loanCharge.updateWaivedAmount(getCurrency()); - } - } else { - loanCharge.setActive(false); - } - } - } + this.loanTransactions.addAll(changedTransactionDetail.getNewTransactionMappings().values()); + updateLoanSummaryDerivedFields(); - processPostDisbursementTransactions(); + return changedTransactionDetail; } public void processPostDisbursementTransactions() { @@ -4235,7 +2685,7 @@ public void processPostDisbursementTransactions() { } } - private LoanScheduleDTO getRecalculatedSchedule(final ScheduleGeneratorDTO generatorDTO) { + public LoanScheduleDTO getRecalculatedSchedule(final ScheduleGeneratorDTO generatorDTO) { if (!this.repaymentScheduleDetail().isEnableDownPayment() && (!this.repaymentScheduleDetail().isInterestRecalculationEnabled() || isNpa || isChargedOff())) { return null; @@ -4656,147 +3106,6 @@ public BigDecimal getGuaranteeAmount() { return this.guaranteeAmountDerived == null ? BigDecimal.ZERO : this.guaranteeAmountDerived; } - public void creditBalanceRefund(LoanTransaction newCreditBalanceRefundTransaction, - LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, List existingTransactionIds, - List existingReversedTransactionIds) { - validateAccountStatus(LoanEvent.LOAN_CREDIT_BALANCE_REFUND); - - validateRefundDateIsAfterLastRepayment(newCreditBalanceRefundTransaction.getTransactionDate()); - - if (!newCreditBalanceRefundTransaction.isGreaterThanZeroAndLessThanOrEqualTo(this.totalOverpaid)) { - final String errorMessage = "Transaction Amount (" - + newCreditBalanceRefundTransaction.getAmount(getCurrency()).getAmount().toString() - + ") must be > zero and <= Overpaid amount (" + this.totalOverpaid.toString() + ")."; - final List dataValidationErrors = new ArrayList<>(); - final ApiParameterError error = ApiParameterError.parameterError( - "error.msg.transactionAmount.invalid.must.be.>zero.and<=overpaidamount", errorMessage, "transactionAmount", - newCreditBalanceRefundTransaction.getAmount(getCurrency())); - dataValidationErrors.add(error); - - throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", - dataValidationErrors); - } - - existingTransactionIds.addAll(findExistingTransactionIds()); - existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); - - this.loanTransactions.add(newCreditBalanceRefundTransaction); - - updateLoanSummaryDerivedFields(); - - if (MathUtil.isEmpty(totalOverpaid)) { - this.overpaidOnDate = null; - this.closedOnDate = newCreditBalanceRefundTransaction.getTransactionDate(); - defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_CREDIT_BALANCE_REFUND, this); - } - - } - - public ChangedTransactionDetail makeRefundForActiveLoan(final LoanTransaction loanTransaction, - final LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, - final List existingReversedTransactionIds, final boolean allowTransactionsOnHoliday, final List holidays, - final WorkingDays workingDays, final boolean allowTransactionsOnNonWorkingDay) { - validateAccountStatus(LoanEvent.LOAN_REFUND); - validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REFUND, loanTransaction.getTransactionDate()); - - validateRefundDateIsAfterLastRepayment(loanTransaction.getTransactionDate()); - - validateRepaymentDateIsOnHoliday(loanTransaction.getTransactionDate(), allowTransactionsOnHoliday, holidays); - validateRepaymentDateIsOnNonWorkingDay(loanTransaction.getTransactionDate(), workingDays, allowTransactionsOnNonWorkingDay); - - existingTransactionIds.addAll(findExistingTransactionIds()); - existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); - - return handleRefundTransaction(loanTransaction, loanLifecycleStateMachine, null); - - } - - private void validateRefundDateIsAfterLastRepayment(final LocalDate refundTransactionDate) { - final LocalDate possibleNextRefundDate = possibleNextRefundDate(); - - if (possibleNextRefundDate == null || DateUtils.isBefore(refundTransactionDate, possibleNextRefundDate)) { - throw new InvalidRefundDateException(refundTransactionDate.toString()); - } - } - - private ChangedTransactionDetail handleRefundTransaction(final LoanTransaction loanTransaction, - final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction) { - ChangedTransactionDetail changedTransactionDetail = null; - - loanLifecycleStateMachine.transition(LoanEvent.LOAN_REFUND, this); - - loanTransaction.updateLoan(this); - - if (getStatus().isOverpaid() || getStatus().isClosed()) { - final String errorMessage = "This refund option is only for active loans "; - throw new InvalidLoanStateTransitionException("transaction", "is.exceeding.overpaid.amount", errorMessage, this.totalOverpaid, - loanTransaction.getAmount(getCurrency()).getAmount()); - } else if (this.getTotalPaidInRepayments().isZero()) { - final String errorMessage = "Cannot refund when no payment has been made"; - throw new InvalidLoanStateTransitionException("transaction", "no.payment.yet.made.for.loan", errorMessage); - } - - if (loanTransaction.isNotZero()) { - addLoanTransaction(loanTransaction); - } - if (loanTransaction.isNotRefundForActiveLoan()) { - final String errorMessage = "A transaction of type refund was expected but not received."; - throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.refund.transaction", errorMessage); - } - - final LocalDate loanTransactionDate = extractTransactionDate(loanTransaction); - - if (DateUtils.isDateInTheFuture(loanTransactionDate)) { - final String errorMessage = "The transaction date cannot be in the future."; - throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, loanTransactionDate); - } - - if (this.loanProduct.isMultiDisburseLoan() && adjustedTransaction == null) { - BigDecimal totalDisbursed = getDisbursedAmount(); - BigDecimal totalPrincipalAdjusted = this.summary.getTotalPrincipalAdjustments(); - BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted); - if (totalPrincipalCredited.compareTo(this.summary.getTotalPrincipalRepaid()) < 0) { - final String errorMessage = "The transaction amount cannot exceed threshold."; - throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage); - } - } - - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(); - - // If it's a refund - if (adjustedTransaction == null) { - loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, new TransactionCtx(getCurrency(), - getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null)); - } else { - changedTransactionDetail = reprocessTransactions(); - } - - updateLoanSummaryDerivedFields(); - - doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine); - - return changedTransactionDetail; - } - - public void handleChargebackTransaction(final LoanTransaction chargebackTransaction, - final LoanLifecycleStateMachine loanLifecycleStateMachine) { - if (!chargebackTransaction.isChargeback()) { - final String errorMessage = "A transaction of type chargeback was expected but not received."; - throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.chargeback.transaction", errorMessage); - } - - final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = getTransactionProcessor(); - - addLoanTransaction(chargebackTransaction); - loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargebackTransaction, new TransactionCtx(getCurrency(), - getRepaymentScheduleInstallments(), getActiveCharges(), new MoneyHolder(getTotalOverpaidAsMoney()), null)); - - updateLoanSummaryDerivedFields(); - if (!doPostLoanTransactionChecks(chargebackTransaction.getTransactionDate(), loanLifecycleStateMachine)) { - loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGEBACK, this); - } - } - public LocalDate possibleNextRefundDate() { final LocalDate now = DateUtils.getBusinessLocalDate(); @@ -4811,7 +3120,7 @@ public LocalDate possibleNextRefundDate() { return lastTransactionDate == null ? now : lastTransactionDate; } - private LocalDate getActualDisbursementDate(final LoanCharge loanCharge) { + public LocalDate getActualDisbursementDate(final LoanCharge loanCharge) { LocalDate actualDisbursementDate = this.actualDisbursementDate; if (loanCharge.isDueAtDisbursement() && loanCharge.isActive()) { LoanTrancheDisbursementCharge trancheDisbursementCharge = loanCharge.getTrancheDisbursementCharge(); @@ -4833,81 +3142,7 @@ public void addTrancheLoanCharge(final Charge charge) { } } - public Map undoLastDisbursal(ScheduleGeneratorDTO scheduleGeneratorDTO, List existingTransactionIds, - List existingReversedTransactionIds, Loan loan) { - validateAccountStatus(LoanEvent.LOAN_DISBURSAL_UNDO_LAST); - validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_DISBURSAL_UNDO_LAST, getDisbursementDate()); - - final Map actualChanges = new LinkedHashMap<>(); - List loanTransactions = retrieveListOfTransactionsByType(LoanTransactionType.DISBURSEMENT); - loanTransactions.sort(Comparator.comparing(LoanTransaction::getId)); - final LoanTransaction lastDisbursalTransaction = loanTransactions.get(loanTransactions.size() - 1); - final LocalDate lastTransactionDate = lastDisbursalTransaction.getTransactionDate(); - - existingTransactionIds.addAll(findExistingTransactionIds()); - existingReversedTransactionIds.addAll(findExistingReversedTransactionIds()); - - loanTransactions = retrieveListOfTransactionsExcludeAccruals(); - Collections.reverse(loanTransactions); - for (final LoanTransaction previousTransaction : loanTransactions) { - if (DateUtils.isBefore(lastTransactionDate, previousTransaction.getTransactionDate()) - && (previousTransaction.isRepaymentLikeType() || previousTransaction.isWaiver() - || previousTransaction.isChargePayment())) { - throw new UndoLastTrancheDisbursementException(previousTransaction.getId()); - } - if (previousTransaction.getId().compareTo(lastDisbursalTransaction.getId()) < 0) { - break; - } - } - final LoanDisbursementDetails disbursementDetail = loan.getDisbursementDetails(lastTransactionDate, - lastDisbursalTransaction.getAmount()); - updateLoanToLastDisbursalState(disbursementDetail); - this.loanTermVariations.removeIf(loanTermVariations -> (loanTermVariations.getTermType().isDueDateVariation() - && DateUtils.isAfter(loanTermVariations.fetchDateValue(), lastTransactionDate)) - || (loanTermVariations.getTermType().isEMIAmountVariation() - && DateUtils.isEqual(loanTermVariations.getTermApplicableFrom(), lastTransactionDate)) - || DateUtils.isAfter(loanTermVariations.getTermApplicableFrom(), lastTransactionDate)); - reverseExistingTransactionsTillLastDisbursal(lastDisbursalTransaction); - loan.recalculateScheduleFromLastTransaction(scheduleGeneratorDTO); - actualChanges.put("undolastdisbursal", "true"); - actualChanges.put("disbursedAmount", this.getDisbursedAmount()); - updateLoanSummaryDerivedFields(); - - doPostLoanTransactionChecks(getLastUserTransactionDate(), loanLifecycleStateMachine); - - return actualChanges; - } - - /** - * Reverse only disbursement, accruals, and repayments at disbursal transactions - */ - public void reverseExistingTransactionsTillLastDisbursal(LoanTransaction lastDisbursalTransaction) { - for (final LoanTransaction transaction : this.loanTransactions) { - if (!DateUtils.isBefore(transaction.getTransactionDate(), lastDisbursalTransaction.getTransactionDate()) - && transaction.getId().compareTo(lastDisbursalTransaction.getId()) >= 0 - && transaction.isAllowTypeTransactionAtTheTimeOfLastUndo()) { - transaction.reverse(); - } - } - if (isAutoRepaymentForDownPaymentEnabled()) { - // identify down-payment amount for the transaction - BigDecimal disbursedAmountPercentageForDownPayment = this.loanRepaymentScheduleDetail - .getDisbursedAmountPercentageForDownPayment(); - Money downPaymentMoney = Money.of(getCurrency(), - MathUtil.percentageOf(lastDisbursalTransaction.getAmount(), disbursedAmountPercentageForDownPayment, 19)); - - // find the latest matching down-payment transaction based on date, amount and transaction type - Optional downPaymentTransaction = this.loanTransactions.stream() - .filter(tr -> tr.getTransactionDate().equals(lastDisbursalTransaction.getTransactionDate()) - && tr.getTypeOf().isDownPayment() && tr.getAmount().compareTo(downPaymentMoney.getAmount()) == 0) - .max(Comparator.comparing(LoanTransaction::getId)); - - // reverse the down-payment transaction - downPaymentTransaction.ifPresent(LoanTransaction::reverse); - } - } - - private void updateLoanToLastDisbursalState(LoanDisbursementDetails disbursementDetail) { + public void updateLoanToLastDisbursalState(LoanDisbursementDetails disbursementDetail) { for (final LoanCharge charge : getActiveCharges()) { if (charge.isOverdueInstallmentCharge()) { charge.setActive(false); @@ -5135,104 +3370,6 @@ private double calculateInterestForDays(int daysInPeriod, BigDecimal interest, i return interest.doubleValue() / daysInPeriod * days; } - public ChangedTransactionDetail handleForeClosureTransactions(final LoanTransaction repaymentTransaction, - final LoanLifecycleStateMachine loanLifecycleStateMachine, final ScheduleGeneratorDTO scheduleGeneratorDTO) { - LoanEvent event = LoanEvent.LOAN_FORECLOSURE; - validateAccountStatus(event); - validateForForeclosure(repaymentTransaction.getTransactionDate()); - this.loanSubStatus = LoanSubStatus.FORECLOSED.getValue(); - return handleRepaymentOrRecoveryOrWaiverTransaction(repaymentTransaction, loanLifecycleStateMachine, null, scheduleGeneratorDTO); - } - - public void validateForForeclosure(final LocalDate transactionDate) { - if (getLoanProductRelatedDetail().isInterestRecalculationEnabled()) { - final String defaultUserMessage = "The loan with interest recalculation enabled cannot be foreclosed."; - throw new LoanForeclosureException("loan.with.interest.recalculation.enabled.cannot.be.foreclosured", defaultUserMessage, - getId()); - } - - LocalDate lastUserTransactionDate = getLastUserTransactionDate(); - - if (DateUtils.isDateInTheFuture(transactionDate)) { - final String defaultUserMessage = "The transactionDate cannot be in the future."; - throw new LoanForeclosureException("loan.foreclosure.transaction.date.is.in.future", defaultUserMessage, transactionDate); - } - - if (DateUtils.isBefore(transactionDate, lastUserTransactionDate)) { - final String defaultUserMessage = "The transactionDate cannot be earlier than the last transaction date."; - throw new LoanForeclosureException("loan.foreclosure.transaction.date.cannot.before.the.last.transaction.date", - defaultUserMessage, transactionDate); - } - } - - public void updateInstallmentsPostDate(LocalDate transactionDate) { - List newInstallments = new ArrayList<>(this.repaymentScheduleInstallments); - final MonetaryCurrency currency = getCurrency(); - Money totalPrincipal = Money.zero(currency); - Money[] balances = retriveIncomeForOverlappingPeriod(transactionDate); - boolean isInterestComponent = true; - for (final LoanRepaymentScheduleInstallment installment : this.repaymentScheduleInstallments) { - if (!DateUtils.isAfter(transactionDate, installment.getDueDate())) { - totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency)); - newInstallments.remove(installment); - if (DateUtils.isEqual(transactionDate, installment.getDueDate())) { - isInterestComponent = false; - } - } - - } - - for (LoanDisbursementDetails loanDisbursementDetails : getDisbursementDetails()) { - if (loanDisbursementDetails.actualDisbursementDate() == null) { - totalPrincipal = Money.of(currency, totalPrincipal.getAmount().subtract(loanDisbursementDetails.principal())); - } - } - - LocalDate installmentStartDate = getDisbursementDate(); - - if (!newInstallments.isEmpty()) { - installmentStartDate = newInstallments.get(newInstallments.size() - 1).getDueDate(); - } - - int installmentNumber = newInstallments.size(); - - if (!isInterestComponent) { - installmentNumber++; - } - - LoanRepaymentScheduleInstallment newInstallment = new LoanRepaymentScheduleInstallment(null, newInstallments.size() + 1, - installmentStartDate, transactionDate, totalPrincipal.getAmount(), balances[0].getAmount(), balances[1].getAmount(), - balances[2].getAmount(), isInterestComponent, null); - newInstallment.updateInstallmentNumber(newInstallments.size() + 1); - newInstallments.add(newInstallment); - updateLoanScheduleOnForeclosure(newInstallments); - - Set charges = this.getActiveCharges(); - int penaltyWaitPeriod = 0; - for (LoanCharge loanCharge : charges) { - if (DateUtils.isAfter(loanCharge.getDueLocalDate(), transactionDate)) { - loanCharge.setActive(false); - } else if (loanCharge.getDueLocalDate() == null) { - recalculateLoanCharge(loanCharge, penaltyWaitPeriod); - loanCharge.updateWaivedAmount(currency); - } - } - - for (LoanTransaction loanTransaction : getLoanTransactions()) { - if (loanTransaction.isChargesWaiver()) { - for (LoanChargePaidBy chargePaidBy : loanTransaction.getLoanChargesPaid()) { - if ((chargePaidBy.getLoanCharge().isDueDateCharge() - && DateUtils.isBefore(transactionDate, chargePaidBy.getLoanCharge().getDueLocalDate())) - || (chargePaidBy.getLoanCharge().isInstalmentFee() && chargePaidBy.getInstallmentNumber() != null - && chargePaidBy.getInstallmentNumber() > installmentNumber)) { - loanTransaction.reverse(); - } - } - - } - } - } - public void updateLoanScheduleOnForeclosure(final Collection installments) { this.repaymentScheduleInstallments.clear(); for (final LoanRepaymentScheduleInstallment installment : installments) { diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java index 074693e521e..a9f7270bcf8 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java @@ -448,7 +448,6 @@ private LoanTransaction(final Loan loan, final Office office, final LoanTransact } public void reverse() { - this.loan.validateRepaymentTypeTransactionNotBeforeAChargeRefund(this, "reversed"); this.reversed = true; this.reversedOnDate = DateUtils.getBusinessLocalDate(); this.loanTransactionToRepaymentScheduleMappings.clear(); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java index 059f93e5815..3f1430c112e 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java @@ -50,6 +50,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.CreocoreLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.HeavensFamilyLoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.springframework.util.CollectionUtils; /** @@ -64,6 +65,7 @@ public abstract class AbstractLoanRepaymentScheduleTransactionProcessor implements LoanRepaymentScheduleTransactionProcessor { public final SingleLoanChargeRepaymentScheduleProcessingWrapper loanChargeProcessor = new SingleLoanChargeRepaymentScheduleProcessingWrapper(); + public final LoanChargeValidator loanChargeValidator = new LoanChargeValidator(); @Override public boolean accept(String s) { @@ -501,6 +503,7 @@ private List getMergedTransactionList(List tra protected void createNewTransaction(LoanTransaction loanTransaction, LoanTransaction newLoanTransaction, ChangedTransactionDetail changedTransactionDetail) { + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, "reversed"); loanTransaction.reverse(); loanTransaction.updateExternalId(null); newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations()); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeValidator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeValidator.java new file mode 100644 index 00000000000..34e2eb06037 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanChargeValidator.java @@ -0,0 +1,101 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.serialization; + +import java.time.LocalDate; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.charge.exception.LoanChargeCannotBeAddedException; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; +import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException; +import org.springframework.stereotype.Component; + +@Component +public final class LoanChargeValidator { + + public void validateLoanIsNotClosed(final Loan loan, final LoanCharge loanCharge) { + if (loan.isClosed()) { + final String defaultUserMessage = "This charge cannot be added as the loan is already closed."; + throw new LoanChargeCannotBeAddedException("loanCharge", "loan.is.closed", defaultUserMessage, loan.getId(), loanCharge.name()); + + } + } + + // NOTE: to remove this constraint requires that loan transactions + // that represent the waive of charges also be removed (or reversed)M + // if you want ability to remove loan charges that are waived. + public void validateLoanChargeIsNotWaived(final Loan loan, final LoanCharge loanCharge) { + if (loanCharge.isWaived()) { + final String defaultUserMessage = "This loan charge cannot be removed as the charge as already been waived."; + throw new LoanChargeCannotBeAddedException("loanCharge", "loanCharge.is.waived", defaultUserMessage, loan.getId(), + loanCharge.name()); + } + } + + public void validateChargeHasValidSpecifiedDateIfApplicable(final Loan loan, final LoanCharge loanCharge, + final LocalDate disbursementDate) { + if (loanCharge.isSpecifiedDueDate() && DateUtils.isBefore(loanCharge.getDueLocalDate(), disbursementDate)) { + final String defaultUserMessage = "This charge with specified due date cannot be added as the it is not in schedule range."; + throw new LoanChargeCannotBeAddedException("loanCharge", "specified.due.date.outside.range", defaultUserMessage, + loan.getDisbursementDate(), loanCharge.name()); + } + } + + public void validateChargeAdditionForDisbursedLoan(final Loan loan, final LoanCharge loanCharge) { + if (loan.isChargesAdditionAllowed() && loanCharge.isDueAtDisbursement()) { + // Note: added this constraint to restrict adding disbursement + // charges to a loan + // after it is disbursed + // if the loan charge payment type is 'Disbursement'. + // To undo this constraint would mean resolving how charges due are + // disbursement are handled at present. + // When a loan is disbursed and has charges due at disbursement, a + // transaction is created to auto record + // payment of the charges (user has no choice in saying they were or + // were not paid) - so its assumed they were paid. + final String defaultUserMessage = "This charge which is due at disbursement cannot be added as the loan is already disbursed."; + throw new LoanChargeCannotBeAddedException("loanCharge", "due.at.disbursement.and.loan.is.disbursed", defaultUserMessage, + loan.getId(), loanCharge.name()); + } + } + + public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final Loan loan, final LoanTransaction repaymentTransaction, + final String reversedOrCreated) { + if (repaymentTransaction.isRepaymentLikeType() && !repaymentTransaction.isChargeRefund()) { + for (LoanTransaction txn : loan.getLoanTransactions()) { + if (txn.isChargeRefund() && DateUtils.isBefore(repaymentTransaction.getTransactionDate(), txn.getTransactionDate())) { + final String errorMessage = "loan.transaction.cant.be." + reversedOrCreated + ".because.later.charge.refund.exists"; + final String details = "Loan Transaction: " + loan.getId() + " Can't be " + reversedOrCreated + + " because a Later Charge Refund Exists."; + throw new LoanChargeRefundException(errorMessage, details); + } + } + } + } + + public void validateChargePaymentNotInFuture(final LoanTransaction paymentTransaction) { + if (DateUtils.isDateInTheFuture(paymentTransaction.getTransactionDate())) { + final String errorMessage = "The date on which a loan charge paid cannot be in the future."; + throw new InvalidLoanStateTransitionException("charge.payment", "cannot.be.a.future.date", errorMessage, + paymentTransaction.getTransactionDate()); + } + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDownPaymentTransactionValidator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDownPaymentTransactionValidator.java new file mode 100644 index 00000000000..507741ee477 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDownPaymentTransactionValidator.java @@ -0,0 +1,224 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.serialization; + +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.organisation.holiday.domain.Holiday; +import org.apache.fineract.organisation.holiday.service.HolidayUtil; +import org.apache.fineract.organisation.workingdays.domain.WorkingDays; +import org.apache.fineract.organisation.workingdays.service.WorkingDaysUtil; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException; +import org.springframework.stereotype.Component; + +@Component +public final class LoanDownPaymentTransactionValidator { + + public void validateRepaymentDateIsOnNonWorkingDay(final LocalDate repaymentDate, final WorkingDays workingDays, + final boolean allowTransactionsOnNonWorkingDay) { + if (!allowTransactionsOnNonWorkingDay && !WorkingDaysUtil.isWorkingDay(workingDays, repaymentDate)) { + final String errorMessage = "Repayment date cannot be on a non working day"; + throw new LoanApplicationDateException("repayment.date.on.non.working.day", errorMessage, repaymentDate); + } + } + + public void validateRepaymentDateIsOnHoliday(final LocalDate repaymentDate, final boolean allowTransactionsOnHoliday, + final List holidays) { + if (!allowTransactionsOnHoliday && HolidayUtil.isHoliday(repaymentDate, holidays)) { + final String errorMessage = "Repayment date cannot be on a holiday"; + throw new LoanApplicationDateException("repayment.date.on.holiday", errorMessage, repaymentDate); + } + } + + public void validateLoanStatusIsActiveOrFullyPaidOrOverpaid(final Loan loan) { + if (!(loan.isOpen() || loan.isClosedObligationsMet() || loan.isOverPaid())) { + final List dataValidationErrors = new ArrayList<>(); + final String defaultUserMessage = "Loan must be Active, Fully Paid or Overpaid"; + final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.must.be.active.fully.paid.or.overpaid", + defaultUserMessage); + dataValidationErrors.add(error); + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } + + public void validateRepaymentTypeAccountStatus(final Loan loan, final LoanTransaction repaymentTransaction, final LoanEvent event) { + if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isInterestPaymentWaiver() + || repaymentTransaction.isMerchantIssuedRefund() || repaymentTransaction.isPayoutRefund() + || repaymentTransaction.isChargeRefund() || repaymentTransaction.isRepayment() || repaymentTransaction.isDownPayment() + || repaymentTransaction.isInterestRefund()) { + validateLoanStatusIsActiveOrFullyPaidOrOverpaid(loan); + } else { + validateAccountStatus(loan, event); + } + } + + public void validateAccountStatus(final Loan loan, final LoanEvent event) { + final List dataValidationErrors = new ArrayList<>(); + + switch (event) { + case LOAN_APPROVED -> { + if (!loan.isSubmittedAndPendingApproval()) { + final String defaultUserMessage = "Loan Account Approval is not allowed. Loan Account is not in submitted and pending approval state."; + final ApiParameterError error = ApiParameterError + .generalError("error.msg.loan.approve.account.is.not.submitted.and.pending.state", defaultUserMessage); + dataValidationErrors.add(error); + } + } + case LOAN_APPROVAL_UNDO -> { + if (!loan.isApproved()) { + final String defaultUserMessage = "Loan Account Undo Approval is not allowed. Loan Account is not in approved state."; + final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.undo.approval.account.is.not.approved", + defaultUserMessage); + dataValidationErrors.add(error); + } + } + case LOAN_DISBURSED -> { + if ((!(loan.isApproved() && loan.isNotDisbursed()) && !loan.getLoanProduct().isMultiDisburseLoan()) + || (loan.getLoanProduct().isMultiDisburseLoan() && !loan.isAllTranchesNotDisbursed())) { + 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); + dataValidationErrors.add(error); + } + } + case LOAN_DISBURSAL_UNDO -> { + if (!loan.isOpen()) { + final String defaultUserMessage = "Loan Undo disbursal is not allowed. Loan Account is not active."; + final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.undo.disbursal.account.is.not.active", + defaultUserMessage); + dataValidationErrors.add(error); + } + if (loan.isOpen() && loan.isTopup()) { + final String defaultUserMessage = "Loan Undo disbursal is not allowed on Topup Loans"; + final ApiParameterError error = ApiParameterError + .generalError("error.msg.loan.undo.disbursal.not.allowed.on.topup.loan", defaultUserMessage); + dataValidationErrors.add(error); + } + } + case LOAN_REPAYMENT_OR_WAIVER -> { + if (!loan.isOpen()) { + final String defaultUserMessage = "Loan Repayment (or its types) or Waiver is not allowed. Loan Account is not active."; + final ApiParameterError error = ApiParameterError + .generalError("error.msg.loan.repayment.or.waiver.account.is.not.active", defaultUserMessage); + dataValidationErrors.add(error); + } + } + case WRITE_OFF_OUTSTANDING -> { + if (!loan.isOpen()) { + final String defaultUserMessage = "Loan Written off is not allowed. Loan Account is not active."; + final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.writtenoff.account.is.not.active", + defaultUserMessage); + dataValidationErrors.add(error); + } + } + case WRITE_OFF_OUTSTANDING_UNDO -> { + if (!loan.isClosedWrittenOff()) { + final String defaultUserMessage = "Loan Undo Written off is not allowed. Loan Account is not Written off."; + final ApiParameterError error = ApiParameterError + .generalError("error.msg.loan.undo.writtenoff.account.is.not.written.off", defaultUserMessage); + dataValidationErrors.add(error); + } + } + case LOAN_CHARGE_PAYMENT -> { + if (!loan.isOpen()) { + final String defaultUserMessage = "Charge payment is not allowed. Loan Account is not Active."; + final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.charge.payment.account.is.not.active", + defaultUserMessage); + dataValidationErrors.add(error); + } + } + case LOAN_CLOSED -> { + if (!loan.isOpen()) { + final String defaultUserMessage = "Closing Loan Account is not allowed. Loan Account is not Active."; + final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.close.account.is.not.active", + defaultUserMessage); + dataValidationErrors.add(error); + } + } + case LOAN_EDIT_MULTI_DISBURSE_DATE -> { + if (loan.isClosed()) { + final String defaultUserMessage = "Edit disbursement is not allowed. Loan Account is not active."; + final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.edit.disbursement.account.is.not.active", + defaultUserMessage); + dataValidationErrors.add(error); + } + } + case LOAN_RECOVERY_PAYMENT -> { + if (!loan.isClosedWrittenOff()) { + final String defaultUserMessage = "Recovery repayments may only be made on loans which are written off"; + final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.account.is.not.written.off", + defaultUserMessage); + dataValidationErrors.add(error); + } + } + case LOAN_REFUND -> { + if (!loan.isOpen()) { + final String defaultUserMessage = "Loan Refund is not allowed. Loan Account is not active."; + final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.refund.account.is.not.active", + defaultUserMessage); + dataValidationErrors.add(error); + } + } + case LOAN_DISBURSAL_UNDO_LAST -> { + if (!loan.isOpen()) { + final String defaultUserMessage = "Loan Undo last disbursal is not allowed. Loan Account is not active."; + final ApiParameterError error = ApiParameterError + .generalError("error.msg.loan.undo.last.disbursal.account.is.not.active", defaultUserMessage); + dataValidationErrors.add(error); + } + } + case LOAN_FORECLOSURE -> { + if (!loan.isOpen()) { + final String defaultUserMessage = "Loan foreclosure is not allowed. Loan Account is not active."; + final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.foreclosure.account.is.not.active", + defaultUserMessage); + dataValidationErrors.add(error); + } + } + case LOAN_CREDIT_BALANCE_REFUND -> { + if (!loan.getStatus().isOverpaid()) { + final String defaultUserMessage = "Loan Credit Balance Refund is not allowed. Loan Account is not Overpaid."; + final ApiParameterError error = ApiParameterError + .generalError("error.msg.loan.credit.balance.refund.account.is.not.overpaid", defaultUserMessage); + dataValidationErrors.add(error); + } + } + case LOAN_CHARGE_ADJUSTMENT -> { + if (!(loan.getStatus().isActive() || loan.getStatus().isClosedObligationsMet() || loan.getStatus().isOverpaid())) { + final String defaultUserMessage = "Loan Charge Adjustment is not allowed. Loan Account must be either Active, Fully repaid or Overpaid."; + final ApiParameterError error = ApiParameterError + .generalError("error.msg.loan.charge.adjustment.account.is.not.in.valid.state", defaultUserMessage); + dataValidationErrors.add(error); + } + } + default -> { + } + } + + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanRefundValidator.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanRefundValidator.java new file mode 100644 index 00000000000..25d9f030c6e --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanRefundValidator.java @@ -0,0 +1,114 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.serialization; + +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; +import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; +import org.springframework.stereotype.Component; + +@Component +public final class LoanRefundValidator { + + public void validateTransferRefund(final Loan loan, final LoanTransaction loanTransaction) { + if (loan.getStatus().isOverpaid()) { + if (loan.getTotalOverpaid().compareTo(loanTransaction.getAmount(loan.getCurrency()).getAmount()) < 0) { + final String errorMessage = "The refund amount must be less than or equal to overpaid amount "; + throw new InvalidLoanStateTransitionException("transaction", "is.exceeding.overpaid.amount", errorMessage, + loan.getTotalOverpaid(), loanTransaction.getAmount(loan.getCurrency()).getAmount()); + } else if (!loan.isAfterLastRepayment(loanTransaction, loan.getLoanTransactions())) { + final String errorMessage = "Transfer funds is allowed only after last repayment date"; + throw new InvalidLoanStateTransitionException("transaction", "is.not.after.repayment.date", errorMessage); + } + } else { + final String errorMessage = "Transfer funds is allowed only for loan accounts with overpaid status "; + throw new InvalidLoanStateTransitionException("transaction", "is.not.a.overpaid.loan", errorMessage); + } + } + + public void validateTransactionDateAfterDisbursement(final Loan loan, final LocalDate loanTransactionDate) { + if (DateUtils.isBefore(loanTransactionDate, loan.getDisbursementDate())) { + final String errorMessage = "The transaction date cannot be before the loan disbursement date: " + + loan.getDisbursementDate().toString(); + throw new InvalidLoanStateTransitionException("transaction", "cannot.be.before.disbursement.date", errorMessage, + loanTransactionDate, loan.getDisbursementDate()); + } + } + + public void validateCreditBalanceRefund(final Loan loan, final LoanTransaction newCreditBalanceRefundTransaction) { + if (!newCreditBalanceRefundTransaction.isGreaterThanZeroAndLessThanOrEqualTo(loan.getTotalOverpaid())) { + final String errorMessage = "Transaction Amount (" + + newCreditBalanceRefundTransaction.getAmount(loan.getCurrency()).getAmount().toString() + + ") must be > zero and <= Overpaid amount (" + loan.getTotalOverpaid().toString() + ")."; + final List dataValidationErrors = new ArrayList<>(); + final ApiParameterError error = ApiParameterError.parameterError( + "error.msg.transactionAmount.invalid.must.be.>zero.and<=overpaidamount", errorMessage, "transactionAmount", + newCreditBalanceRefundTransaction.getAmount(loan.getCurrency())); + dataValidationErrors.add(error); + + throw new PlatformApiDataValidationException("validation.msg.validation.errors.exist", "Validation errors exist.", + dataValidationErrors); + } + } + + public void validateRefundEligibility(final Loan loan, final LoanTransaction loanTransaction) { + if (loan.getStatus().isOverpaid() || loan.getStatus().isClosed()) { + final String errorMessage = "This refund option is only for active loans "; + throw new InvalidLoanStateTransitionException("transaction", "is.exceeding.overpaid.amount", errorMessage, + loan.getTotalOverpaid(), loanTransaction.getAmount(loan.getCurrency()).getAmount()); + } else if (loan.getTotalPaidInRepayments().isZero()) { + final String errorMessage = "Cannot refund when no payment has been made"; + throw new InvalidLoanStateTransitionException("transaction", "no.payment.yet.made.for.loan", errorMessage); + } + } + + public void validateRefundTransactionType(final LoanTransaction loanTransaction) { + if (loanTransaction.isNotRefundForActiveLoan()) { + final String errorMessage = "A transaction of type refund was expected but not received."; + throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.refund.transaction", errorMessage); + } + } + + public void validateTransactionDateNotInFuture(final LocalDate transactionDate) { + if (DateUtils.isDateInTheFuture(transactionDate)) { + final String errorMessage = "The transaction date cannot be in the future."; + throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, transactionDate); + } + } + + public void validateTransactionAmountThreshold(final Loan loan, final LoanTransaction adjustedTransaction) { + if (loan.getLoanProduct().isMultiDisburseLoan() && adjustedTransaction == null) { + final BigDecimal totalDisbursed = loan.getDisbursedAmount(); + final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments(); + final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted); + if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0) { + final String errorMessage = "The transaction amount cannot exceed threshold."; + throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage); + } + } + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeService.java new file mode 100644 index 00000000000..5718e7375d1 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeService.java @@ -0,0 +1,84 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.math.BigDecimal; +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; + +@RequiredArgsConstructor +public class LoanChargeService { + + private final LoanChargeValidator loanChargeValidator; + + public void recalculateAllCharges(final Loan loan) { + Set charges = loan.getActiveCharges(); + int penaltyWaitPeriod = 0; + for (final LoanCharge loanCharge : charges) { + recalculateLoanCharge(loan, loanCharge, penaltyWaitPeriod); + } + loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement()); + } + + public void recalculateLoanCharge(final Loan loan, final LoanCharge loanCharge, final int penaltyWaitPeriod) { + BigDecimal amount = BigDecimal.ZERO; + BigDecimal chargeAmt; + BigDecimal totalChargeAmt = BigDecimal.ZERO; + if (loanCharge.getChargeCalculation().isPercentageBased()) { + if (loanCharge.isOverdueInstallmentCharge()) { + amount = loan.calculateOverdueAmountPercentageAppliedTo(loanCharge, penaltyWaitPeriod); + } else { + amount = loan.calculateAmountPercentageAppliedTo(loanCharge); + } + chargeAmt = loanCharge.getPercentage(); + if (loanCharge.isInstalmentFee()) { + totalChargeAmt = loan.calculatePerInstallmentChargeAmount(loanCharge); + } + } else { + chargeAmt = loanCharge.amountOrPercentage(); + } + if (loanCharge.isActive()) { + loan.clearLoanInstallmentChargesBeforeRegeneration(loanCharge); + loanCharge.update(chargeAmt, loanCharge.getDueLocalDate(), amount, loan.fetchNumberOfInstallmensAfterExceptions(), + totalChargeAmt); + loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate()); + } + } + + public void makeChargePayment(final Loan loan, final Long chargeId, final LoanLifecycleStateMachine loanLifecycleStateMachine, + final List existingTransactionIds, final List existingReversedTransactionIds, + final LoanTransaction paymentTransaction, final Integer installmentNumber) { + loanChargeValidator.validateChargePaymentNotInFuture(paymentTransaction); + existingTransactionIds.addAll(loan.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); + LoanCharge charge = null; + for (final LoanCharge loanCharge : loan.getCharges()) { + if (loanCharge.isActive() && chargeId.equals(loanCharge.getId())) { + charge = loanCharge; + } + } + loan.handleChargePaidTransaction(charge, paymentTransaction, loanLifecycleStateMachine, installmentNumber); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerService.java index 57ee4e9c3f9..01f71bba9fe 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerService.java @@ -20,11 +20,17 @@ import org.apache.fineract.infrastructure.core.api.JsonCommand; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; +import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; public interface LoanDownPaymentHandlerService { LoanTransaction handleDownPayment(ScheduleGeneratorDTO scheduleGeneratorDTO, JsonCommand command, LoanTransaction disbursementTransaction, Loan loan); + + ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(Loan loan, LoanTransaction newTransactionDetail, + LoanLifecycleStateMachine loanLifecycleStateMachine, LoanTransaction transactionForAdjustment, + ScheduleGeneratorDTO scheduleGeneratorDTO); } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java index 8fdc76e98e4..8cb9468b3f4 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImpl.java @@ -18,17 +18,39 @@ */ package org.apache.fineract.portfolio.loanaccount.service; +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.ACTUAL_DISBURSEMENT_DATE; + +import java.math.BigDecimal; +import java.time.LocalDate; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; +import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionDownPaymentPostBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionDownPaymentPreBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; +import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; +import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; +import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; +import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanRefundValidator; @Slf4j @RequiredArgsConstructor @@ -36,12 +58,16 @@ public class LoanDownPaymentHandlerServiceImpl implements LoanDownPaymentHandler private final LoanTransactionRepository loanTransactionRepository; private final BusinessEventNotifierService businessEventNotifierService; + private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator; + private final LoanScheduleService loanScheduleService; + private final LoanRefundService loanRefundService; + private final LoanRefundValidator loanRefundValidator; @Override public LoanTransaction handleDownPayment(ScheduleGeneratorDTO scheduleGeneratorDTO, JsonCommand command, LoanTransaction disbursementTransaction, Loan loan) { businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionDownPaymentPreBusinessEvent(loan)); - LoanTransaction downPaymentTransaction = loan.handleDownPayment(disbursementTransaction, command, scheduleGeneratorDTO); + LoanTransaction downPaymentTransaction = handleDownPayment(loan, disbursementTransaction, command, scheduleGeneratorDTO); if (downPaymentTransaction != null) { downPaymentTransaction = loanTransactionRepository.saveAndFlush(downPaymentTransaction); businessEventNotifierService.notifyPostBusinessEvent(new LoanTransactionDownPaymentPostBusinessEvent(downPaymentTransaction)); @@ -49,4 +75,165 @@ public LoanTransaction handleDownPayment(ScheduleGeneratorDTO scheduleGeneratorD } return downPaymentTransaction; } + + @Override + public ChangedTransactionDetail handleRepaymentOrRecoveryOrWaiverTransaction(final Loan loan, final LoanTransaction loanTransaction, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction, + final ScheduleGeneratorDTO scheduleGeneratorDTO) { + ChangedTransactionDetail changedTransactionDetail = null; + + if (loanTransaction.isRecoveryRepayment()) { + loanLifecycleStateMachine.transition(LoanEvent.LOAN_RECOVERY_PAYMENT, loan); + } + + if (loanTransaction.isRecoveryRepayment() + && loanTransaction.getAmount(loan.getCurrency()).getAmount().compareTo(loan.getSummary().getTotalWrittenOff()) > 0) { + 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); + } + + loanTransaction.updateLoan(loan); + + final boolean isTransactionChronologicallyLatest = loan.isChronologicallyLatestRepaymentOrWaiver(loanTransaction); + + if (loanTransaction.isNotZero()) { + loan.addLoanTransaction(loanTransaction); + } + + if (loanTransaction.isNotRepaymentLikeType() && loanTransaction.isNotWaiver() && loanTransaction.isNotRecoveryRepayment()) { + final String errorMessage = "A transaction of type repayment or recovery repayment or waiver was expected but not received."; + throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.repayment.or.waiver.or.recovery.transaction", + errorMessage); + } + + final LocalDate loanTransactionDate = loanRefundService.extractTransactionDate(loan, loanTransaction); + + if (DateUtils.isDateInTheFuture(loanTransactionDate)) { + final String errorMessage = "The transaction date cannot be in the future."; + throw new InvalidLoanStateTransitionException("transaction", "cannot.be.a.future.date", errorMessage, loanTransactionDate); + } + + if (loanTransaction.isInterestWaiver()) { + Money totalInterestOutstandingOnLoan = loan.getTotalInterestOutstandingOnLoan(); + if (adjustedTransaction != null) { + totalInterestOutstandingOnLoan = totalInterestOutstandingOnLoan.plus(adjustedTransaction.getAmount(loan.loanCurrency())); + } + if (loanTransaction.getAmount(loan.getCurrency()).isGreaterThan(totalInterestOutstandingOnLoan)) { + final String errorMessage = "The amount of interest to waive cannot be greater than total interest outstanding on loan."; + throw new InvalidLoanStateTransitionException("waive.interest", "amount.exceeds.total.outstanding.interest", errorMessage, + loanTransaction.getAmount(loan.getCurrency()), totalInterestOutstandingOnLoan.getAmount()); + } + } + + loanRefundValidator.validateTransactionAmountThreshold(loan, adjustedTransaction); + + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor(); + + final LoanRepaymentScheduleInstallment currentInstallment = loan + .fetchLoanRepaymentScheduleInstallmentByDueDate(loanTransaction.getTransactionDate()); + + boolean reprocess = loan.isForeclosure() || !isTransactionChronologicallyLatest || adjustedTransaction != null + || !DateUtils.isEqualBusinessDate(loanTransaction.getTransactionDate()) || currentInstallment == null + || !currentInstallment.getTotalOutstanding(loan.getCurrency()).isEqualTo(loanTransaction.getAmount(loan.getCurrency())); + + if (isTransactionChronologicallyLatest && adjustedTransaction == null + && (!reprocess || !loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) && !loan.isForeclosure()) { + loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, + new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), + new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); + reprocess = false; + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + if (currentInstallment == null || currentInstallment.isNotFullyPaidOff()) { + reprocess = true; + } else { + final LoanRepaymentScheduleInstallment nextInstallment = loan + .fetchRepaymentScheduleInstallment(currentInstallment.getInstallmentNumber() + 1); + if (nextInstallment != null && nextInstallment.getTotalPaidInAdvance(loan.getCurrency()).isGreaterThanZero()) { + reprocess = true; + } + } + } + } + if (reprocess) { + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() + && !loan.getLoanProductRelatedDetail().getLoanScheduleType().equals(LoanScheduleType.PROGRESSIVE)) { + loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); + } + changedTransactionDetail = loan.reprocessTransactions(); + } + + loan.updateLoanSummaryDerivedFields(); + + /** + * FIXME: Vishwas, skipping post loan transaction checks for Loan recoveries + **/ + if (loanTransaction.isNotRecoveryRepayment()) { + loan.doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine); + } + + if (loan.getLoanProduct().isMultiDisburseLoan()) { + final BigDecimal totalDisbursed = loan.getDisbursedAmount(); + final BigDecimal totalPrincipalAdjusted = loan.getSummary().getTotalPrincipalAdjustments(); + final BigDecimal totalPrincipalCredited = totalDisbursed.add(totalPrincipalAdjusted); + if (totalPrincipalCredited.compareTo(loan.getSummary().getTotalPrincipalRepaid()) < 0 + && loan.repaymentScheduleDetail().getPrincipal().minus(totalDisbursed).isGreaterThanZero()) { + final String errorMessage = "The transaction amount cannot exceed threshold."; + throw new InvalidLoanStateTransitionException("transaction", "amount.exceeds.threshold", errorMessage); + } + } + + return changedTransactionDetail; + } + + private LoanTransaction handleDownPayment(final Loan loan, final LoanTransaction disbursementTransaction, final JsonCommand command, + final ScheduleGeneratorDTO scheduleGeneratorDTO) { + final LocalDate disbursedOn = command.localDateValueOfParameterNamed(ACTUAL_DISBURSEMENT_DATE); + final BigDecimal disbursedAmountPercentageForDownPayment = loan.getLoanRepaymentScheduleDetail() + .getDisbursedAmountPercentageForDownPayment(); + ExternalId externalId = ExternalId.empty(); + if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } + Money downPaymentMoney = Money.of(loan.getCurrency(), + MathUtil.percentageOf(disbursementTransaction.getAmount(), disbursedAmountPercentageForDownPayment, 19)); + if (loan.getLoanProduct().getInstallmentAmountInMultiplesOf() != null) { + downPaymentMoney = Money.roundToMultiplesOf(downPaymentMoney, loan.getLoanProduct().getInstallmentAmountInMultiplesOf()); + } + final Money adjustedDownPaymentMoney = switch (loan.getLoanProductRelatedDetail().getLoanScheduleType()) { + // For Cumulative loan: To check whether the loan was overpaid when the disbursement happened and to get the + // proper amount after the disbursement we are using two balances: + // 1. Whether the loan is still overpaid after the disbursement, + // 2. if the loan is not overpaid anymore after the disbursement, but was it more overpaid than the + // calculated down-payment amount? + case CUMULATIVE -> { + if (loan.getTotalOverpaidAsMoney().isGreaterThanZero()) { + yield Money.zero(loan.getCurrency()); + } + yield MathUtil.negativeToZero(downPaymentMoney.minus(MathUtil.negativeToZero(disbursementTransaction + .getAmount(loan.getCurrency()).minus(disbursementTransaction.getOutstandingLoanBalanceMoney(loan.getCurrency()))))); + } + // For Progressive loan: Disbursement transaction portion balances are enough to see whether the overpayment + // amount was more than the calculated down-payment amount + case PROGRESSIVE -> + MathUtil.negativeToZero(downPaymentMoney.minus(disbursementTransaction.getOverPaymentPortion(loan.getCurrency()))); + }; + + if (adjustedDownPaymentMoney.isGreaterThanZero()) { + final LoanTransaction downPaymentTransaction = LoanTransaction.downPayment(loan.getOffice(), adjustedDownPaymentMoney, null, + disbursedOn, externalId); + final LoanEvent event = LoanEvent.LOAN_REPAYMENT_OR_WAIVER; + loanDownPaymentTransactionValidator.validateRepaymentTypeAccountStatus(loan, downPaymentTransaction, event); + final HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO(); + loanDownPaymentTransactionValidator.validateRepaymentDateIsOnHoliday(downPaymentTransaction.getTransactionDate(), + holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays()); + loanDownPaymentTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(downPaymentTransaction.getTransactionDate(), + holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay()); + + handleRepaymentOrRecoveryOrWaiverTransaction(loan, downPaymentTransaction, loan.getLoanLifecycleStateMachine(), null, + scheduleGeneratorDTO); + return downPaymentTransaction; + } else { + return null; + } + } } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanRefundService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanRefundService.java new file mode 100644 index 00000000000..d9fd097b1b8 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanRefundService.java @@ -0,0 +1,123 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.time.LocalDate; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.MathUtil; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; +import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanRefundValidator; + +@RequiredArgsConstructor +public class LoanRefundService { + + private final LoanRefundValidator loanRefundValidator; + + public void makeRefund(final Loan loan, final LoanTransaction loanTransaction, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, + final List existingReversedTransactionIds) { + existingTransactionIds.addAll(loan.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); + + loanRefundValidator.validateTransferRefund(loan, loanTransaction); + + loanTransaction.updateLoan(loan); + + if (loanTransaction.isNotZero()) { + loan.addLoanTransaction(loanTransaction); + } + loan.updateLoanSummaryDerivedFields(); + loan.doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine); + } + + public LocalDate extractTransactionDate(final Loan loan, final LoanTransaction loanTransaction) { + final LocalDate loanTransactionDate = loanTransaction.getTransactionDate(); + loanRefundValidator.validateTransactionDateAfterDisbursement(loan, loanTransactionDate); + return loanTransactionDate; + } + + public void makeRefundForActiveLoan(final Loan loan, final LoanTransaction loanTransaction, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, + final List existingReversedTransactionIds) { + existingTransactionIds.addAll(loan.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); + + handleRefundTransaction(loan, loanTransaction, loanLifecycleStateMachine, null); + } + + public void creditBalanceRefund(final Loan loan, final LoanTransaction newCreditBalanceRefundTransaction, + final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, final List existingTransactionIds, + final List existingReversedTransactionIds) { + loanRefundValidator.validateCreditBalanceRefund(loan, newCreditBalanceRefundTransaction); + + existingTransactionIds.addAll(loan.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); + + loan.getLoanTransactions().add(newCreditBalanceRefundTransaction); + + loan.updateLoanSummaryDerivedFields(); + + if (MathUtil.isEmpty(loan.getTotalOverpaid())) { + loan.setOverpaidOnDate(null); + loan.setClosedOnDate(newCreditBalanceRefundTransaction.getTransactionDate()); + defaultLoanLifecycleStateMachine.transition(LoanEvent.LOAN_CREDIT_BALANCE_REFUND, loan); + } + } + + private void handleRefundTransaction(final Loan loan, final LoanTransaction loanTransaction, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final LoanTransaction adjustedTransaction) { + loanLifecycleStateMachine.transition(LoanEvent.LOAN_REFUND, loan); + + loanTransaction.updateLoan(loan); + + loanRefundValidator.validateRefundEligibility(loan, loanTransaction); + + if (loanTransaction.isNotZero()) { + loan.addLoanTransaction(loanTransaction); + } + + loanRefundValidator.validateRefundTransactionType(loanTransaction); + + final LocalDate loanTransactionDate = extractTransactionDate(loan, loanTransaction); + + loanRefundValidator.validateTransactionDateNotInFuture(loanTransactionDate); + loanRefundValidator.validateTransactionAmountThreshold(loan, adjustedTransaction); + + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor(); + + // If it's a refund + if (adjustedTransaction == null) { + loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, + new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), + new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); + } else { + loan.reprocessTransactions(); + } + + loan.updateLoanSummaryDerivedFields(); + loan.doPostLoanTransactionChecks(loanTransaction.getTransactionDate(), loanLifecycleStateMachine); + } +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java new file mode 100644 index 00000000000..b7fe87bb2bc --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanScheduleService.java @@ -0,0 +1,121 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.time.LocalDate; +import java.util.List; +import java.util.Set; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; +import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleDTO; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; + +@RequiredArgsConstructor +public class LoanScheduleService { + + private final LoanChargeService loanChargeService; + + /** + * Ability to regenerate the repayment schedule based on the loans current details/state. + */ + public void regenerateRepaymentSchedule(final Loan loan, final ScheduleGeneratorDTO scheduleGeneratorDTO) { + final LoanScheduleModel loanSchedule = loan.regenerateScheduleModel(scheduleGeneratorDTO); + if (loanSchedule == null) { + return; + } + loan.updateLoanSchedule(loanSchedule); + final Set charges = loan.getActiveCharges(); + for (final LoanCharge loanCharge : charges) { + if (!loanCharge.isWaived()) { + loanChargeService.recalculateLoanCharge(loan, loanCharge, scheduleGeneratorDTO.getPenaltyWaitPeriod()); + } + } + } + + public void recalculateScheduleFromLastTransaction(final Loan loan, final ScheduleGeneratorDTO generatorDTO) { + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO); + } else { + regenerateRepaymentSchedule(loan, generatorDTO); + } + loan.reprocessTransactions(); + } + + public ChangedTransactionDetail recalculateScheduleFromLastTransaction(final Loan loan, final ScheduleGeneratorDTO generatorDTO, + final List existingTransactionIds, final List existingReversedTransactionIds) { + existingTransactionIds.addAll(loan.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); + /* + * LocalDate recalculateFrom = null; List loanTransactions = + * this.retrieveListOfTransactionsPostDisbursementExcludeAccruals(); for (LoanTransaction loanTransaction : + * loanTransactions) { if (recalculateFrom == null || + * loanTransaction.getTransactionDate().isAfter(recalculateFrom)) { recalculateFrom = + * loanTransaction.getTransactionDate(); } } generatorDTO.setRecalculateFrom(recalculateFrom); + */ + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO); + } else { + regenerateRepaymentSchedule(loan, generatorDTO); + } + return loan.reprocessTransactions(); + } + + public void regenerateRepaymentScheduleWithInterestRecalculation(final Loan loan, final ScheduleGeneratorDTO generatorDTO) { + final LocalDate lastTransactionDate = loan.getLastUserTransactionDate(); + final LoanScheduleDTO loanSchedule = loan.getRecalculatedSchedule(generatorDTO); + if (loanSchedule == null) { + return; + } + // Either the installments got recalculated or the model + if (loanSchedule.getInstallments() != null) { + loan.updateLoanSchedule(loanSchedule.getInstallments()); + } else { + loan.updateLoanSchedule(loanSchedule.getLoanScheduleModel()); + } + loan.setInterestRecalculatedOn(DateUtils.getBusinessLocalDate()); + final LocalDate lastRepaymentDate = loan.getLastRepaymentPeriodDueDate(true); + final Set charges = loan.getActiveCharges(); + for (final LoanCharge loanCharge : charges) { + if (!loanCharge.isDueAtDisbursement()) { + loan.updateOverdueScheduleInstallment(loanCharge); + if (loanCharge.getDueLocalDate() == null || !DateUtils.isBefore(lastRepaymentDate, loanCharge.getDueLocalDate())) { + if ((loanCharge.isInstalmentFee() || !loanCharge.isWaived()) && (loanCharge.getDueLocalDate() == null + || !DateUtils.isAfter(lastTransactionDate, loanCharge.getDueLocalDate()))) { + loanChargeService.recalculateLoanCharge(loan, loanCharge, generatorDTO.getPenaltyWaitPeriod()); + loanCharge.updateWaivedAmount(loan.getCurrency()); + } + } else { + loanCharge.setActive(false); + } + } + } + + loan.processPostDisbursementTransactions(); + } + + public ChangedTransactionDetail handleRegenerateRepaymentScheduleWithInterestRecalculation(final Loan loan, + final ScheduleGeneratorDTO generatorDTO) { + regenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO); + return loan.reprocessTransactions(); + } +} diff --git a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java index 6d78a6c9d58..ae1f67eb667 100644 --- a/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java +++ b/fineract-progressive-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java @@ -802,6 +802,7 @@ protected void createNewTransaction(final LoanTransaction oldTransaction, final oldChargebackRelations.forEach(relations::remove); relations.add(LoanTransactionRelation.linkToTransaction(originalTransaction, newTransaction, CHARGEBACK)); } + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(oldTransaction.getLoan(), oldTransaction, "reversed"); oldTransaction.reverse(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/GroupingTypesWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/GroupingTypesWritePlatformServiceJpaRepositoryImpl.java index 65c1b05844f..1de72234ef6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/GroupingTypesWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/service/GroupingTypesWritePlatformServiceJpaRepositoryImpl.java @@ -82,6 +82,7 @@ import org.apache.fineract.portfolio.group.serialization.GroupingTypesDataValidator; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService; import org.apache.fineract.portfolio.note.domain.Note; import org.apache.fineract.portfolio.note.domain.NoteRepository; import org.apache.fineract.portfolio.savings.domain.SavingsAccount; @@ -115,6 +116,7 @@ public class GroupingTypesWritePlatformServiceJpaRepositoryImpl implements Group private final AccountNumberGenerator accountNumberGenerator; private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService; private final BusinessEventNotifierService businessEventNotifierService; + private final LoanOfficerService loanOfficerService; private CommandProcessingResult createGroupingType(final JsonCommand command, final GroupTypes groupingType, final Long centerId) { try { @@ -517,7 +519,7 @@ public CommandProcessingResult assignGroupOrCenterStaff(final Long groupId, fina if (this.loanRepositoryWrapper.doNonClosedLoanAccountsExistForClient(client.getId())) { for (final Loan loan : this.loanRepositoryWrapper.findLoanByClientId(client.getId())) { if (loan.isDisbursed() && !loan.isClosed()) { - loan.reassignLoanOfficer(staff, loanOfficerReassignmentDate); + loanOfficerService.reassignLoanOfficer(loan, staff, loanOfficerReassignmentDate); } } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/starter/GroupConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/starter/GroupConfiguration.java index ebd60f843ad..c39245a88c4 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/starter/GroupConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/group/starter/GroupConfiguration.java @@ -57,6 +57,7 @@ import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformService; import org.apache.fineract.portfolio.group.service.GroupingTypesWritePlatformServiceJpaRepositoryImpl; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService; import org.apache.fineract.portfolio.note.domain.NoteRepository; import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; @@ -90,14 +91,14 @@ public GroupingTypesWritePlatformService groupingTypesWritePlatformService(Platf ConfigurationDomainService configurationDomainService, SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper, AccountNumberFormatRepositoryWrapper accountNumberFormatRepository, AccountNumberGenerator accountNumberGenerator, EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, - BusinessEventNotifierService businessEventNotifierService + BusinessEventNotifierService businessEventNotifierService, LoanOfficerService loanOfficerService ) { return new GroupingTypesWritePlatformServiceJpaRepositoryImpl(context, groupRepository, clientRepositoryWrapper, officeRepositoryWrapper, staffRepository, noteRepository, groupLevelRepository, fromApiJsonDeserializer, loanRepositoryWrapper, codeValueRepository, commandProcessingService, calendarInstanceRepository, configurationDomainService, savingsAccountRepositoryWrapper, accountNumberFormatRepository, accountNumberGenerator, - entityDatatableChecksWritePlatformService, businessEventNotifierService + entityDatatableChecksWritePlatformService, businessEventNotifierService, loanOfficerService ); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java index cb4254bc9aa..6452ced33d6 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java @@ -99,11 +99,19 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanForeclosureValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator; import org.apache.fineract.portfolio.loanaccount.service.InterestRefundService; import org.apache.fineract.portfolio.loanaccount.service.InterestRefundServiceDelegate; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService; +import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService; +import org.apache.fineract.portfolio.loanaccount.service.LoanRefundService; +import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService; import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes; @@ -148,6 +156,14 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { private final LoanAccrualsProcessingService loanAccrualsProcessingService; private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory; private final InterestRefundServiceDelegate interestRefundServiceDelegate; + private final LoanTransactionValidator loanTransactionValidator; + private final LoanForeclosureValidator loanForeclosureValidator; + private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator; + private final LoanChargeService loanChargeService; + private final LoanScheduleService loanScheduleService; + private final LoanDownPaymentHandlerService loanDownPaymentHandlerService; + private final LoanChargeValidator loanChargeValidator; + private final LoanRefundService loanRefundService; @Transactional @Override @@ -235,9 +251,20 @@ public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransact final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom, holidayDetailDto); - final ChangedTransactionDetail changedTransactionDetail = loan.makeRepayment(newRepaymentTransaction, - defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, isRecoveryRepayment, - scheduleGeneratorDTO, isHolidayValidationDone); + if (!isHolidayValidationDone) { + final HolidayDetailDTO holidayDetailDTO = scheduleGeneratorDTO.getHolidayDetailDTO(); + loanTransactionValidator.validateRepaymentDateIsOnHoliday(newRepaymentTransaction.getTransactionDate(), + holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays()); + loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newRepaymentTransaction.getTransactionDate(), + holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay()); + } + final LoanEvent event = isRecoveryRepayment ? LoanEvent.LOAN_RECOVERY_PAYMENT : LoanEvent.LOAN_REPAYMENT_OR_WAIVER; + loanTransactionValidator.validateActivityNotBeforeLastTransactionDate(loan, newRepaymentTransaction.getTransactionDate(), event); + loanDownPaymentTransactionValidator.validateRepaymentTypeAccountStatus(loan, newRepaymentTransaction, event); + loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, event, + newRepaymentTransaction.getTransactionDate()); + final ChangedTransactionDetail changedTransactionDetail = makeRepayment(loan, newRepaymentTransaction, + defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { loanAccrualsProcessingService.reprocessExistingAccruals(loan); @@ -445,8 +472,17 @@ public LoanTransaction makeChargePayment(final Loan loan, final Long chargeId, f HolidayDetailDTO holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay); - loan.makeChargePayment(chargeId, defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, - holidayDetailDTO, newPaymentTransaction, installmentNumber); + loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_CHARGE_PAYMENT); + loanTransactionValidator.validateRepaymentDateIsOnHoliday(newPaymentTransaction.getTransactionDate(), + holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays()); + loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newPaymentTransaction.getTransactionDate(), + holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay()); + loanTransactionValidator.validateActivityNotBeforeLastTransactionDate(loan, newPaymentTransaction.getTransactionDate(), + LoanEvent.LOAN_CHARGE_PAYMENT); + loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_CHARGE_PAYMENT, + newPaymentTransaction.getTransactionDate()); + loanChargeService.makeChargePayment(loan, chargeId, defaultLoanLifecycleStateMachine, existingTransactionIds, + existingReversedTransactionIds, newPaymentTransaction, installmentNumber); } saveLoanTransactionWithDataIntegrityViolationChecks(newPaymentTransaction); saveAndFlushLoanWithDataIntegrityViolationChecks(loan); @@ -532,8 +568,13 @@ public LoanTransaction makeRefund(final Long accountId, final CommandProcessingR final WorkingDays workingDays = this.workingDaysRepository.findOne(); final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled(); - loan.makeRefund(newRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, - allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay); + loanTransactionValidator.validateRepaymentDateIsOnHoliday(newRefundTransaction.getTransactionDate(), allowTransactionsOnHoliday, + holidays); + loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newRefundTransaction.getTransactionDate(), workingDays, + allowTransactionsOnNonWorkingDay); + + loanRefundService.makeRefund(loan, newRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, + existingReversedTransactionIds); saveLoanTransactionWithDataIntegrityViolationChecks(newRefundTransaction); this.loanRepositoryWrapper.saveAndFlush(loan); @@ -605,6 +646,7 @@ public void reverseTransfer(final LoanTransaction loanTransaction) { + " reversal is not allowed before or on the date when the loan got charged-off", loanTransaction.getId()); } + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, "reversed"); loanTransaction.reverse(); saveLoanTransactionWithDataIntegrityViolationChecks(loanTransaction); } @@ -671,8 +713,11 @@ public LoanTransaction creditBalanceRefund(final Loan loan, final LocalDate tran LoanTransaction newCreditBalanceRefundTransaction = LoanTransaction.creditBalanceRefund(loan, loan.getOffice(), refundAmount, transactionDate, externalId, paymentDetail); - loan.creditBalanceRefund(newCreditBalanceRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, - existingReversedTransactionIds); + loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_CREDIT_BALANCE_REFUND); + loanTransactionValidator.validateRefundDateIsAfterLastRepayment(loan, newCreditBalanceRefundTransaction.getTransactionDate()); + + loanRefundService.creditBalanceRefund(loan, newCreditBalanceRefundTransaction, defaultLoanLifecycleStateMachine, + existingTransactionIds, existingReversedTransactionIds); newCreditBalanceRefundTransaction = this.loanTransactionRepository.saveAndFlush(newCreditBalanceRefundTransaction); @@ -709,14 +754,22 @@ public LoanTransaction makeRefundForActiveLoan(Long accountId, CommandProcessing } final LoanTransaction newRefundTransaction = LoanTransaction.refundForActiveLoan(loan.getOffice(), refundAmount, paymentDetail, transactionDate, txnExternalId); + loanTransactionValidator.validateRefundDateIsAfterLastRepayment(loan, newRefundTransaction.getTransactionDate()); final boolean allowTransactionsOnHoliday = this.configurationDomainService.allowTransactionsOnHolidayEnabled(); final List holidays = this.holidayRepository.findByOfficeIdAndGreaterThanDate(loan.getOfficeId(), transactionDate, HolidayStatusType.ACTIVE.getValue()); final WorkingDays workingDays = this.workingDaysRepository.findOne(); final boolean allowTransactionsOnNonWorkingDay = this.configurationDomainService.allowTransactionsOnNonWorkingDayEnabled(); - loan.makeRefundForActiveLoan(newRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, - existingReversedTransactionIds, allowTransactionsOnHoliday, holidays, workingDays, allowTransactionsOnNonWorkingDay); + loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_REFUND); + loanTransactionValidator.validateRepaymentDateIsOnHoliday(newRefundTransaction.getTransactionDate(), allowTransactionsOnHoliday, + holidays); + loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(newRefundTransaction.getTransactionDate(), workingDays, + allowTransactionsOnNonWorkingDay); + loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_REFUND, + newRefundTransaction.getTransactionDate()); + loanRefundService.makeRefundForActiveLoan(loan, newRefundTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, + existingReversedTransactionIds); this.loanTransactionRepository.saveAndFlush(newRefundTransaction); @@ -765,7 +818,7 @@ public LoanTransaction foreCloseLoan(Loan loan, final LocalDate foreClosureDate, Money feePayable = foreCloseDetail.getFeeChargesCharged(currency); Money penaltyPayable = foreCloseDetail.getPenaltyChargesCharged(currency); Money payPrincipal = foreCloseDetail.getPrincipal(currency); - loan.updateInstallmentsPostDate(foreClosureDate); + updateInstallmentsPostDate(loan, foreClosureDate); LoanTransaction payment = null; @@ -778,7 +831,11 @@ public LoanTransaction foreCloseLoan(Loan loan, final LocalDate foreClosureDate, } List transactionIds = new ArrayList<>(); - final ChangedTransactionDetail changedTransactionDetail = loan.handleForeClosureTransactions(payment, + if (payment != null) { + loanForeclosureValidator.validateForForeclosure(loan, payment.getTransactionDate()); + } + loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_FORECLOSURE); + final ChangedTransactionDetail changedTransactionDetail = handleForeClosureTransactions(loan, payment, defaultLoanLifecycleStateMachine, scheduleGeneratorDTO); loanAccrualsProcessingService.reprocessExistingAccruals(loan); @@ -916,7 +973,7 @@ public Pair makeRefund(final Loan loan, final } } else { if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { - loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); + loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); } loan.getLoanTransactions().add(refundTransaction); if (interestRefundTransaction != null) { @@ -1015,4 +1072,93 @@ public LoanTransaction applyInterestRefund(final Loan loan, final LoanRefundRequ return interestRefundTransaction; } + + private void updateInstallmentsPostDate(final Loan loan, final LocalDate transactionDate) { + final List newInstallments = new ArrayList<>(loan.getRepaymentScheduleInstallments()); + final MonetaryCurrency currency = loan.getCurrency(); + Money totalPrincipal = Money.zero(currency); + final Money[] balances = loan.retriveIncomeForOverlappingPeriod(transactionDate); + boolean isInterestComponent = true; + for (final LoanRepaymentScheduleInstallment installment : loan.getRepaymentScheduleInstallments()) { + if (!DateUtils.isAfter(transactionDate, installment.getDueDate())) { + totalPrincipal = totalPrincipal.plus(installment.getPrincipal(currency)); + newInstallments.remove(installment); + if (DateUtils.isEqual(transactionDate, installment.getDueDate())) { + isInterestComponent = false; + } + } + + } + + for (LoanDisbursementDetails loanDisbursementDetails : loan.getDisbursementDetails()) { + if (loanDisbursementDetails.actualDisbursementDate() == null) { + totalPrincipal = Money.of(currency, totalPrincipal.getAmount().subtract(loanDisbursementDetails.principal())); + } + } + + LocalDate installmentStartDate = loan.getDisbursementDate(); + + if (!newInstallments.isEmpty()) { + installmentStartDate = newInstallments.get(newInstallments.size() - 1).getDueDate(); + } + + int installmentNumber = newInstallments.size(); + + if (!isInterestComponent) { + installmentNumber++; + } + + final LoanRepaymentScheduleInstallment newInstallment = new LoanRepaymentScheduleInstallment(null, newInstallments.size() + 1, + installmentStartDate, transactionDate, totalPrincipal.getAmount(), balances[0].getAmount(), balances[1].getAmount(), + balances[2].getAmount(), isInterestComponent, null); + newInstallment.updateInstallmentNumber(newInstallments.size() + 1); + newInstallments.add(newInstallment); + loan.updateLoanScheduleOnForeclosure(newInstallments); + + final Set charges = loan.getActiveCharges(); + final int penaltyWaitPeriod = 0; + for (LoanCharge loanCharge : charges) { + if (DateUtils.isAfter(loanCharge.getDueLocalDate(), transactionDate)) { + loanCharge.setActive(false); + } else if (loanCharge.getDueLocalDate() == null) { + loanChargeService.recalculateLoanCharge(loan, loanCharge, penaltyWaitPeriod); + loanCharge.updateWaivedAmount(currency); + } + } + + for (LoanTransaction loanTransaction : loan.getLoanTransactions()) { + if (loanTransaction.isChargesWaiver()) { + for (LoanChargePaidBy chargePaidBy : loanTransaction.getLoanChargesPaid()) { + if ((chargePaidBy.getLoanCharge().isDueDateCharge() + && DateUtils.isBefore(transactionDate, chargePaidBy.getLoanCharge().getDueLocalDate())) + || (chargePaidBy.getLoanCharge().isInstalmentFee() && chargePaidBy.getInstallmentNumber() != null + && chargePaidBy.getInstallmentNumber() > installmentNumber)) { + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), + loanTransaction, "reversed"); + loanTransaction.reverse(); + } + } + + } + } + } + + @SuppressWarnings("null") + private ChangedTransactionDetail makeRepayment(final Loan loan, final LoanTransaction repaymentTransaction, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, + final List existingReversedTransactionIds, final ScheduleGeneratorDTO scheduleGeneratorDTO) { + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loan, repaymentTransaction, "created"); + existingTransactionIds.addAll(loan.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); + + return loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, repaymentTransaction, + loanLifecycleStateMachine, null, scheduleGeneratorDTO); + } + + private ChangedTransactionDetail handleForeClosureTransactions(final Loan loan, final LoanTransaction repaymentTransaction, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final ScheduleGeneratorDTO scheduleGeneratorDTO) { + loan.setLoanSubStatus(LoanSubStatus.FORECLOSED.getValue()); + return loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, repaymentTransaction, + loanLifecycleStateMachine, null, scheduleGeneratorDTO); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java index eb02ba9bf5e..b6ddf56c001 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleAssembler.java @@ -115,7 +115,10 @@ import org.apache.fineract.portfolio.loanaccount.serialization.VariableLoanScheduleFromApiJsonValidator; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeAssembler; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService; import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementDetailsAssembler; +import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementService; +import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.domain.AmortizationMethod; @@ -158,6 +161,9 @@ public class LoanScheduleAssembler { private final LoanRepositoryWrapper loanRepositoryWrapper; private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final LoanAccrualsProcessingService loanAccrualsProcessingService; + private final LoanDisbursementService loanDisbursementService; + private final LoanChargeService loanChargeService; + private final LoanScheduleService loanScheduleService; public LoanApplicationTerms assembleLoanTerms(final JsonElement element) { final Long loanProductId = this.fromApiJsonHelper.extractLongNamed("productId", element); @@ -936,7 +942,7 @@ public void assempleVariableScheduleFrom(final Loan loan, final String json) { } final LocalDate recalculateFrom = null; ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - loan.regenerateRepaymentSchedule(scheduleGeneratorDTO); + loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); loanAccrualsProcessingService.reprocessExistingAccruals(loan); } @@ -1478,11 +1484,11 @@ public Pair> assembleLoanApproval(AppUser currentUser, actualChanges.put(LoanApiConstants.disbursementNetDisbursalAmountParameterName, loan.getNetDisbursalAmount()); if (disbursementDataArray != null) { - loan.updateDisbursementDetails(command, actualChanges); + loanDisbursementService.updateDisbursementDetails(loan, command, actualChanges); } } - loan.recalculateAllCharges(); + loanChargeService.recalculateAllCharges(loan); loan.setApprovedOnDate(approvedOn); loan.setApprovedBy(currentUser); @@ -1507,7 +1513,7 @@ public Pair> assembleLoanApproval(AppUser currentUser, if (!actualChanges.isEmpty()) { if (actualChanges.containsKey(LoanApiConstants.approvedLoanAmountParameterName) || actualChanges.containsKey("recalculateLoanSchedule") || actualChanges.containsKey("expectedDisbursementDate")) { - loan.regenerateRepaymentSchedule(loanUtilService.buildScheduleGeneratorDTO(loan, null)); + loanScheduleService.regenerateRepaymentSchedule(loan, loanUtilService.buildScheduleGeneratorDTO(loan, null)); loanAccrualsProcessingService.reprocessExistingAccruals(loan); } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleWritePlatformServiceImpl.java index 9b9b7e4ee53..bcc6373b3da 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/loanschedule/service/LoanScheduleWritePlatformServiceImpl.java @@ -37,6 +37,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariations; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; +import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -52,6 +53,7 @@ public class LoanScheduleWritePlatformServiceImpl implements LoanScheduleWritePl private final LoanUtilService loanUtilService; private final BusinessEventNotifierService businessEventNotifierService; private final LoanAccrualsProcessingService loanAccrualsProcessingService; + private final LoanScheduleService loanScheduleService; @Override public CommandProcessingResult addLoanScheduleVariations(final Long loanId, final JsonCommand command) { @@ -98,7 +100,7 @@ public CommandProcessingResult deleteLoanScheduleVariations(final Long loanId) { loan.getLoanTermVariations().clear(); final LocalDate recalculateFrom = null; ScheduleGeneratorDTO scheduleGeneratorDTO = loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - loan.regenerateRepaymentSchedule(scheduleGeneratorDTO); + loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccountDomainService.saveLoanWithDataIntegrityViolationChecks(loan); businessEventNotifierService.notifyPostBusinessEvent(new LoanScheduleVariationsDeletedBusinessEvent(loan)); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java index 38371fb53f0..0d14d584188 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/rescheduleloan/service/LoanRescheduleRequestWritePlatformServiceImpl.java @@ -81,6 +81,7 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService; import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService; import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService; import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService; import org.apache.fineract.useradministration.domain.AppUser; @@ -121,6 +122,7 @@ public class LoanRescheduleRequestWritePlatformServiceImpl implements LoanResche private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService; private final BusinessEventNotifierService businessEventNotifierService; private final LoanAccrualsProcessingService loanAccrualsProcessingService; + private final LoanChargeService loanChargeService; /** * create a new instance of the LoanRescheduleRequest object from the JsonCommand object and persist @@ -439,7 +441,7 @@ public CommandProcessingResult approve(JsonCommand jsonCommand) { loan.updateLoanSchedule(loanSchedule.getLoanScheduleModel()); } loanAccrualsProcessingService.reprocessExistingAccruals(loan); - loan.recalculateAllCharges(); + loanChargeService.recalculateAllCharges(loan); ChangedTransactionDetail changedTransactionDetail = loan.reprocessTransactions(); this.loanRepaymentScheduleHistoryRepository.saveAll(loanRepaymentScheduleHistoryList); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java index 34996041ea4..f9db634b50e 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanApplicationValidator.java @@ -2106,7 +2106,7 @@ private void compareApprovedToProposedPrincipal(Loan loan, BigDecimal approvedLo } } - private BigDecimal getOverAppliedMax(Loan loan) { + public BigDecimal getOverAppliedMax(Loan loan) { LoanProduct loanProduct = loan.getLoanProduct(); if ("percentage".equals(loanProduct.getOverAppliedCalculationType())) { BigDecimal overAppliedNumber = BigDecimal.valueOf(loanProduct.getOverAppliedNumber()); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDisbursementValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDisbursementValidator.java new file mode 100644 index 00000000000..e6aef725912 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanDisbursementValidator.java @@ -0,0 +1,83 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.serialization; + +import java.math.BigDecimal; +import java.time.LocalDate; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; +import org.apache.fineract.portfolio.loanaccount.exception.LoanDisbursalException; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public final class LoanDisbursementValidator { + + private final LoanApplicationValidator loanApplicationValidator; + + public void compareDisbursedToApprovedOrProposedPrincipal(final Loan loan, final BigDecimal disbursedAmount, + final BigDecimal totalDisbursed) { + if (loan.loanProduct().isDisallowExpectedDisbursements() && loan.loanProduct().isAllowApprovedDisbursedAmountsOverApplied()) { + final BigDecimal maxDisbursedAmount = loanApplicationValidator.getOverAppliedMax(loan); + if (totalDisbursed.compareTo(maxDisbursedAmount) > 0) { + final String errorMessage = String.format( + "Loan disbursal amount can't be greater than maximum applied loan amount calculation. " + + "Total disbursed amount: %s Maximum disbursal amount: %s", + totalDisbursed.stripTrailingZeros().toPlainString(), maxDisbursedAmount.stripTrailingZeros().toPlainString()); + throw new InvalidLoanStateTransitionException("disbursal", + "amount.can't.be.greater.than.maximum.applied.loan.amount.calculation", errorMessage, disbursedAmount, + maxDisbursedAmount); + } + } else { + if (totalDisbursed.compareTo(loan.getApprovedPrincipal()) > 0) { + final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved principal "; + throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.principal", totalDisbursed, + loan.getApprovedPrincipal()); + } + } + } + + public void validateDisburseAmountNotExceedingApprovedAmount(final Loan loan, final BigDecimal diff, + final BigDecimal principalDisbursed) { + if (!loan.loanProduct().isMultiDisburseLoan() && diff.compareTo(BigDecimal.ZERO) < 0) { + final String errorMsg = "Loan can't be disbursed,disburse amount is exceeding approved amount "; + throw new LoanDisbursalException(errorMsg, "disburse.amount.must.be.less.than.approved.amount", principalDisbursed, + loan.getLoanRepaymentScheduleDetail().getPrincipal().getAmount()); + } + } + + public void validateDisburseDate(final Loan loan, final LocalDate disbursedOn, final LocalDate expectedDate) { + if (expectedDate != null + && (DateUtils.isAfter(disbursedOn, loan.fetchRepaymentScheduleInstallment(1).getDueDate()) + || DateUtils.isAfter(disbursedOn, expectedDate)) + && DateUtils.isEqual(disbursedOn, loan.getActualDisbursementDate())) { + final String errorMessage = "submittedOnDate cannot be after the loans expectedFirstRepaymentOnDate: " + expectedDate; + throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.after.expected.first.repayment.date", errorMessage, + disbursedOn, expectedDate); + } + + if (DateUtils.isDateInTheFuture(disbursedOn)) { + final String errorMessage = "The date on which a loan with identifier : " + loan.getAccountNumber() + + " is disbursed cannot be in the future."; + throw new InvalidLoanStateTransitionException("disbursal", "cannot.be.a.future.date", errorMessage, disbursedOn); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanForeclosureValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanForeclosureValidator.java new file mode 100644 index 00000000000..a4e22403ff5 --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanForeclosureValidator.java @@ -0,0 +1,48 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.serialization; + +import java.time.LocalDate; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException; +import org.springframework.stereotype.Component; + +@Component +public final class LoanForeclosureValidator { + + public void validateForForeclosure(final Loan loan, final LocalDate transactionDate) { + if (loan.isInterestRecalculationEnabledForProduct()) { + final String defaultUserMessage = "The loan with interest recalculation enabled cannot be foreclosed."; + throw new LoanForeclosureException("loan.with.interest.recalculation.enabled.cannot.be.foreclosured", defaultUserMessage, + loan.getId()); + } + + if (DateUtils.isDateInTheFuture(transactionDate)) { + final String defaultUserMessage = "The transactionDate cannot be in the future."; + throw new LoanForeclosureException("loan.foreclosure.transaction.date.is.in.future", defaultUserMessage, transactionDate); + } + + if (DateUtils.isBefore(transactionDate, loan.getLastUserTransactionDate())) { + final String defaultUserMessage = "The transactionDate cannot be earlier than the last transaction date."; + throw new LoanForeclosureException("loan.foreclosure.transaction.date.cannot.before.the.last.transaction.date", + defaultUserMessage, transactionDate); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanOfficerValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanOfficerValidator.java new file mode 100644 index 00000000000..eb0ca30419c --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanOfficerValidator.java @@ -0,0 +1,81 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.serialization; + +import java.time.LocalDate; +import java.util.Optional; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanOfficerAssignmentHistory; +import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentDateException; +import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerUnassignmentDateException; +import org.springframework.stereotype.Component; + +@Component +public final class LoanOfficerValidator { + + public void validateUnassignDate(final Loan loan, final LocalDate unassignDate) { + final Optional latestHistoryRecordOptional = loan.findLatestIncompleteHistoryRecord(); + + if (latestHistoryRecordOptional.isEmpty()) { + return; + } + + final LocalDate startDate = latestHistoryRecordOptional.get().getStartDate(); + if (DateUtils.isAfter(startDate, unassignDate)) { + final String errorMessage = "The Loan officer Unassign date(" + unassignDate + ") cannot be before its assignment date (" + + startDate + ")."; + throw new LoanOfficerUnassignmentDateException("cannot.be.before.assignment.date", errorMessage, loan.getId(), + loan.getLoanOfficer().getId(), startDate, unassignDate); + } else if (DateUtils.isDateInTheFuture(unassignDate)) { + final String errorMessage = "The Loan Officer Unassign date (" + unassignDate + ") cannot be in the future."; + throw new LoanOfficerUnassignmentDateException("cannot.be.a.future.date", errorMessage, unassignDate); + } + } + + public void validateReassignment(final Loan loan, final LocalDate assignmentDate, + final LoanOfficerAssignmentHistory lastAssignmentRecord) { + if (loan.isSubmittedOnDateAfter(assignmentDate)) { + final String errorMessage = "The Loan Officer assignment date (" + assignmentDate.toString() + + ") cannot be before loan submitted date (" + loan.getSubmittedOnDate().toString() + ")."; + throw new LoanOfficerAssignmentDateException("cannot.be.before.loan.submittal.date", errorMessage, assignmentDate, + loan.getSubmittedOnDate()); + } + + if (lastAssignmentRecord != null && lastAssignmentRecord.isEndDateAfter(assignmentDate)) { + final String errorMessage = "The Loan Officer assignment date (" + assignmentDate + + ") cannot be before previous Loan Officer unassigned date (" + lastAssignmentRecord.getEndDate() + ")."; + throw new LoanOfficerAssignmentDateException("cannot.be.before.previous.unassignement.date", errorMessage, assignmentDate, + lastAssignmentRecord.getEndDate()); + } + + if (DateUtils.isDateInTheFuture(assignmentDate)) { + final String errorMessage = "The Loan Officer assignment date (" + assignmentDate + ") cannot be in the future."; + throw new LoanOfficerAssignmentDateException("cannot.be.a.future.date", errorMessage, assignmentDate); + } + } + + public void validateAssignmentDateWithHistory(final Loan loan, final Optional latestHistoryRecord, + final LocalDate assignmentDate) { + if (latestHistoryRecord.isPresent() && latestHistoryRecord.get().isBeforeStartDate(assignmentDate)) { + final String errorMessage = "Loan with identifier " + loan.getId() + " was already assigned before date " + assignmentDate; + throw new LoanOfficerAssignmentDateException("is.before.last.assignment.date", errorMessage, loan.getId(), assignmentDate); + } + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java index a15efeb0f95..35f6dc69351 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/serialization/LoanTransactionValidator.java @@ -79,6 +79,8 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; +import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; +import org.apache.fineract.portfolio.loanaccount.exception.InvalidRefundDateException; import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException; import org.apache.fineract.portfolio.loanaccount.exception.LoanChargeRefundException; import org.apache.fineract.portfolio.loanaccount.exception.LoanDisbursalException; @@ -102,6 +104,7 @@ public final class LoanTransactionValidator { private final LoanUtilService loanUtilService; private final EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService; private final CalendarInstanceRepository calendarInstanceRepository; + private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator; private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { if (!dataValidationErrors.isEmpty()) { @@ -233,34 +236,6 @@ public void validateDisbursement(JsonCommand command, boolean isAccountTransfer, }); } - private static @NotNull BigDecimal collectTotalCollateral(Set 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 getDisbursementParameters(boolean isAccountTransfer) { - Set disbursementParameters; - - if (isAccountTransfer) { - disbursementParameters = new HashSet<>(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale", "dateFormat", - LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.fixedEmiAmountParameterName, - LoanApiConstants.disbursementNetDisbursalAmountParameterName)); - } else { - disbursementParameters = new HashSet<>(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale", "dateFormat", - "paymentTypeId", "accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber", "adjustRepaymentDate", - LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.fixedEmiAmountParameterName, - LoanApiConstants.postDatedChecks, LoanApiConstants.disbursementNetDisbursalAmountParameterName)); - } - return disbursementParameters; - } - public void validateDisbursementWithPostDatedChecks(final String json, final Long loanId) { final JsonElement jsonElement = this.fromApiJsonHelper.parse(json); final List dataValidationErrors = new ArrayList<>(); @@ -411,50 +386,6 @@ public void validateNewRepaymentTransaction(final String json) { validatePaymentTransaction(json); } - private void validatePaymentTransaction(String json) { - if (StringUtils.isBlank(json)) { - throw new InvalidJsonException(); - } - - final Set transactionParameters = new HashSet<>( - Arrays.asList("transactionDate", "transactionAmount", "externalId", "note", "locale", "dateFormat", "paymentTypeId", - "accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber", "loanId")); - - final Type typeOfMap = new TypeToken>() {}.getType(); - this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters); - - final List dataValidationErrors = new ArrayList<>(); - final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); - - final JsonElement element = this.fromApiJsonHelper.parse(json); - final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element); - baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull(); - - final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element); - baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount(); - - final String note = this.fromApiJsonHelper.extractStringNamed("note", element); - baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000); - - validatePaymentDetails(baseDataValidator, element); - throwExceptionIfValidationWarningsExist(dataValidationErrors); - } - - private void validatePaymentDetails(final DataValidatorBuilder baseDataValidator, final JsonElement element) { - // Validate all string payment detail fields for max length - final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("paymentTypeId", element); - - baseDataValidator.reset().parameter("paymentTypeId").value(paymentTypeId).ignoreIfNull().integerGreaterThanZero(); - - final Set paymentDetailParameters = new HashSet<>( - Arrays.asList("accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber")); - for (final String paymentDetailParameterName : paymentDetailParameters) { - final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element); - baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull() - .notExceedingLengthOf(50); - } - } - public void validateTransactionWithNoAmount(final String json) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); @@ -705,17 +636,6 @@ public void validateLoanGroupIsActive(final Loan loan) { } } - public void validateLoanStatusIsActiveOrFullyPaidOrOverpaid(final Loan loan) { - if (!(loan.isOpen() || loan.isClosedObligationsMet() || loan.isOverPaid())) { - final List dataValidationErrors = new ArrayList<>(); - final String defaultUserMessage = "Loan must be Active, Fully Paid or Overpaid"; - final ApiParameterError error = ApiParameterError.generalError("error.msg.loan.must.be.active.fully.paid.or.overpaid", - defaultUserMessage); - dataValidationErrors.add(error); - throw new PlatformApiDataValidationException(dataValidationErrors); - } - } - public void validateLoanHasNoLaterChargeRefundTransactionToReverseOrCreateATransaction(Loan loan, LocalDate transactionDate, String reversedOrCreated) { for (LoanTransaction txn : loan.getLoanTransactions()) { @@ -834,7 +754,7 @@ public void validateLoanTransactionInterestPaymentWaiver(JsonCommand command) { validateLoanClientIsActive(loan); validateLoanHasCurrency(loan); validateLoanGroupIsActive(loan); - validateLoanStatusIsActiveOrFullyPaidOrOverpaid(loan); + loanDownPaymentTransactionValidator.validateLoanStatusIsActiveOrFullyPaidOrOverpaid(loan); validateLoanDisbursementIsBeforeTransactionDate(loan, transactionDate); validateLoanHasNoLaterChargeRefundTransactionToReverseOrCreateATransaction(loan, transactionDate, "created"); @@ -868,7 +788,7 @@ public void validateRefund(String json) { public void validateRefund(final Loan loan, LoanTransactionType loanTransactionType, final LocalDate transactionDate, ScheduleGeneratorDTO scheduleGeneratorDTO) { checkClientOrGroupActive(loan); - validateLoanStatusIsActiveOrFullyPaidOrOverpaid(loan); + loanDownPaymentTransactionValidator.validateLoanStatusIsActiveOrFullyPaidOrOverpaid(loan); validateActivityNotBeforeClientOrGroupTransferDate(loan, transactionDate); validateRepaymentTypeTransactionNotBeforeAChargeRefund(loan, loanTransactionType, transactionDate); validateTransactionNotBeforeLastTransactionDate(loan, loanTransactionType, transactionDate); @@ -880,6 +800,165 @@ public void validateRefund(final Loan loan, LoanTransactionType loanTransactionT validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(loan); } + public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final Loan loan, final LoanTransactionType loanTransactionType, + final LocalDate transactionDate) { + if (loanTransactionType.isRepaymentType() && !loanTransactionType.isChargeRefund()) { + for (LoanTransaction txn : loan.getLoanTransactions()) { + if (txn.isChargeRefund() && DateUtils.isBefore(transactionDate, txn.getTransactionDate())) { + final String errorMessage = "loan.transaction.cant.be.created.because.later.charge.refund.exists"; + final String details = "Loan Transaction: " + loan.getId() + " Can't be created because a Later Charge Refund Exists."; + throw new LoanChargeRefundException(errorMessage, details); + } + } + } + } + + public void validateRefundDateIsAfterLastRepayment(final Loan loan, final LocalDate refundTransactionDate) { + final LocalDate possibleNextRefundDate = loan.possibleNextRefundDate(); + + if (possibleNextRefundDate == null || DateUtils.isBefore(refundTransactionDate, possibleNextRefundDate)) { + throw new InvalidRefundDateException(refundTransactionDate.toString()); + } + } + + public void validateActivityNotBeforeClientOrGroupTransferDate(final Loan loan, final LoanEvent event, final LocalDate activityDate) { + if (loan.getClient() != null && loan.getClient().getOfficeJoiningDate() != null) { + final LocalDate clientOfficeJoiningDate = loan.getClient().getOfficeJoiningDate(); + if (DateUtils.isBefore(activityDate, clientOfficeJoiningDate)) { + String errorMessage = null; + String action = null; + String postfix = null; + switch (event) { + case LOAN_APPROVED -> { + errorMessage = "The date on which a loan is approved cannot be earlier than client's transfer date to this office"; + action = "approval"; + postfix = "cannot.be.before.client.transfer.date"; + } + case LOAN_APPROVAL_UNDO -> { + errorMessage = "The date on which a loan is approved cannot be earlier than client's transfer date to this office"; + action = "approval"; + postfix = "cannot.be.undone.before.client.transfer.date"; + } + case LOAN_DISBURSED -> { + errorMessage = "The date on which a loan is disbursed cannot be earlier than client's transfer date to this office"; + action = "disbursal"; + postfix = "cannot.be.before.client.transfer.date"; + } + case LOAN_DISBURSAL_UNDO -> { + errorMessage = "Cannot undo a disbursal done in another branch"; + action = "disbursal"; + postfix = "cannot.be.undone.before.client.transfer.date"; + } + case LOAN_REPAYMENT_OR_WAIVER -> { + errorMessage = "The date on which a repayment or waiver is made cannot be earlier than client's transfer date to this office"; + action = "repayment.or.waiver"; + postfix = "cannot.be.made.before.client.transfer.date"; + } + case WRITE_OFF_OUTSTANDING -> { + errorMessage = "The date on which a write off is made cannot be earlier than client's transfer date to this office"; + action = "writeoff"; + postfix = "cannot.be.undone.before.client.transfer.date"; + } + case REPAID_IN_FULL -> { + errorMessage = "The date on which the loan is repaid in full cannot be earlier than client's transfer date to this office"; + action = "close"; + postfix = "cannot.be.undone.before.client.transfer.date"; + } + case LOAN_CHARGE_PAYMENT -> { + errorMessage = "The date on which a charge payment is made cannot be earlier than client's transfer date to this office"; + action = "charge.payment"; + postfix = "cannot.be.made.before.client.transfer.date"; + } + case LOAN_REFUND -> { + errorMessage = "The date on which a refund is made cannot be earlier than client's transfer date to this office"; + action = "refund"; + postfix = "cannot.be.made.before.client.transfer.date"; + } + case LOAN_DISBURSAL_UNDO_LAST -> { + errorMessage = "Cannot undo a last disbursal in another branch"; + action = "disbursal"; + postfix = "cannot.be.undone.before.client.transfer.date"; + } + default -> { + } + } + throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, clientOfficeJoiningDate); + } + } + } + + private static @NotNull BigDecimal collectTotalCollateral(Set 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 getDisbursementParameters(boolean isAccountTransfer) { + Set disbursementParameters; + + if (isAccountTransfer) { + disbursementParameters = new HashSet<>(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale", "dateFormat", + LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.fixedEmiAmountParameterName, + LoanApiConstants.disbursementNetDisbursalAmountParameterName)); + } else { + disbursementParameters = new HashSet<>(Arrays.asList("actualDisbursementDate", "externalId", "note", "locale", "dateFormat", + "paymentTypeId", "accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber", "adjustRepaymentDate", + LoanApiConstants.principalDisbursedParameterName, LoanApiConstants.fixedEmiAmountParameterName, + LoanApiConstants.postDatedChecks, LoanApiConstants.disbursementNetDisbursalAmountParameterName)); + } + return disbursementParameters; + } + + private void validatePaymentTransaction(String json) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Set transactionParameters = new HashSet<>( + Arrays.asList("transactionDate", "transactionAmount", "externalId", "note", "locale", "dateFormat", "paymentTypeId", + "accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber", "loanId")); + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, transactionParameters); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource("loan.transaction"); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + final LocalDate transactionDate = this.fromApiJsonHelper.extractLocalDateNamed("transactionDate", element); + baseDataValidator.reset().parameter("transactionDate").value(transactionDate).notNull(); + + final BigDecimal transactionAmount = this.fromApiJsonHelper.extractBigDecimalWithLocaleNamed("transactionAmount", element); + baseDataValidator.reset().parameter("transactionAmount").value(transactionAmount).notNull().positiveAmount(); + + final String note = this.fromApiJsonHelper.extractStringNamed("note", element); + baseDataValidator.reset().parameter("note").value(note).notExceedingLengthOf(1000); + + validatePaymentDetails(baseDataValidator, element); + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + private void validatePaymentDetails(final DataValidatorBuilder baseDataValidator, final JsonElement element) { + // Validate all string payment detail fields for max length + final Integer paymentTypeId = this.fromApiJsonHelper.extractIntegerWithLocaleNamed("paymentTypeId", element); + + baseDataValidator.reset().parameter("paymentTypeId").value(paymentTypeId).ignoreIfNull().integerGreaterThanZero(); + + final Set paymentDetailParameters = new HashSet<>( + Arrays.asList("accountNumber", "checkNumber", "routingCode", "receiptNumber", "bankNumber")); + for (final String paymentDetailParameterName : paymentDetailParameters) { + final String paymentDetailParameterValue = this.fromApiJsonHelper.extractStringNamed(paymentDetailParameterName, element); + baseDataValidator.reset().parameter(paymentDetailParameterName).value(paymentDetailParameterValue).ignoreIfNull() + .notExceedingLengthOf(50); + } + } + private void checkClientOrGroupActive(final Loan loan) { final Client client = loan.client(); if (client != null && client.isNotActive()) { @@ -903,19 +982,6 @@ private void validateActivityNotBeforeClientOrGroupTransferDate(final Loan loan, } } - public void validateRepaymentTypeTransactionNotBeforeAChargeRefund(final Loan loan, final LoanTransactionType loanTransactionType, - final LocalDate transactionDate) { - if (loanTransactionType.isRepaymentType() && !loanTransactionType.isChargeRefund()) { - for (LoanTransaction txn : loan.getLoanTransactions()) { - if (txn.isChargeRefund() && DateUtils.isBefore(transactionDate, txn.getTransactionDate())) { - final String errorMessage = "loan.transaction.cant.be.created.because.later.charge.refund.exists"; - final String details = "Loan Transaction: " + loan.getId() + " Can't be created because a Later Charge Refund Exists."; - throw new LoanChargeRefundException(errorMessage, details); - } - } - } - } - private void validateTransactionNotBeforeLastTransactionDate(final Loan loan, LoanTransactionType loanTransactionType, final LocalDate transactionDate) { if (!((LoanScheduleType.CUMULATIVE.equals(loan.getLoanProductRelatedDetail().getLoanScheduleType()) @@ -947,4 +1013,26 @@ private void validateTransactionNotBeforeLastTransactionDate(final Loan loan, Lo throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, lastTransactionDate); } } + + public void validateIfTransactionIsChargeback(final LoanTransaction chargebackTransaction) { + if (!chargebackTransaction.isChargeback()) { + final String errorMessage = "A transaction of type chargeback was expected but not received."; + throw new InvalidLoanTransactionTypeException("transaction", "is.not.a.chargeback.transaction", errorMessage); + } + } + + public void validateLoanRescheduleDate(final Loan loan) { + if (DateUtils.isBefore(loan.getRescheduledOnDate(), loan.getDisbursementDate())) { + final String errorMessage = "The date on which a loan is rescheduled cannot be before the loan disbursement date: " + + loan.getDisbursementDate().toString(); + throw new InvalidLoanStateTransitionException("close.reschedule", "cannot.be.before.submittal.date", errorMessage, + loan.getRescheduledOnDate(), loan.getDisbursementDate()); + } + + if (DateUtils.isDateInTheFuture(loan.getRescheduledOnDate())) { + final String errorMessage = "The date on which a loan is rescheduled cannot be in the future."; + throw new InvalidLoanStateTransitionException("close.reschedule", "cannot.be.a.future.date", errorMessage, + loan.getRescheduledOnDate()); + } + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java index daf83a7ca9b..36d06724334 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualActivityProcessingServiceImpl.java @@ -35,6 +35,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.springframework.stereotype.Component; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @@ -48,6 +49,7 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi private final LoanWritePlatformService loanWritePlatformService; private final ExternalIdFactory externalIdFactory; private final BusinessEventNotifierService businessEventNotifierService; + private final LoanChargeValidator loanChargeValidator; @Override @Transactional(propagation = Propagation.REQUIRES_NEW) @@ -92,6 +94,7 @@ public void processAccrualActivityForLoanClosure(Loan loan) { } private void reverseAccrualActivityTransaction(LoanTransaction loanTransaction) { + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, "reversed"); loanTransaction.reverse(); LoanAdjustTransactionBusinessEvent.Data data = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction); businessEventNotifierService.notifyPostBusinessEvent(new LoanAdjustTransactionBusinessEvent(data)); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java index d2d40343c4a..bb867e29970 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAccrualsProcessingServiceImpl.java @@ -56,12 +56,14 @@ import org.apache.fineract.infrastructure.jobs.exception.JobExecutionException; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.organisation.office.domain.OfficeRepository; import org.apache.fineract.portfolio.loanaccount.data.AccrualChargeData; import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodData; import org.apache.fineract.portfolio.loanaccount.data.AccrualPeriodsData; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; +import org.apache.fineract.portfolio.loanaccount.domain.LoanChargeRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalcualtionAdditionalDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanInterestRecalculationDetails; @@ -76,6 +78,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGenerator; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleGeneratorFactory; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanproduct.domain.InterestRecalculationCompoundingMethod; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.springframework.stereotype.Component; @@ -99,6 +102,9 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing private final LoanTransactionRepository loanTransactionRepository; private final LoanScheduleGeneratorFactory loanScheduleFactory; private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory; + private final OfficeRepository officeRepository; + private final LoanChargeRepository loanChargeRepository; + private final LoanChargeValidator loanChargeValidator; /** * method adds accrual for batch job "Add Periodic Accrual Transactions" and add accruals api for Loan @@ -735,6 +741,8 @@ private void reprocessNonPeriodicAccruals(Loan loan, final List for (LoanTransaction loanTransaction : accruals) { if (loanTransaction.getInterestPortion(loan.getCurrency()).isGreaterThanZero()) { if (loanTransaction.getInterestPortion(loan.getCurrency()).isNotEqualTo(interestApplied)) { + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, + "reversed"); loanTransaction.reverse(); if (isExternalIdAutoGenerationEnabled) { externalId = ExternalId.generate(); @@ -749,6 +757,8 @@ private void reprocessNonPeriodicAccruals(Loan loan, final List LoanCharge loanCharge = chargePaidBy.getLoanCharge(); Money chargeAmount = loanCharge.getAmount(loan.getCurrency()); if (chargeAmount.isNotEqualTo(loanTransaction.getAmount(loan.getCurrency()))) { + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), + loanTransaction, "reversed"); loanTransaction.reverse(); loan.handleChargeAppliedTransaction(loanCharge, loanTransaction.getTransactionDate()); } @@ -776,6 +786,8 @@ private void checkAndUpdateAccrualsForInstallment(Loan loan, List transactions, Loc boolean reversed = false; for (LoanTransaction loanTransaction : transactions) { if (DateUtils.isAfter(loanTransaction.getTransactionDate(), effectiveDate)) { + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, + "reversed"); reverseAccrual(loanTransaction); reversed = true; } @@ -1187,6 +1205,8 @@ private boolean reverseTransactionsOnOrAfter(List transactions, boolean reversed = false; for (LoanTransaction loanTransaction : transactions) { if (!DateUtils.isBefore(loanTransaction.getTransactionDate(), date)) { + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, + "reversed"); reverseAccrual(loanTransaction); reversed = true; } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java index 1101a259423..f17de8edefc 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java @@ -85,6 +85,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleAssembler; import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator; import org.apache.fineract.portfolio.loanproduct.LoanProductConstants; import org.apache.fineract.portfolio.loanproduct.domain.RecalculationFrequencyType; import org.apache.fineract.portfolio.note.domain.Note; @@ -124,6 +125,8 @@ public class LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa private final GSIMReadPlatformService gsimReadPlatformService; private final LoanLifecycleStateMachine defaultLoanLifecycleStateMachine; private final LoanAccrualsProcessingService loanAccrualsProcessingService; + private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator; + private final LoanScheduleService loanScheduleService; @Transactional @Override @@ -615,6 +618,7 @@ public CommandProcessingResult undoApplicationApproval(final Long loanId, final Loan loan = retrieveLoanBy(loanId); loanApplicationTransitionValidator.checkClientOrGroupActive(loan); + loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_APPROVAL_UNDO); final Map changes = loan.undoApproval(defaultLoanLifecycleStateMachine); if (!changes.isEmpty()) { @@ -625,7 +629,7 @@ public CommandProcessingResult undoApplicationApproval(final Long loanId, final || changes.containsKey(LoanApiConstants.disbursementPrincipalParameterName)) { LocalDate recalculateFrom = null; ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - loan.regenerateRepaymentSchedule(scheduleGeneratorDTO); + loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); loanAccrualsProcessingService.reprocessExistingAccruals(loan); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java index 214bb16d3d2..5a683baa8d1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssemblerImpl.java @@ -135,6 +135,9 @@ public class LoanAssemblerImpl implements LoanAssembler { private final LoanChargeMapper loanChargeMapper; private final LoanCollateralManagementMapper loanCollateralManagementMapper; private final LoanAccrualsProcessingService loanAccrualsProcessingService; + private final LoanDisbursementService loanDisbursementService; + private final LoanChargeService loanChargeService; + private final LoanOfficerService loanOfficerService; @Override public Loan assembleFrom(final Long accountId) { @@ -276,7 +279,7 @@ public Loan assembleFrom(final JsonCommand command) { loanApplication.setHelpers(defaultLoanLifecycleStateMachine, this.loanSummaryWrapper, this.loanRepaymentScheduleTransactionProcessorFactory); // TODO: review - loanApplication.recalculateAllCharges(); + loanChargeService.recalculateAllCharges(loanApplication); topUpLoanConfiguration(element, loanApplication); loanAccrualsProcessingService.reprocessExistingAccruals(loanApplication); return loanApplication; @@ -608,7 +611,7 @@ public Map updateFrom(JsonCommand command, Loan loan) { final Long newValue = command.longValueOfParameterNamed(LoanApiConstants.loanOfficerIdParameterName); changes.put(LoanApiConstants.loanOfficerIdParameterName, newValue); final Staff newOfficer = findLoanOfficerByIdIfProvided(newValue); - loan.updateLoanOfficerOnLoanApplication(newOfficer); + loanOfficerService.updateLoanOfficerOnLoanApplication(loan, newOfficer); } Long existingLoanPurposeId = null; @@ -718,7 +721,7 @@ public Map updateFrom(JsonCommand command, Loan loan) { } if (loanProduct.isMultiDisburseLoan()) { - loan.updateDisbursementDetails(command, changes); + loanDisbursementService.updateDisbursementDetails(loan, command, changes); if (command.isChangeInBigDecimalParameterNamed(LoanApiConstants.maxOutstandingBalanceParameterName, loan.getMaxOutstandingLoanBalance())) { loan.setMaxOutstandingLoanBalance( @@ -841,7 +844,7 @@ public Map updateFrom(JsonCommand command, Loan loan) { final LoanScheduleModel loanSchedule = this.calculationPlatformService.calculateLoanSchedule(query, false); loan.updateLoanSchedule(loanSchedule); loanAccrualsProcessingService.reprocessExistingAccruals(loan); - loan.recalculateAllCharges(); + loanChargeService.recalculateAllCharges(loan); } // Changes to modify loan rates. diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java index d69d78c3cc8..3e3b8dee0fd 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImpl.java @@ -105,6 +105,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanOverdueInstallmentCharge; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheDisbursementCharge; @@ -127,6 +128,8 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.ScheduledDateGenerator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator; import org.apache.fineract.portfolio.loanproduct.data.LoanOverdueDTO; import org.apache.fineract.portfolio.loanproduct.exception.LinkedAccountRequiredException; import org.apache.fineract.portfolio.note.domain.Note; @@ -167,6 +170,9 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo private final NoteRepository noteRepository; private final LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService; private final LoanAccrualsProcessingService loanAccrualsProcessingService; + private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator; + private final LoanChargeValidator loanChargeValidator; + private final LoanScheduleService loanScheduleService; private static boolean isPartOfThisInstallment(LoanCharge loanCharge, LoanRepaymentScheduleInstallment e) { return DateUtils.isAfter(loanCharge.getDueDate(), e.getFromDate()) && !DateUtils.isAfter(loanCharge.getDueDate(), e.getDueDate()); @@ -454,6 +460,7 @@ public CommandProcessingResult updateLoanCharge(final Long loanId, final Long lo } businessEventNotifierService.notifyPreBusinessEvent(new LoanUpdateChargeBusinessEvent(loanCharge)); + loanChargeValidator.validateLoanIsNotClosed(loan, loanCharge); final Map changes = loan.updateLoanCharge(loanCharge, command); this.loanRepositoryWrapper.save(loan); @@ -538,7 +545,8 @@ public CommandProcessingResult waiveLoanCharge(final Long loanId, final Long loa } } - final LoanTransaction waiveTransaction = loan.waiveLoanCharge(loanCharge, defaultLoanLifecycleStateMachine, changes, + loanChargeValidator.validateLoanIsNotClosed(loan, loanCharge); + final LoanTransaction waiveTransaction = waiveLoanCharge(loan, loanCharge, defaultLoanLifecycleStateMachine, changes, existingTransactionIds, existingReversedTransactionIds, loanInstallmentNumber, scheduleGeneratorDTO, accruedCharge, externalId); @@ -588,6 +596,8 @@ public CommandProcessingResult deleteLoanCharge(final Long loanId, final Long lo } businessEventNotifierService.notifyPreBusinessEvent(new LoanDeleteChargeBusinessEvent(loanCharge)); + loanChargeValidator.validateLoanIsNotClosed(loan, loanCharge); + loanChargeValidator.validateLoanChargeIsNotWaived(loan, loanCharge); loan.removeLoanCharge(loanCharge); this.loanRepositoryWrapper.save(loan); businessEventNotifierService.notifyPostBusinessEvent(new LoanDeleteChargeBusinessEvent(loanCharge)); @@ -890,6 +900,8 @@ private void undoInstalmentFee(Map changes, Loan loan, LoanTrans throw new LoanChargeWaiveCannotBeReversedException( LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.NOT_WAIVED, loanCharge.getId()); } + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, + "reversed"); // Reverse waived transaction loanTransaction.reverse(); // Set manually adjusted value to `1` @@ -936,6 +948,7 @@ private void undoSpecifiedDueDateCharge(final Map changes, final throw new LoanChargeWaiveCannotBeReversedException( LoanChargeWaiveCannotBeReversedException.LoanChargeWaiveCannotUndoReason.NOT_WAIVED, loanCharge.getId()); } + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, "reversed"); loanTransaction.reverse(); loanTransaction.setManuallyAdjustedOrReversed(); loanCharge.setOutstandingAmount(loanCharge.amountOutstanding().add(amountWaived)); @@ -1025,6 +1038,8 @@ private boolean addCharge(final Loan loan, final Charge chargeDefinition, LoanCh } } + loanChargeValidator.validateChargeAdditionForDisbursedLoan(loan, loanCharge); + loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate()); loan.addLoanCharge(loanCharge); loanCharge = this.loanChargeRepository.saveAndFlush(loanCharge); @@ -1139,8 +1154,8 @@ public Loan runScheduleRecalculation(Loan loan, final LocalDate recalculateFrom) if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { final List existingTransactionIds = loan.findExistingTransactionIds(); ScheduleGeneratorDTO generatorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - ChangedTransactionDetail changedTransactionDetail = loan - .handleRegenerateRepaymentScheduleWithInterestRecalculation(generatorDTO); + ChangedTransactionDetail changedTransactionDetail = loanScheduleService + .handleRegenerateRepaymentScheduleWithInterestRecalculation(loan, generatorDTO); loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); @@ -1313,7 +1328,7 @@ private void loanChargeAdjustmentEntranceValidation(final LoanCharge loanCharge, "Transaction amount cannot be higher than the available charge amount for adjustment: " + availableAmountForAdjustment); } checkClientOrGroupActive(loan); - loan.validateAccountStatus(LoanEvent.LOAN_CHARGE_ADJUSTMENT); + loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_CHARGE_ADJUSTMENT); } private BigDecimal calculateAvailableAmountForChargeAdjustment(final LoanCharge loanCharge) { @@ -1356,4 +1371,95 @@ private void checkClientOrGroupActive(final Loan loan) { } } } + + public LoanTransaction waiveLoanCharge(final Loan loan, final LoanCharge loanCharge, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map changes, + final List existingTransactionIds, final List existingReversedTransactionIds, final Integer loanInstallmentNumber, + final ScheduleGeneratorDTO scheduleGeneratorDTO, final Money accruedCharge, final ExternalId externalId) { + final Money amountWaived = loanCharge.waive(loan.getCurrency(), loanInstallmentNumber); + changes.put("amount", amountWaived.getAmount()); + + Money unrecognizedIncome = amountWaived.zero(); + Money chargeComponent = amountWaived; + if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { + Money receivableCharge; + if (loanInstallmentNumber != null) { + receivableCharge = accruedCharge + .minus(loanCharge.getInstallmentLoanCharge(loanInstallmentNumber).getAmountPaid(loan.getCurrency())); + } else { + receivableCharge = accruedCharge.minus(loanCharge.getAmountPaid(loan.getCurrency())); + } + if (receivableCharge.isLessThanZero()) { + receivableCharge = amountWaived.zero(); + } + if (amountWaived.isGreaterThan(receivableCharge)) { + chargeComponent = receivableCharge; + unrecognizedIncome = amountWaived.minus(receivableCharge); + } + } + Money feeChargesWaived = chargeComponent; + Money penaltyChargesWaived = Money.zero(loan.getCurrency()); + if (loanCharge.isPenaltyCharge()) { + penaltyChargesWaived = chargeComponent; + feeChargesWaived = Money.zero(loan.getCurrency()); + } + + LocalDate transactionDate = loan.getDisbursementDate(); + final LocalDate businessDate = DateUtils.getBusinessLocalDate(); + if (loanCharge.isDueDateCharge()) { + if (DateUtils.isAfter(loanCharge.getDueLocalDate(), businessDate)) { + transactionDate = businessDate; + } else { + transactionDate = loanCharge.getDueLocalDate(); + } + } else if (loanCharge.isInstalmentFee()) { + LocalDate repaymentDueDate = loanCharge.getInstallmentLoanCharge(loanInstallmentNumber).getRepaymentInstallment().getDueDate(); + if (DateUtils.isAfter(repaymentDueDate, businessDate)) { + transactionDate = businessDate; + } else { + transactionDate = repaymentDueDate; + } + } + + scheduleGeneratorDTO.setRecalculateFrom(transactionDate); + + loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement()); + + existingTransactionIds.addAll(loan.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); + + final LoanTransaction waiveLoanChargeTransaction = LoanTransaction.waiveLoanCharge(loan, loan.getOffice(), amountWaived, + transactionDate, feeChargesWaived, penaltyChargesWaived, unrecognizedIncome, externalId); + final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(waiveLoanChargeTransaction, loanCharge, + waiveLoanChargeTransaction.getAmount(loan.getCurrency()).getAmount(), loanInstallmentNumber); + waiveLoanChargeTransaction.getLoanChargesPaid().add(loanChargePaidBy); + loan.addLoanTransaction(waiveLoanChargeTransaction); + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled() + && DateUtils.isBefore(loanCharge.getDueLocalDate(), businessDate)) { + loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); + } + // Waive of charges whose due date falls after latest 'repayment' transaction don't require entire loan schedule + // to be reprocessed. + if (!loanCharge.isDueAtDisbursement() && loanCharge.isPaidOrPartiallyPaid(loan.getCurrency())) { + /* + * TODO Vishwas Currently we do not allow waiving fully paid loan charge and waiving partially paid loan + * charges only waives the remaining amount. + * + * Consider removing this block of code or logically completing it for the future by getting the list of + * affected Transactions + */ + loan.reprocessTransactions(); + } else { + // reprocess loan schedule based on charge been waived. + final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper(); + wrapper.reprocess(loan.getCurrency(), loan.getDisbursementDate(), loan.getRepaymentScheduleInstallments(), + loan.getActiveCharges()); + } + + loan.updateLoanSummaryDerivedFields(); + + loan.doPostLoanTransactionChecks(waiveLoanChargeTransaction.getTransactionDate(), loanLifecycleStateMachine); + + return waiveLoanChargeTransaction; + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java new file mode 100644 index 00000000000..e45bf68328f --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanDisbursementService.java @@ -0,0 +1,300 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.RECALCULATE_LOAN_SCHEDULE; + +import com.google.common.base.Splitter; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import jakarta.validation.constraints.NotNull; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.util.Collection; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.Objects; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.infrastructure.core.serialization.JsonParserHelper; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.organisation.monetary.domain.Money; +import org.apache.fineract.portfolio.charge.domain.Charge; +import org.apache.fineract.portfolio.charge.domain.ChargeTimeType; +import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy; +import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTrancheDisbursementCharge; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanDisbursementValidator; +import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; + +@RequiredArgsConstructor +public class LoanDisbursementService { + + private final LoanChargeValidator loanChargeValidator; + private final LoanDisbursementValidator loanDisbursementValidator; + + public void updateDisbursementDetails(final Loan loan, final JsonCommand jsonCommand, final Map actualChanges) { + final List disbursementList = loan.fetchDisbursementIds(); + final List loanChargeIds = loan.fetchLoanTrancheChargeIds(); + final int chargeIdLength = loanChargeIds.size(); + String chargeIds; + // From modify application page, if user removes all charges, we should + // get empty array. + // So we need to remove all charges applied for this loan + final boolean removeAllCharges = jsonCommand.parameterExists(LoanApiConstants.chargesParameterName) + && jsonCommand.arrayOfParameterNamed(LoanApiConstants.chargesParameterName).isEmpty(); + + if (jsonCommand.parameterExists(LoanApiConstants.disbursementDataParameterName)) { + final JsonArray disbursementDataArray = jsonCommand.arrayOfParameterNamed(LoanApiConstants.disbursementDataParameterName); + if (disbursementDataArray != null && !disbursementDataArray.isEmpty()) { + String dateFormat; + Locale locale = null; + final Map dateAndLocale = loan.getDateFormatAndLocale(jsonCommand); + dateFormat = dateAndLocale.get(LoanApiConstants.dateFormatParameterName); + if (dateAndLocale.containsKey(LoanApiConstants.localeParameterName)) { + locale = JsonParserHelper.localeFromString(dateAndLocale.get(LoanApiConstants.localeParameterName)); + } + for (JsonElement jsonElement : disbursementDataArray) { + final JsonObject jsonObject = jsonElement.getAsJsonObject(); + final Map parsedDisbursementData = loan.parseDisbursementDetails(jsonObject, dateFormat, locale); + final LocalDate expectedDisbursementDate = (LocalDate) parsedDisbursementData + .get(LoanApiConstants.expectedDisbursementDateParameterName); + final BigDecimal principal = (BigDecimal) parsedDisbursementData + .get(LoanApiConstants.disbursementPrincipalParameterName); + final Long disbursementID = (Long) parsedDisbursementData.get(LoanApiConstants.disbursementIdParameterName); + chargeIds = (String) parsedDisbursementData.get(LoanApiConstants.loanChargeIdParameterName); + if (chargeIds != null) { + if (chargeIds.contains(",")) { + final Iterable chargeId = Splitter.on(',').split(chargeIds); + for (String loanChargeId : chargeId) { + loanChargeIds.remove(Long.parseLong(loanChargeId)); + } + } else { + loanChargeIds.remove(Long.parseLong(chargeIds)); + } + } + createOrUpdateDisbursementDetails(loan, disbursementID, actualChanges, expectedDisbursementDate, principal, + disbursementList); + } + removeDisbursementAndAssociatedCharges(loan, actualChanges, disbursementList, loanChargeIds, chargeIdLength, + removeAllCharges); + } + } + } + + public Money adjustDisburseAmount(final Loan loan, @NotNull final JsonCommand command, + @NotNull final LocalDate actualDisbursementDate) { + Money disburseAmount = loan.getLoanRepaymentScheduleDetail().getPrincipal().zero(); + final BigDecimal principalDisbursed = command.bigDecimalValueOfParameterNamed(LoanApiConstants.principalDisbursedParameterName); + if (loan.getActualDisbursementDate() == null || DateUtils.isBefore(actualDisbursementDate, loan.getActualDisbursementDate())) { + loan.setActualDisbursementDate(actualDisbursementDate); + } + BigDecimal diff = BigDecimal.ZERO; + final Collection details = loan.fetchUndisbursedDetail(); + if (principalDisbursed == null) { + disburseAmount = loan.getLoanRepaymentScheduleDetail().getPrincipal(); + if (!details.isEmpty()) { + disburseAmount = disburseAmount.zero(); + for (LoanDisbursementDetails disbursementDetails : details) { + disbursementDetails.updateActualDisbursementDate(actualDisbursementDate); + disburseAmount = disburseAmount.plus(disbursementDetails.principal()); + } + } + } else { + if (loan.getLoanProduct().isMultiDisburseLoan()) { + disburseAmount = Money.of(loan.getCurrency(), principalDisbursed); + } else { + disburseAmount = disburseAmount.plus(principalDisbursed); + } + + if (details.isEmpty()) { + diff = loan.getLoanRepaymentScheduleDetail().getPrincipal().minus(principalDisbursed).getAmount(); + } else { + for (LoanDisbursementDetails disbursementDetails : details) { + disbursementDetails.updateActualDisbursementDate(actualDisbursementDate); + disbursementDetails.updatePrincipal(principalDisbursed); + } + } + if (loan.loanProduct().isMultiDisburseLoan()) { + Collection loanDisburseDetails = loan.getDisbursementDetails(); + BigDecimal setPrincipalAmount = BigDecimal.ZERO; + BigDecimal totalAmount = BigDecimal.ZERO; + for (LoanDisbursementDetails disbursementDetails : loanDisburseDetails) { + if (disbursementDetails.actualDisbursementDate() != null) { + setPrincipalAmount = setPrincipalAmount.add(disbursementDetails.principal()); + } + totalAmount = totalAmount.add(disbursementDetails.principal()); + } + loan.getLoanRepaymentScheduleDetail().setPrincipal(setPrincipalAmount); + loanDisbursementValidator.compareDisbursedToApprovedOrProposedPrincipal(loan, disburseAmount.getAmount(), totalAmount); + } else { + loan.getLoanRepaymentScheduleDetail() + .setPrincipal(loan.getLoanRepaymentScheduleDetail().getPrincipal().minus(diff).getAmount()); + } + loanDisbursementValidator.validateDisburseAmountNotExceedingApprovedAmount(loan, diff, principalDisbursed); + } + return disburseAmount; + } + + public void handleDisbursementTransaction(final Loan loan, final LocalDate disbursedOn, final PaymentDetail paymentDetail) { + // add repayment transaction to track incoming money from client to mfi + // for (charges due at time of disbursement) + + /* + * TODO Vishwas: do we need to be able to pass in payment type details for repayments at disbursements too? + */ + + final Money totalFeeChargesDueAtDisbursement = loan.getSummary().getTotalFeeChargesDueAtDisbursement(loan.getCurrency()); + /* + * all Charges repaid at disbursal is marked as repaid and "APPLY Charge" transactions are created for all other + * fees ( which are created during disbursal but not repaid) + */ + + Money disbursentMoney = Money.zero(loan.getCurrency()); + final LoanTransaction chargesPayment = LoanTransaction.repaymentAtDisbursement(loan.getOffice(), disbursentMoney, paymentDetail, + disbursedOn, null); + final Integer installmentNumber = null; + for (final LoanCharge charge : loan.getActiveCharges()) { + LocalDate actualDisbursementDate = loan.getActualDisbursementDate(charge); + /* + * create a Charge applied transaction if Up front Accrual, None or Cash based accounting is enabled + */ + if ((charge.getCharge().getChargeTimeType().equals(ChargeTimeType.DISBURSEMENT.getValue()) + && disbursedOn.equals(actualDisbursementDate) && !charge.isWaived() && !charge.isFullyPaid()) + || (charge.getCharge().getChargeTimeType().equals(ChargeTimeType.TRANCHE_DISBURSEMENT.getValue()) + && disbursedOn.equals(actualDisbursementDate) && !charge.isWaived() && !charge.isFullyPaid())) { + if (totalFeeChargesDueAtDisbursement.isGreaterThanZero() && !charge.getChargePaymentMode().isPaymentModeAccountTransfer()) { + charge.markAsFullyPaid(); + // Add "Loan Charge Paid By" details to this transaction + final LoanChargePaidBy loanChargePaidBy = new LoanChargePaidBy(chargesPayment, charge, charge.amount(), + installmentNumber); + chargesPayment.getLoanChargesPaid().add(loanChargePaidBy); + disbursentMoney = disbursentMoney.plus(charge.amount()); + } + } else if (disbursedOn.equals(loan.getActualDisbursementDate()) + && loan.isNoneOrCashOrUpfrontAccrualAccountingEnabledOnLoanProduct()) { + loan.handleChargeAppliedTransaction(charge, disbursedOn); + } + } + + if (disbursentMoney.isGreaterThanZero()) { + final Money zero = Money.zero(loan.getCurrency()); + chargesPayment.updateComponentsAndTotal(zero, zero, disbursentMoney, zero); + chargesPayment.updateLoan(loan); + loan.addLoanTransaction(chargesPayment); + loan.updateLoanOutstandingBalances(); + } + + final LocalDate expectedDate = loan.getExpectedFirstRepaymentOnDate(); + loanDisbursementValidator.validateDisburseDate(loan, disbursedOn, expectedDate); + } + + private void createOrUpdateDisbursementDetails(final Loan loan, final Long disbursementID, final Map actualChanges, + final LocalDate expectedDisbursementDate, final BigDecimal principal, final List existingDisbursementList) { + if (disbursementID != null) { + LoanDisbursementDetails loanDisbursementDetail = loan.fetchLoanDisbursementsById(disbursementID); + existingDisbursementList.remove(disbursementID); + if (loanDisbursementDetail.actualDisbursementDate() == null) { + LocalDate actualDisbursementDate = null; + LoanDisbursementDetails disbursementDetails = new LoanDisbursementDetails(expectedDisbursementDate, actualDisbursementDate, + principal, loan.getNetDisbursalAmount(), false); + disbursementDetails.updateLoan(loan); + if (!loanDisbursementDetail.equals(disbursementDetails)) { + loanDisbursementDetail.copy(disbursementDetails); + actualChanges.put("disbursementDetailId", disbursementID); + actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true); + } + } + } else { + final var disbursementDetails = loan.addLoanDisbursementDetails(expectedDisbursementDate, principal); + for (LoanTrancheCharge trancheCharge : loan.getTrancheCharges()) { + Charge chargeDefinition = trancheCharge.getCharge(); + ExternalId externalId = ExternalId.empty(); + if (TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } + final LoanCharge loanCharge = new LoanCharge(loan, chargeDefinition, principal, null, null, null, expectedDisbursementDate, + null, null, BigDecimal.ZERO, externalId); + LoanTrancheDisbursementCharge loanTrancheDisbursementCharge = new LoanTrancheDisbursementCharge(loanCharge, + disbursementDetails); + loanCharge.updateLoanTrancheDisbursementCharge(loanTrancheDisbursementCharge); + + loanChargeValidator.validateChargeAdditionForDisbursedLoan(loan, loanCharge); + loanChargeValidator.validateChargeHasValidSpecifiedDateIfApplicable(loan, loanCharge, loan.getDisbursementDate()); + loan.addLoanCharge(loanCharge); + } + actualChanges.put(LoanApiConstants.disbursementDataParameterName, expectedDisbursementDate + "-" + principal); + actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true); + } + } + + private void removeDisbursementAndAssociatedCharges(final Loan loan, final Map actualChanges, + final List disbursementList, final List loanChargeIds, final int chargeIdLength, final boolean removeAllCharges) { + if (removeAllCharges) { + final LoanCharge[] tempCharges = new LoanCharge[loan.getCharges().size()]; + loan.getCharges().toArray(tempCharges); + for (LoanCharge loanCharge : tempCharges) { + loanChargeValidator.validateLoanIsNotClosed(loan, loanCharge); + loanChargeValidator.validateLoanChargeIsNotWaived(loan, loanCharge); + loan.removeLoanCharge(loanCharge); + } + loan.getTrancheCharges().clear(); + } else { + if (!loanChargeIds.isEmpty() && loanChargeIds.size() != chargeIdLength) { + for (Long chargeId : loanChargeIds) { + final LoanCharge deleteCharge = loan.fetchLoanChargesById(chargeId); + if (loan.getCharges().contains(deleteCharge)) { + loanChargeValidator.validateLoanIsNotClosed(loan, deleteCharge); + loanChargeValidator.validateLoanChargeIsNotWaived(loan, deleteCharge); + loan.removeLoanCharge(deleteCharge); + } + } + } + } + for (Long id : disbursementList) { + removeChargesByDisbursementID(loan, id); + loan.removeDisbursementDetails(id); + actualChanges.put(RECALCULATE_LOAN_SCHEDULE, true); + } + } + + private void removeChargesByDisbursementID(final Loan loan, final Long id) { + loan.getCharges().stream() // + .filter(charge -> { // + final LoanTrancheDisbursementCharge transCharge = charge.getTrancheDisbursementCharge(); // + if (transCharge == null || !Objects.equals(id, transCharge.getloanDisbursementDetails().getId())) { + return false; + } + loanChargeValidator.validateLoanIsNotClosed(loan, charge); // + loanChargeValidator.validateLoanChargeIsNotWaived(loan, charge); // + return true; // + }) // + .forEach(loan::removeLoanCharge); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanOfficerService.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanOfficerService.java new file mode 100644 index 00000000000..9a793a6944d --- /dev/null +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanOfficerService.java @@ -0,0 +1,71 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanaccount.service; + +import java.time.LocalDate; +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.organisation.staff.domain.Staff; +import org.apache.fineract.portfolio.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanOfficerAssignmentHistory; +import org.apache.fineract.portfolio.loanaccount.exception.LoanOfficerAssignmentException; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanOfficerValidator; + +@RequiredArgsConstructor +public class LoanOfficerService { + + private final LoanOfficerValidator loanOfficerValidator; + + public void reassignLoanOfficer(final Loan loan, final Staff newLoanOfficer, final LocalDate assignmentDate) { + final Optional latestHistoryRecord = loan.findLatestIncompleteHistoryRecord(); + final LoanOfficerAssignmentHistory lastAssignmentRecord = loan.findLastAssignmentHistoryRecord(newLoanOfficer); + + // assignment date should not be less than loan submitted date + loanOfficerValidator.validateReassignment(loan, assignmentDate, lastAssignmentRecord); + loanOfficerValidator.validateAssignmentDateWithHistory(loan, latestHistoryRecord, assignmentDate); + + if (latestHistoryRecord.isPresent() && loan.getLoanOfficer().identifiedBy(newLoanOfficer)) { + latestHistoryRecord.get().updateStartDate(assignmentDate); + } else if (latestHistoryRecord.isPresent() && latestHistoryRecord.get().matchesStartDateOf(assignmentDate)) { + latestHistoryRecord.get().updateLoanOfficer(newLoanOfficer); + loan.setLoanOfficer(newLoanOfficer); + } else { + // loan officer correctly changed from previous loan officer to new loan officer + latestHistoryRecord.ifPresent(loanOfficerAssignmentHistory -> loanOfficerAssignmentHistory.updateEndDate(assignmentDate)); + + loan.setLoanOfficer(newLoanOfficer); + if (loan.isNotSubmittedAndPendingApproval()) { + final LoanOfficerAssignmentHistory loanOfficerAssignmentHistory = LoanOfficerAssignmentHistory.createNew(loan, + loan.getLoanOfficer(), assignmentDate); + loan.getLoanOfficerHistory().add(loanOfficerAssignmentHistory); + } + } + } + + public void updateLoanOfficerOnLoanApplication(final Loan loan, final Staff newLoanOfficer) { + if (!loan.isSubmittedAndPendingApproval()) { + Long loanOfficerId = null; + if (loan.getLoanOfficer() != null) { + loanOfficerId = loan.getLoanOfficer().getId(); + } + throw new LoanOfficerAssignmentException(loan.getId(), loanOfficerId); + } + loan.setLoanOfficer(newLoanOfficer); + } +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java index 6d951c04724..43fa5368826 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanReadPlatformServiceImpl.java @@ -118,6 +118,7 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.data.OverdueLoanScheduleData; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanForeclosureValidator; import org.apache.fineract.portfolio.loanproduct.data.LoanProductData; import org.apache.fineract.portfolio.loanproduct.data.TransactionProcessingStrategyData; import org.apache.fineract.portfolio.loanproduct.domain.InterestMethod; @@ -170,6 +171,7 @@ public class LoanReadPlatformServiceImpl implements LoanReadPlatformService, Loa private final LoanTransactionRepository loanTransactionRepository; private final LoanChargePaidByReadService loanChargePaidByReadService; private final LoanTransactionRelationReadService loanTransactionRelationReadService; + private final LoanForeclosureValidator loanForeclosureValidator; @Override public LoanAccountData retrieveOne(final Long loanId) { @@ -2028,7 +2030,7 @@ public LoanTransactionData retrieveLoanForeclosureTemplate(final Long loanId, fi this.context.authenticatedUser(); final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId, true); - loan.validateForForeclosure(transactionDate); + loanForeclosureValidator.validateForForeclosure(loan, transactionDate); final MonetaryCurrency currency = loan.getCurrency(); final ApplicationCurrency applicationCurrency = this.applicationCurrencyRepository.findOneWithNotFoundDetection(currency); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java index 8fd879d5fa5..38fe5ccbcd9 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImpl.java @@ -18,6 +18,13 @@ */ package org.apache.fineract.portfolio.loanaccount.service; +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.ACTUAL_DISBURSEMENT_DATE; +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.CLOSED_ON_DATE; +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.EXTERNAL_ID; +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.PARAM_STATUS; +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.TRANSACTION_DATE; +import static org.apache.fineract.portfolio.loanaccount.domain.Loan.WRITTEN_OFF_ON_DATE; + import com.google.common.collect.Lists; import com.google.gson.JsonArray; import com.google.gson.JsonElement; @@ -30,12 +37,15 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; +import java.util.Comparator; import java.util.HashMap; import java.util.LinkedHashMap; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Objects; +import java.util.Optional; import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -156,6 +166,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleTransactionProcessorFactory; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; @@ -175,6 +186,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException; import org.apache.fineract.portfolio.loanaccount.exception.ExceedingTrancheCountException; +import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanTransactionTypeException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidPaidInAdvanceAmountException; import org.apache.fineract.portfolio.loanaccount.exception.LoanForeclosureException; @@ -184,6 +196,7 @@ import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException; import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataNotAllowedException; import org.apache.fineract.portfolio.loanaccount.exception.MultiDisbursementDataRequiredException; +import org.apache.fineract.portfolio.loanaccount.exception.UndoLastTrancheDisbursementException; import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDomainService; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel; import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModelPeriod; @@ -191,6 +204,9 @@ import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService; import org.apache.fineract.portfolio.loanaccount.rescheduleloan.domain.LoanRescheduleRequest; import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanOfficerValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer; import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; @@ -263,6 +279,12 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf private final AccountTransferRepository accountTransferRepository; private final LoanTransactionAssembler loanTransactionAssembler; private final LoanAccrualsProcessingService loanAccrualsProcessingService; + private final LoanOfficerValidator loanOfficerValidator; + private final LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator; + private final LoanDisbursementService loanDisbursementService; + private final LoanScheduleService loanScheduleService; + private final LoanChargeValidator loanChargeValidator; + private final LoanOfficerService loanOfficerService; @Transactional @Override @@ -354,7 +376,7 @@ public CommandProcessingResult disburseLoan(final Long loanId, final JsonCommand if (netDisbursalAmount != null) { loan.setNetDisbursalAmount(netDisbursalAmount); } - Money disburseAmount = loan.adjustDisburseAmount(command, actualDisbursementDate); + Money disburseAmount = loanDisbursementService.adjustDisburseAmount(loan, command, actualDisbursementDate); Money amountToDisburse = disburseAmount.copy(); boolean recalculateSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincipal()); final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); @@ -556,25 +578,27 @@ private ChangedTransactionDetail disburseLoan(JsonCommand command, boolean isPay PaymentDetail paymentDetail, Loan loan, AppUser currentUser, Map changes, ScheduleGeneratorDTO scheduleGeneratorDTO) { final PaymentDetail paymentDetail1 = isPaymentTypeApplicableForDisbursementCharge ? paymentDetail : null; - final LocalDate actualDisbursementDate1 = command.localDateValueOfParameterNamed(Loan.ACTUAL_DISBURSEMENT_DATE); + final LocalDate actualDisbursementDate1 = command.localDateValueOfParameterNamed(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)); + changes.put(ACTUAL_DISBURSEMENT_DATE, command.stringValueOfParameterNamed(ACTUAL_DISBURSEMENT_DATE)); boolean disbursementMissedParam = loan.isDisbursementMissed(); LocalDate firstInstallmentDueDate = loan.fetchRepaymentScheduleInstallment(1).getDueDate(); if ((loan.repaymentScheduleDetail().isInterestRecalculationEnabled() && (DateUtils.isBeforeBusinessDate(firstInstallmentDueDate) || disbursementMissedParam))) { - loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); + loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); } loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement()); loan.updateLoanRepaymentPeriodsDerivedFields(actualDisbursementDate1); - loan.handleDisbursementTransaction(actualDisbursementDate1, paymentDetail1); + loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_DISBURSED, + actualDisbursementDate1); + loanDisbursementService.handleDisbursementTransaction(loan, actualDisbursementDate1, paymentDetail1); loan.updateLoanSummaryDerivedFields(); final Money interestApplied = Money.of(loan.getCurrency(), loan.getSummary().getTotalInterestCharged()); @@ -603,7 +627,7 @@ private ChangedTransactionDetail disburseLoan(JsonCommand command, boolean isPay } loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSED, loan); - changes.put(Loan.PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus())); + changes.put(PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus())); return changedTransactionDetail; } @@ -762,13 +786,13 @@ public Map bulkLoanDisbursal(final JsonCommand command, final Co // disbursement date and next available meeting dates // assuming repayment schedule won't regenerate because expected // disbursement and actual disbursement happens on same date - loan.validateAccountStatus(LoanEvent.LOAN_DISBURSED); + loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_DISBURSED); updateLoanCounters(loan, actualDisbursementDate); boolean canDisburse = loan.canDisburse(); ChangedTransactionDetail changedTransactionDetail = null; if (canDisburse) { Money amountBeforeAdjust = loan.getPrincipal(); - Money disburseAmount = loan.adjustDisburseAmount(command, actualDisbursementDate); + Money disburseAmount = loanDisbursementService.adjustDisburseAmount(loan, command, actualDisbursementDate); boolean recalculateSchedule = amountBeforeAdjust.isNotEqualTo(loan.getPrincipal()); final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); if (isAccountTransfer) { @@ -901,7 +925,10 @@ public CommandProcessingResult undoLoanDisbursal(final Long loanId, final JsonCo // Remove post dated checks if added. loan.removePostDatedChecks(); - final Map changes = loan.undoDisbursal(scheduleGeneratorDTO, existingTransactionIds, + loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_DISBURSAL_UNDO); + loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_DISBURSAL_UNDO, + loan.getDisbursementDate()); + final Map changes = undoDisbursal(loan, scheduleGeneratorDTO, existingTransactionIds, existingReversedTransactionIds); loanAccrualsProcessingService.reprocessExistingAccruals(loan); @@ -1121,7 +1148,7 @@ private ChangedTransactionDetail reprocessChangedLoanTransactions(Loan loan, LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor, ScheduleGeneratorDTO scheduleGeneratorDTO) { if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { - loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); + loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); loanAccrualsProcessingService.reprocessExistingAccruals(loan); loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); } @@ -1137,6 +1164,8 @@ public Loan reverseReplayAccrualActivityTransaction(Loan loan, final LoanTransac transactionDate); if (!newLoanTransaction.getDateOf().isEqual(loanTransaction.getDateOf()) || !LoanTransaction.transactionAmountsMatch(loan.getCurrency(), loanTransaction, newLoanTransaction)) { + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), loanTransaction, + "reversed"); loanTransaction.reverse(); loanTransaction.updateExternalId(null); newLoanTransaction.copyLoanTransactionRelations(loanTransaction.getLoanTransactionRelations()); @@ -1340,9 +1369,9 @@ public Map makeLoanBulkRepayment(final CollectionSheetBulkRepaym isHolidayEnabled = this.configurationDomainService.isRescheduleRepaymentsOnHolidaysEnabled(); holidayDetailDTO = new HolidayDetailDTO(isHolidayEnabled, holidays, workingDays, allowTransactionsOnHoliday, allowTransactionsOnNonWorkingDay); - loan.validateRepaymentDateIsOnHoliday(singleLoanRepaymentCommand.getTransactionDate(), + loanTransactionValidator.validateRepaymentDateIsOnHoliday(singleLoanRepaymentCommand.getTransactionDate(), holidayDetailDTO.isAllowTransactionsOnHoliday(), holidayDetailDTO.getHolidays()); - loan.validateRepaymentDateIsOnNonWorkingDay(singleLoanRepaymentCommand.getTransactionDate(), + loanTransactionValidator.validateRepaymentDateIsOnNonWorkingDay(singleLoanRepaymentCommand.getTransactionDate(), holidayDetailDTO.getWorkingDays(), holidayDetailDTO.isAllowTransactionsOnNonWorkingDay()); isHolidayValidationDone = true; break; @@ -1578,7 +1607,7 @@ public ChangedTransactionDetail adjustExistingTransaction(final Loan loan, final existingTransactionIds.addAll(loan.findExistingTransactionIds()); existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); - loan.validateActivityNotBeforeClientOrGroupTransferDate(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, + loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_REPAYMENT_OR_WAIVER, transactionForAdjustment.getTransactionDate()); if (transactionForAdjustment.isNotRepaymentLikeType() && transactionForAdjustment.isNotWaiver() @@ -1588,6 +1617,8 @@ public ChangedTransactionDetail adjustExistingTransaction(final Loan loan, final "adjustment.is.only.allowed.to.repayment.or.waiver.or.creditbalancerefund.transactions", errorMessage); } + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(transactionForAdjustment.getLoan(), + transactionForAdjustment, "reversed"); transactionForAdjustment.reverse(reversalExternalId); transactionForAdjustment.manuallyAdjustedOrReversed(); @@ -1599,6 +1630,8 @@ public ChangedTransactionDetail adjustExistingTransaction(final Loan loan, final .anyMatch(relation -> relation.getRelationType().equals(LoanTransactionRelationTypeEnum.RELATED) && relation.getToTransaction().getId().equals(transactionForAdjustment.getId()))) .forEach(loanTransaction -> { + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(loanTransaction.getLoan(), + loanTransaction, "reversed"); loanTransaction.reverse(); loanTransaction.manuallyAdjustedOrReversed(); LoanAdjustTransactionBusinessEvent.Data eventData = new LoanAdjustTransactionBusinessEvent.Data(loanTransaction); @@ -1609,6 +1642,8 @@ public ChangedTransactionDetail adjustExistingTransaction(final Loan loan, final if (loan.isClosedWrittenOff()) { // find write off transaction and reverse it final LoanTransaction writeOffTransaction = loan.findWriteOffTransaction(); + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(writeOffTransaction.getLoan(), writeOffTransaction, + "reversed"); writeOffTransaction.reverse(); } @@ -1617,8 +1652,8 @@ public ChangedTransactionDetail adjustExistingTransaction(final Loan loan, final } if (newTransactionDetail.isRepaymentLikeType() || newTransactionDetail.isInterestWaiver()) { - changedTransactionDetail = loan.handleRepaymentOrRecoveryOrWaiverTransaction(newTransactionDetail, loanLifecycleStateMachine, - transactionForAdjustment, scheduleGeneratorDTO); + changedTransactionDetail = loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, + newTransactionDetail, loanLifecycleStateMachine, transactionForAdjustment, scheduleGeneratorDTO); } return changedTransactionDetail; @@ -1693,7 +1728,7 @@ public CommandProcessingResult chargebackLoanTransaction(final Long loanId, fina newTransaction = this.loanTransactionRepository.saveAndFlush(newTransaction); - loan.handleChargebackTransaction(newTransaction, loanLifecycleStateMachine); + handleChargebackTransaction(loan, newTransaction, loanLifecycleStateMachine); loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); @@ -1777,7 +1812,12 @@ public CommandProcessingResult waiveInterestOnLoan(final Long loanId, final Json } ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - final ChangedTransactionDetail changedTransactionDetail = loan.waiveInterest(waiveInterestTransaction, loanLifecycleStateMachine, + loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_REPAYMENT_OR_WAIVER); + loanTransactionValidator.validateActivityNotBeforeLastTransactionDate(loan, waiveInterestTransaction.getTransactionDate(), + LoanEvent.LOAN_REPAYMENT_OR_WAIVER); + loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_REPAYMENT_OR_WAIVER, + waiveInterestTransaction.getTransactionDate()); + final ChangedTransactionDetail changedTransactionDetail = waiveInterest(loan, waiveInterestTransaction, loanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { @@ -1874,8 +1914,11 @@ public CommandProcessingResult writeOff(final Long loanId, final JsonCommand com } ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - - final ChangedTransactionDetail changedTransactionDetail = loan.closeAsWrittenOff(command, loanLifecycleStateMachine, changes, + final LocalDate writtenOffOnLocalDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE); + loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.WRITE_OFF_OUTSTANDING); + loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.WRITE_OFF_OUTSTANDING, + writtenOffOnLocalDate); + final ChangedTransactionDetail changedTransactionDetail = closeAsWrittenOff(loan, command, loanLifecycleStateMachine, changes, existingTransactionIds, existingReversedTransactionIds, currentUser, scheduleGeneratorDTO); loanAccrualsProcessingService.reprocessExistingAccruals(loan); @@ -1948,8 +1991,11 @@ public CommandProcessingResult closeLoan(final Long loanId, final JsonCommand co recalculateFrom = command.localDateValueOfParameterNamed("transactionDate"); } - ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); - ChangedTransactionDetail changedTransactionDetail = loan.close(command, loanLifecycleStateMachine, changes, existingTransactionIds, + final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom); + final LocalDate closureDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE); + loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_CLOSED); + loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.REPAID_IN_FULL, closureDate); + ChangedTransactionDetail changedTransactionDetail = close(loan, command, loanLifecycleStateMachine, changes, existingTransactionIds, existingReversedTransactionIds, scheduleGeneratorDTO); loanAccrualsProcessingService.reprocessExistingAccruals(loan); @@ -2038,7 +2084,7 @@ public CommandProcessingResult closeAsRescheduled(final Long loanId, final JsonC changes.put("locale", command.locale()); changes.put("dateFormat", command.dateFormat()); - loan.closeAsMarkedForReschedule(command, loanLifecycleStateMachine, changes); + closeAsMarkedForReschedule(loan, command, loanLifecycleStateMachine, changes); saveLoanWithDataIntegrityViolationChecks(loan); @@ -2149,7 +2195,7 @@ public LoanTransaction acceptLoanTransfer(final Loan loan, final LocalDate trans loanLifecycleStateMachine.transition(LoanEvent.LOAN_REPAYMENT_OR_WAIVER, loan); } if (loanOfficer != null) { - loan.reassignLoanOfficer(loanOfficer, transferDate); + loanOfficerService.reassignLoanOfficer(loan, loanOfficer, transferDate); } this.loanTransactionRepository.saveAndFlush(newTransferAcceptanceTransaction); @@ -2218,7 +2264,7 @@ public CommandProcessingResult loanReassignment(final Long loanId, final JsonCom throw new LoanOfficerAssignmentException(loanId, fromLoanOfficerId); } - loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment); + loanOfficerService.reassignLoanOfficer(loan, toLoanOfficer, dateOfLoanOfficerAssignment); saveLoanWithDataIntegrityViolationChecks(loan); businessEventNotifierService.notifyPostBusinessEvent(new LoanReassignOfficerBusinessEvent(loan)); @@ -2263,7 +2309,7 @@ public CommandProcessingResult bulkLoanReassignment(final JsonCommand command) { throw new LoanOfficerAssignmentException(loanId, fromLoanOfficerId); } - loan.reassignLoanOfficer(toLoanOfficer, dateOfLoanOfficerAssignment); + loanOfficerService.reassignLoanOfficer(loan, toLoanOfficer, dateOfLoanOfficerAssignment); saveLoanWithDataIntegrityViolationChecks(loan); businessEventNotifierService.notifyPostBusinessEvent(new LoanReassignOfficerBusinessEvent(loan)); } @@ -2296,6 +2342,7 @@ public CommandProcessingResult removeLoanOfficer(final Long loanId, final JsonCo } businessEventNotifierService.notifyPreBusinessEvent(new LoanRemoveOfficerBusinessEvent(loan)); + loanOfficerValidator.validateUnassignDate(loan, dateOfLoanOfficerUnassigned); loan.removeLoanOfficer(dateOfLoanOfficerUnassigned); saveLoanWithDataIntegrityViolationChecks(loan); @@ -2392,7 +2439,7 @@ public void applyMeetingDateChanges(final Calendar calendar, final Collection changes = loan.undoLastDisbursal(scheduleGeneratorDTO, existingTransactionIds, - existingReversedTransactionIds, loan); + loanDownPaymentTransactionValidator.validateAccountStatus(loan, LoanEvent.LOAN_DISBURSAL_UNDO_LAST); + loanTransactionValidator.validateActivityNotBeforeClientOrGroupTransferDate(loan, LoanEvent.LOAN_DISBURSAL_UNDO_LAST, + loan.getDisbursementDate()); + final Map changes = undoLastDisbursal(scheduleGeneratorDTO, existingTransactionIds, existingReversedTransactionIds, + loan); loanAccrualsProcessingService.reprocessExistingAccruals(loan); if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { @@ -3268,6 +3317,8 @@ public CommandProcessingResult undoChargeOff(JsonCommand command) { final String reversalExternalId = command.stringValueOfParameterNamedAllowingNull(LoanApiConstants.REVERSAL_EXTERNAL_ID_PARAMNAME); final ExternalId reversalTxnExternalId = ExternalIdFactory.produce(reversalExternalId); + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(chargedOffTransaction.getLoan(), chargedOffTransaction, + "reversed"); chargedOffTransaction.reverse(reversalTxnExternalId); chargedOffTransaction.manuallyAdjustedOrReversed(); @@ -3367,6 +3418,21 @@ public CommandProcessingResult makeRefund(final Long loanId, final LoanTransacti .build(); } + public void handleChargebackTransaction(final Loan loan, final LoanTransaction chargebackTransaction, + final LoanLifecycleStateMachine loanLifecycleStateMachine) { + loanTransactionValidator.validateIfTransactionIsChargeback(chargebackTransaction); + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor(); + + loan.addLoanTransaction(chargebackTransaction); + loanRepaymentScheduleTransactionProcessor.processLatestTransaction(chargebackTransaction, new TransactionCtx(loan.getCurrency(), + loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); + + loan.updateLoanSummaryDerivedFields(); + if (!loan.doPostLoanTransactionChecks(chargebackTransaction.getTransactionDate(), loanLifecycleStateMachine)) { + loanLifecycleStateMachine.transition(LoanEvent.LOAN_CHARGEBACK, loan); + } + } + private void validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(Loan loan) { if (!loan.isMultiDisburmentLoan()) { final String errorMessage = "loan.product.does.not.support.multiple.disbursals.cannot.undo.last.disbursal"; @@ -3400,4 +3466,422 @@ private void validateTransactionsForTransfer(final Loan loan, final LocalDate tr } } } + + private Map undoDisbursal(final Loan loan, final ScheduleGeneratorDTO scheduleGeneratorDTO, + final List existingTransactionIds, final List existingReversedTransactionIds) { + final Map actualChanges = new LinkedHashMap<>(); + final LoanStatus currentStatus = loan.getStatus(); + final LoanStatus statusEnum = this.loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_DISBURSAL_UNDO, loan); + existingTransactionIds.addAll(loan.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); + if (!statusEnum.hasStateOf(currentStatus)) { + this.loanLifecycleStateMachine.transition(LoanEvent.LOAN_DISBURSAL_UNDO, loan); + actualChanges.put(PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus())); + + final LocalDate actualDisbursementDate = loan.getDisbursementDate(); + final boolean isScheduleRegenerateRequired = loan.isActualDisbursedOnDateEarlierOrLaterThanExpected(actualDisbursementDate); + loan.setActualDisbursementDate(null); + loan.setDisbursedBy(null); + final boolean isDisbursedAmountChanged = !MathUtil.isEqualTo(loan.getApprovedPrincipal(), + loan.getLoanRepaymentScheduleDetail().getPrincipal().getAmount()); + loan.getLoanRepaymentScheduleDetail().setPrincipal(loan.getApprovedPrincipal()); + // Remove All the Disbursement Details If the Loan Product is disabled and exists one + if (loan.loanProduct().isDisallowExpectedDisbursements() && !loan.getDisbursementDetails().isEmpty()) { + for (LoanDisbursementDetails disbursementDetail : loan.getAllDisbursementDetails()) { + disbursementDetail.reverse(); + } + } else { + for (final LoanDisbursementDetails details : loan.getDisbursementDetails()) { + details.updateActualDisbursementDate(null); + } + } + final boolean isEmiAmountChanged = !loan.getLoanTermVariations().isEmpty(); + + updateLoanToPreDisbursalState(loan); + if (isScheduleRegenerateRequired || isDisbursedAmountChanged || isEmiAmountChanged + || loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + // clear off actual disbusrement date so schedule regeneration + // uses expected date. + + loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); + if (isDisbursedAmountChanged) { + loan.updateSummaryWithTotalFeeChargesDueAtDisbursement(loan.deriveSumTotalOfChargesDueAtDisbursement()); + } + } else if (loan.isPeriodicAccrualAccountingEnabledOnLoanProduct()) { + for (final LoanRepaymentScheduleInstallment period : loan.getRepaymentScheduleInstallments()) { + period.resetAccrualComponents(); + } + } + + if (loan.isTopup()) { + loan.getLoanTopupDetails().setAccountTransferDetails(null); + loan.getLoanTopupDetails().setTopupAmount(null); + } + + loan.adjustNetDisbursalAmount(loan.getApprovedPrincipal()); + actualChanges.put(ACTUAL_DISBURSEMENT_DATE, ""); + loan.updateLoanSummaryDerivedFields(); + } + + return actualChanges; + } + + public void updateLoanToPreDisbursalState(final Loan loan) { + loan.setActualDisbursementDate(null); + + loan.setAccruedTill(null); + reverseExistingTransactions(loan); + + for (final LoanCharge charge : loan.getActiveCharges()) { + if (charge.isOverdueInstallmentCharge()) { + charge.setActive(false); + } else { + charge.resetToOriginal(loan.loanCurrency()); + } + } + final List installments = loan.getRepaymentScheduleInstallments(); + for (final LoanRepaymentScheduleInstallment currentInstallment : installments) { + currentInstallment.resetDerivedComponents(); + } + for (LoanTermVariations variations : loan.getLoanTermVariations()) { + if (variations.getOnLoanStatus().equals(LoanStatus.ACTIVE.getValue())) { + variations.markAsInactive(); + } + } + final LoanRepaymentScheduleProcessingWrapper wrapper = new LoanRepaymentScheduleProcessingWrapper(); + wrapper.reprocess(loan.getCurrency(), loan.getDisbursementDate(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); + + loan.updateLoanSummaryDerivedFields(); + } + + private void reverseExistingTransactions(final Loan loan) { + final Collection retainTransactions = new ArrayList<>(); + for (final LoanTransaction transaction : loan.getLoanTransactions()) { + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(transaction.getLoan(), transaction, "reversed"); + transaction.reverse(); + if (transaction.getId() != null) { + retainTransactions.add(transaction); + } + } + loan.getLoanTransactions().retainAll(retainTransactions); + } + + private ChangedTransactionDetail closeAsWrittenOff(final Loan loan, final JsonCommand command, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map changes, + final List existingTransactionIds, final List existingReversedTransactionIds, final AppUser currentUser, + final ScheduleGeneratorDTO scheduleGeneratorDTO) { + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor(); + ChangedTransactionDetail changedTransactionDetail = closeDisbursements(loan, scheduleGeneratorDTO); + + final LocalDate writtenOffOnLocalDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE); + loan.setClosedOnDate(writtenOffOnLocalDate); + loan.setWrittenOffOnDate(writtenOffOnLocalDate); + loan.setClosedBy(currentUser); + final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.WRITE_OFF_OUTSTANDING, loan); + + LoanTransaction loanTransaction = null; + if (!statusEnum.hasStateOf(loan.getStatus())) { + loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING, loan); + changes.put(PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus())); + + existingTransactionIds.addAll(loan.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); + + final String txnExternalId = command.stringValueOfParameterNamedAllowingNull(EXTERNAL_ID); + + ExternalId externalId = ExternalIdFactory.produce(txnExternalId); + + if (externalId.isEmpty() && TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } + + changes.put(CLOSED_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE)); + changes.put(WRITTEN_OFF_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE)); + changes.put("externalId", externalId); + + if (DateUtils.isBefore(writtenOffOnLocalDate, loan.getDisbursementDate())) { + final String errorMessage = "The date on which a loan is written off cannot be before the loan disbursement date: " + + loan.getDisbursementDate().toString(); + throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.before.submittal.date", errorMessage, + writtenOffOnLocalDate, loan.getDisbursementDate()); + } + + if (DateUtils.isDateInTheFuture(writtenOffOnLocalDate)) { + final String errorMessage = "The date on which a loan is written off cannot be in the future."; + throw new InvalidLoanStateTransitionException("writeoff", "cannot.be.a.future.date", errorMessage, writtenOffOnLocalDate); + } + + loanTransaction = LoanTransaction.writeoff(loan, loan.getOffice(), writtenOffOnLocalDate, externalId); + LocalDate lastTransactionDate = loan.getLastUserTransactionDate(); + if (DateUtils.isAfter(lastTransactionDate, writtenOffOnLocalDate)) { + final String errorMessage = "The date of the writeoff transaction must occur on or before previous transactions."; + throw new InvalidLoanStateTransitionException("writeoff", "must.occur.on.or.after.other.transaction.dates", errorMessage, + writtenOffOnLocalDate); + } + + loan.addLoanTransaction(loanTransaction); + loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, + new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), + new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); + + loan.updateLoanSummaryDerivedFields(); + } + if (changedTransactionDetail == null) { + changedTransactionDetail = new ChangedTransactionDetail(); + } + changedTransactionDetail.getNewTransactionMappings().put(0L, loanTransaction); + return changedTransactionDetail; + } + + private ChangedTransactionDetail closeDisbursements(final Loan loan, final ScheduleGeneratorDTO scheduleGeneratorDTO) { + ChangedTransactionDetail changedTransactionDetail = null; + if (loan.isDisbursementAllowed() && loan.atLeastOnceDisbursed()) { + loan.getLoanRepaymentScheduleDetail().setPrincipal(loan.getDisbursedAmount()); + loan.removeDisbursementDetail(); + loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); + } + changedTransactionDetail = loan.reprocessTransactions(); + LocalDate lastLoanTransactionDate = loan.getLatestTransactionDate(); + loan.doPostLoanTransactionChecks(lastLoanTransactionDate, loanLifecycleStateMachine); + } + return changedTransactionDetail; + } + + private ChangedTransactionDetail close(final Loan loan, final JsonCommand command, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map changes, + final List existingTransactionIds, final List existingReversedTransactionIds, + final ScheduleGeneratorDTO scheduleGeneratorDTO) { + existingTransactionIds.addAll(loan.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); + + final LocalDate closureDate = command.localDateValueOfParameterNamed(TRANSACTION_DATE); + final String txnExternalId = command.stringValueOfParameterNamedAllowingNull(EXTERNAL_ID); + + ExternalId externalId = ExternalIdFactory.produce(txnExternalId); + if (externalId.isEmpty() && TemporaryConfigurationServiceContainer.isExternalIdAutoGenerationEnabled()) { + externalId = ExternalId.generate(); + } + + loan.setClosedOnDate(closureDate); + changes.put(CLOSED_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE)); + + if (DateUtils.isBefore(closureDate, loan.getDisbursementDate())) { + final String errorMessage = "The date on which a loan is closed cannot be before the loan disbursement date: " + + loan.getDisbursementDate().toString(); + throw new InvalidLoanStateTransitionException("close", "cannot.be.before.submittal.date", errorMessage, closureDate, + loan.getDisbursementDate()); + } + + if (DateUtils.isDateInTheFuture(closureDate)) { + final String errorMessage = "The date on which a loan is closed cannot be in the future."; + throw new InvalidLoanStateTransitionException("close", "cannot.be.a.future.date", errorMessage, closureDate); + } + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = loan.getTransactionProcessor(); + ChangedTransactionDetail changedTransactionDetail = closeDisbursements(loan, scheduleGeneratorDTO); + + LoanTransaction loanTransaction = null; + if (loan.isOpen()) { + final Money totalOutstanding = loan.getSummary().getTotalOutstanding(loan.getCurrency()); + if (totalOutstanding.isGreaterThanZero() && loan.getInArrearsTolerance().isGreaterThanOrEqualTo(totalOutstanding)) { + + loan.setClosedOnDate(closureDate); + final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.REPAID_IN_FULL, loan); + if (!statusEnum.hasStateOf(loan.getStatus())) { + loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, loan); + changes.put(PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus())); + } + changes.put("externalId", externalId); + loanTransaction = LoanTransaction.writeoff(loan, loan.getOffice(), closureDate, externalId); + final boolean isLastTransaction = loan.isChronologicallyLatestTransaction(loanTransaction, loan.getLoanTransactions()); + if (!isLastTransaction) { + final String errorMessage = "The closing date of the loan must be on or after latest transaction date."; + throw new InvalidLoanStateTransitionException("close.loan", "must.occur.on.or.after.latest.transaction.date", + errorMessage, closureDate); + } + + loan.addLoanTransaction(loanTransaction); + loanRepaymentScheduleTransactionProcessor.processLatestTransaction(loanTransaction, + new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), + new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); + + loan.updateLoanSummaryDerivedFields(); + } else if (totalOutstanding.isGreaterThanZero()) { + final String errorMessage = "A loan with money outstanding cannot be closed"; + throw new InvalidLoanStateTransitionException("close", "loan.has.money.outstanding", errorMessage, + totalOutstanding.toString()); + } + } + + if (loan.isOverPaid()) { + final Money totalLoanOverpayment = loan.calculateTotalOverpayment(); + if (totalLoanOverpayment.isGreaterThanZero() && loan.getInArrearsTolerance().isGreaterThanOrEqualTo(totalLoanOverpayment)) { + // TODO - KW - technically should set somewhere that this loan + // has 'overpaid' amount + loan.setClosedOnDate(closureDate); + final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.REPAID_IN_FULL, loan); + if (!statusEnum.hasStateOf(loan.getStatus())) { + loanLifecycleStateMachine.transition(LoanEvent.REPAID_IN_FULL, loan); + changes.put(PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus())); + } + } else if (totalLoanOverpayment.isGreaterThanZero()) { + final String errorMessage = "The loan is marked as 'Overpaid' and cannot be moved to 'Closed (obligations met)."; + throw new InvalidLoanStateTransitionException("close", "loan.is.overpaid", errorMessage, totalLoanOverpayment.toString()); + } + } + + if (changedTransactionDetail == null) { + changedTransactionDetail = new ChangedTransactionDetail(); + } + changedTransactionDetail.getNewTransactionMappings().put(0L, loanTransaction); + return changedTransactionDetail; + } + + private ChangedTransactionDetail updateDisbursementDateAndAmountForTranche(final Loan loan, + final LoanDisbursementDetails disbursementDetails, final JsonCommand command, final Map actualChanges, + final ScheduleGeneratorDTO scheduleGeneratorDTO) { + final Locale locale = command.extractLocale(); + final BigDecimal principal = command.bigDecimalValueOfParameterNamed(LoanApiConstants.updatedDisbursementPrincipalParameterName, + locale); + final LocalDate expectedDisbursementDate = command + .localDateValueOfParameterNamed(LoanApiConstants.updatedDisbursementDateParameterName); + disbursementDetails.updateExpectedDisbursementDateAndAmount(expectedDisbursementDate, principal); + actualChanges.put(LoanApiConstants.expectedDisbursementDateParameterName, + command.stringValueOfParameterNamed(LoanApiConstants.expectedDisbursementDateParameterName)); + actualChanges.put(LoanApiConstants.disbursementIdParameterName, + command.stringValueOfParameterNamed(LoanApiConstants.disbursementIdParameterName)); + actualChanges.put(LoanApiConstants.disbursementPrincipalParameterName, + command.bigDecimalValueOfParameterNamed(LoanApiConstants.disbursementPrincipalParameterName, locale)); + + loan.getLoanRepaymentScheduleDetail().setPrincipal(loan.getPrincipalAmountForRepaymentSchedule()); + + if (loan.repaymentScheduleDetail().isInterestRecalculationEnabled()) { + loanScheduleService.regenerateRepaymentScheduleWithInterestRecalculation(loan, scheduleGeneratorDTO); + } else { + loanScheduleService.regenerateRepaymentSchedule(loan, scheduleGeneratorDTO); + } + + return loan.reprocessTransactions(); + } + + private Map undoLastDisbursal(final ScheduleGeneratorDTO scheduleGeneratorDTO, final List existingTransactionIds, + final List existingReversedTransactionIds, final Loan loan) { + final Map actualChanges = new LinkedHashMap<>(); + List loanTransactions = loan.retrieveListOfTransactionsByType(LoanTransactionType.DISBURSEMENT); + loanTransactions.sort(Comparator.comparing(LoanTransaction::getId)); + final LoanTransaction lastDisbursalTransaction = loanTransactions.get(loanTransactions.size() - 1); + final LocalDate lastTransactionDate = lastDisbursalTransaction.getTransactionDate(); + + existingTransactionIds.addAll(loan.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); + + loanTransactions = loan.retrieveListOfTransactionsExcludeAccruals(); + Collections.reverse(loanTransactions); + for (final LoanTransaction previousTransaction : loanTransactions) { + if (DateUtils.isBefore(lastTransactionDate, previousTransaction.getTransactionDate()) + && (previousTransaction.isRepaymentLikeType() || previousTransaction.isWaiver() + || previousTransaction.isChargePayment())) { + throw new UndoLastTrancheDisbursementException(previousTransaction.getId()); + } + if (previousTransaction.getId().compareTo(lastDisbursalTransaction.getId()) < 0) { + break; + } + } + final LoanDisbursementDetails disbursementDetail = loan.getDisbursementDetails(lastTransactionDate, + lastDisbursalTransaction.getAmount()); + loan.updateLoanToLastDisbursalState(disbursementDetail); + loan.getLoanTermVariations() + .removeIf(loanTermVariations -> (loanTermVariations.getTermType().isDueDateVariation() + && DateUtils.isAfter(loanTermVariations.fetchDateValue(), lastTransactionDate)) + || (loanTermVariations.getTermType().isEMIAmountVariation() + && DateUtils.isEqual(loanTermVariations.getTermApplicableFrom(), lastTransactionDate)) + || DateUtils.isAfter(loanTermVariations.getTermApplicableFrom(), lastTransactionDate)); + reverseExistingTransactionsTillLastDisbursal(loan, lastDisbursalTransaction); + loanScheduleService.recalculateScheduleFromLastTransaction(loan, scheduleGeneratorDTO); + actualChanges.put("undolastdisbursal", "true"); + actualChanges.put("disbursedAmount", loan.getDisbursedAmount()); + loan.updateLoanSummaryDerivedFields(); + + loan.doPostLoanTransactionChecks(loan.getLastUserTransactionDate(), loanLifecycleStateMachine); + + return actualChanges; + } + + private ChangedTransactionDetail waiveInterest(final Loan loan, final LoanTransaction waiveInterestTransaction, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final List existingTransactionIds, + final List existingReversedTransactionIds, final ScheduleGeneratorDTO scheduleGeneratorDTO) { + existingTransactionIds.addAll(loan.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); + + return loanDownPaymentHandlerService.handleRepaymentOrRecoveryOrWaiverTransaction(loan, waiveInterestTransaction, + loanLifecycleStateMachine, null, scheduleGeneratorDTO); + } + + private ChangedTransactionDetail undoWrittenOff(final Loan loan, final LoanLifecycleStateMachine loanLifecycleStateMachine, + final List existingTransactionIds, final List existingReversedTransactionIds) { + existingTransactionIds.addAll(loan.findExistingTransactionIds()); + existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds()); + final LoanTransaction writeOffTransaction = loan.findWriteOffTransaction(); + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(writeOffTransaction.getLoan(), writeOffTransaction, + "reversed"); + writeOffTransaction.reverse(); + loanLifecycleStateMachine.transition(LoanEvent.WRITE_OFF_OUTSTANDING_UNDO, loan); + return loan.reprocessTransactions(); + } + + /** + * Reverse only disbursement, accruals, and repayments at disbursal transactions + */ + public void reverseExistingTransactionsTillLastDisbursal(final Loan loan, final LoanTransaction lastDisbursalTransaction) { + for (final LoanTransaction transaction : loan.getLoanTransactions()) { + if (!DateUtils.isBefore(transaction.getTransactionDate(), lastDisbursalTransaction.getTransactionDate()) + && transaction.getId().compareTo(lastDisbursalTransaction.getId()) >= 0 + && transaction.isAllowTypeTransactionAtTheTimeOfLastUndo()) { + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(transaction.getLoan(), transaction, "reversed"); + transaction.reverse(); + } + } + if (loan.isAutoRepaymentForDownPaymentEnabled()) { + // identify down-payment amount for the transaction + BigDecimal disbursedAmountPercentageForDownPayment = loan.getLoanRepaymentScheduleDetail() + .getDisbursedAmountPercentageForDownPayment(); + Money downPaymentMoney = Money.of(loan.getCurrency(), + MathUtil.percentageOf(lastDisbursalTransaction.getAmount(), disbursedAmountPercentageForDownPayment, 19)); + + // find the latest matching down-payment transaction based on date, amount and transaction type + Optional downPaymentTransaction = loan.getLoanTransactions().stream() + .filter(tr -> tr.getTransactionDate().equals(lastDisbursalTransaction.getTransactionDate()) + && tr.getTypeOf().isDownPayment() && tr.getAmount().compareTo(downPaymentMoney.getAmount()) == 0) + .max(Comparator.comparing(LoanTransaction::getId)); + + // reverse the down-payment transaction + downPaymentTransaction.ifPresent(tr -> { + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(tr.getLoan(), tr, "reversed"); + tr.reverse(); + }); + } + } + + /** + * Behaviour added to comply with capability of previous mifos product to support easier transition to fineract + * platform. + */ + public void closeAsMarkedForReschedule(final Loan loan, final JsonCommand command, + final LoanLifecycleStateMachine loanLifecycleStateMachine, final Map changes) { + final LocalDate rescheduledOn = command.localDateValueOfParameterNamed(TRANSACTION_DATE); + + loan.setClosedOnDate(rescheduledOn); + final LoanStatus statusEnum = loanLifecycleStateMachine.dryTransition(LoanEvent.LOAN_RESCHEDULE, loan); + if (!statusEnum.hasStateOf(loan.getStatus())) { + loanLifecycleStateMachine.transition(LoanEvent.LOAN_RESCHEDULE, loan); + changes.put(PARAM_STATUS, LoanEnumerations.status(loan.getLoanStatus())); + } + + loan.setRescheduledOnDate(rescheduledOn); + changes.put(CLOSED_ON_DATE, command.stringValueOfParameterNamed(TRANSACTION_DATE)); + changes.put("rescheduledOnDate", command.stringValueOfParameterNamed(TRANSACTION_DATE)); + + loanTransactionValidator.validateLoanRescheduleDate(loan); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java index 5f2728e7735..7ec9c815d84 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reaging/LoanReAgingServiceImpl.java @@ -51,6 +51,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; import org.apache.fineract.portfolio.loanaccount.exception.LoanTransactionNotFoundException; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.apache.fineract.portfolio.note.domain.Note; import org.apache.fineract.portfolio.note.domain.NoteRepository; @@ -70,6 +71,7 @@ public class LoanReAgingServiceImpl { private final LoanReAgingParameterRepository reAgingParameterRepository; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; private final NoteRepository noteRepository; + private final LoanChargeValidator loanChargeValidator; public CommandProcessingResult reAge(Long loanId, JsonCommand command) { Loan loan = loanAssembler.assembleFrom(loanId); @@ -141,6 +143,8 @@ public CommandProcessingResult undoReAge(Long loanId, JsonCommand command) { private void reverseReAgeTransaction(LoanTransaction reAgeTransaction, JsonCommand command) { ExternalId reversalExternalId = externalIdFactory.createFromCommand(command, LoanReAgingApiConstants.externalIdParameterName); + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(reAgeTransaction.getLoan(), reAgeTransaction, + "reversed"); reAgeTransaction.reverse(reversalExternalId); reAgeTransaction.manuallyAdjustedOrReversed(); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java index 10a73d3dd7a..d2fee242751 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/reamortization/LoanReAmortizationServiceImpl.java @@ -47,6 +47,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.TransactionCtx; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -62,6 +63,7 @@ public class LoanReAmortizationServiceImpl { private final BusinessEventNotifierService businessEventNotifierService; private final LoanTransactionRepository loanTransactionRepository; private final LoanRepaymentScheduleTransactionProcessorFactory loanRepaymentScheduleTransactionProcessorFactory; + private final LoanChargeValidator loanChargeValidator; public CommandProcessingResult reAmortize(Long loanId, JsonCommand command) { Loan loan = loanAssembler.assembleFrom(loanId); @@ -126,6 +128,8 @@ public CommandProcessingResult undoReAmortize(Long loanId, JsonCommand command) private void reverseReAmortizeTransaction(LoanTransaction reAmortizeTransaction, JsonCommand command) { ExternalId reversalExternalId = externalIdFactory.createFromCommand(command, LoanReAmortizationApiConstants.externalIdParameterName); + loanChargeValidator.validateRepaymentTypeTransactionNotBeforeAChargeRefund(reAmortizeTransaction.getLoan(), reAmortizeTransaction, + "reversed"); reAmortizeTransaction.reverse(reversalExternalId); reAmortizeTransaction.manuallyAdjustedOrReversed(); reAmortizeTransaction.getLoan().reprocessTransactions(); diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java index bd5e5d17d18..fdfaa607608 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java @@ -85,6 +85,12 @@ import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationTransitionValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanDisbursementValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanForeclosureValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanOfficerValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanRefundValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer; import org.apache.fineract.portfolio.loanaccount.service.BulkLoansReadPlatformService; @@ -108,13 +114,18 @@ import org.apache.fineract.portfolio.loanaccount.service.LoanChargePaidByReadService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformServiceImpl; +import org.apache.fineract.portfolio.loanaccount.service.LoanChargeService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeWritePlatformServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementDetailsAssembler; +import org.apache.fineract.portfolio.loanaccount.service.LoanDisbursementService; import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerService; import org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerServiceImpl; +import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceImpl; +import org.apache.fineract.portfolio.loanaccount.service.LoanRefundService; +import org.apache.fineract.portfolio.loanaccount.service.LoanScheduleService; import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanStatusChangePlatformServiceImpl; import org.apache.fineract.portfolio.loanaccount.service.LoanTransactionAssembler; @@ -186,13 +197,15 @@ public LoanApplicationWritePlatformService loanApplicationWritePlatformService(P LoanUtilService loanUtilService, CalendarReadPlatformService calendarReadPlatformService, EntityDatatableChecksWritePlatformService entityDatatableChecksWritePlatformService, GLIMAccountInfoRepository glimRepository, LoanRepository loanRepository, GSIMReadPlatformService gsimReadPlatformService, - LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanAccrualsProcessingService loanAccrualsProcessingService) { + LoanLifecycleStateMachine defaultLoanLifecycleStateMachine, LoanAccrualsProcessingService loanAccrualsProcessingService, + LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanScheduleService loanScheduleService) { return new LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, loanApplicationTransitionValidator, loanApplicationValidator, loanRepositoryWrapper, noteRepository, loanAssembler, loanSummaryWrapper, loanRepaymentScheduleTransactionProcessorFactory, calendarRepository, calendarInstanceRepository, savingsAccountRepository, accountAssociationsRepository, businessEventNotifierService, loanScheduleAssembler, loanUtilService, calendarReadPlatformService, entityDatatableChecksWritePlatformService, glimRepository, loanRepository, - gsimReadPlatformService, defaultLoanLifecycleStateMachine, loanAccrualsProcessingService); + gsimReadPlatformService, defaultLoanLifecycleStateMachine, loanAccrualsProcessingService, + loanDownPaymentTransactionValidator, loanScheduleService); } @Bean @@ -217,14 +230,15 @@ public LoanAssembler loanAssembler(FromJsonHelper fromApiJsonHelper, LoanReposit AccountNumberGenerator accountNumberGenerator, GLIMAccountInfoWritePlatformService glimAccountInfoWritePlatformService, LoanCollateralAssembler loanCollateralAssembler, LoanScheduleCalculationPlatformService calculationPlatformService, LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler, LoanChargeMapper loanChargeMapper, - LoanCollateralManagementMapper loanCollateralManagementMapper, LoanAccrualsProcessingService loanAccrualsProcessingService) { + LoanCollateralManagementMapper loanCollateralManagementMapper, LoanAccrualsProcessingService loanAccrualsProcessingService, + LoanDisbursementService loanDisbursementService, LoanChargeService loanChargeService, LoanOfficerService loanOfficerService) { return new LoanAssemblerImpl(fromApiJsonHelper, loanRepository, loanProductRepository, clientRepository, groupRepository, fundRepository, staffRepository, codeValueRepository, loanScheduleAssembler, loanChargeAssembler, collateralAssembler, loanSummaryWrapper, loanRepaymentScheduleTransactionProcessorFactory, holidayRepository, configurationDomainService, workingDaysRepository, rateAssembler, defaultLoanLifecycleStateMachine, externalIdFactory, accountNumberFormatRepository, glimRepository, accountNumberGenerator, glimAccountInfoWritePlatformService, loanCollateralAssembler, calculationPlatformService, loanDisbursementDetailsAssembler, loanChargeMapper, loanCollateralManagementMapper, - loanAccrualsProcessingService); + loanAccrualsProcessingService, loanDisbursementService, loanChargeService, loanOfficerService); } @Bean @@ -275,9 +289,9 @@ public LoanChargeWritePlatformService loanChargeWritePlatformService(LoanChargeA LoanChargeAssembler loanChargeAssembler, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService, PaymentDetailWritePlatformService paymentDetailWritePlatformService, NoteRepository noteRepository, LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService, - LoanAccrualsProcessingService loanAccrualsProcessingService - - ) { + LoanAccrualsProcessingService loanAccrualsProcessingService, + LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanChargeValidator loanChargeValidator, + LoanScheduleService loanScheduleService) { return new LoanChargeWritePlatformServiceImpl(loanChargeApiJsonValidator, loanAssembler, chargeRepository, businessEventNotifierService, loanTransactionRepository, accountTransfersWritePlatformService, loanRepositoryWrapper, journalEntryWritePlatformService, loanAccountDomainService, loanChargeRepository, loanWritePlatformService, loanUtilService, @@ -285,7 +299,7 @@ public LoanChargeWritePlatformService loanChargeWritePlatformService(LoanChargeA configurationDomainService, loanRepaymentScheduleTransactionProcessorFactory, externalIdFactory, accountTransferDetailRepository, loanChargeAssembler, replayedTransactionBusinessEventService, paymentDetailWritePlatformService, noteRepository, loanAccrualTransactionBusinessEventService, - loanAccrualsProcessingService); + loanAccrualsProcessingService, loanDownPaymentTransactionValidator, loanChargeValidator, loanScheduleService); } @Bean @@ -303,15 +317,16 @@ public LoanReadPlatformServiceImpl loanReadPlatformService(JdbcTemplate jdbcTemp ConfigurationDomainService configurationDomainService, AccountDetailsReadPlatformService accountDetailsReadPlatformService, ColumnValidator columnValidator, DatabaseSpecificSQLGenerator sqlGenerator, DelinquencyReadPlatformService delinquencyReadPlatformService, LoanTransactionRepository loanTransactionRepository, - LoanChargePaidByReadService loanChargePaidByReadService, - LoanTransactionRelationReadService loanTransactionRelationReadService) { + LoanChargePaidByReadService loanChargePaidByReadService, LoanTransactionRelationReadService loanTransactionRelationReadService, + LoanForeclosureValidator loanForeclosureValidator) { return new LoanReadPlatformServiceImpl(jdbcTemplate, context, loanRepositoryWrapper, applicationCurrencyRepository, loanProductReadPlatformService, clientReadPlatformService, groupReadPlatformService, loanDropdownReadPlatformService, fundReadPlatformService, chargeReadPlatformService, codeValueReadPlatformService, calendarReadPlatformService, staffReadPlatformService, paginationHelper, namedParameterJdbcTemplate, paymentTypeReadPlatformService, loanRepaymentScheduleTransactionProcessorFactory, floatingRatesReadPlatformService, loanUtilService, configurationDomainService, accountDetailsReadPlatformService, columnValidator, sqlGenerator, - delinquencyReadPlatformService, loanTransactionRepository, loanChargePaidByReadService, loanTransactionRelationReadService); + delinquencyReadPlatformService, loanTransactionRepository, loanChargePaidByReadService, loanTransactionRelationReadService, + loanForeclosureValidator); } @Bean @@ -362,7 +377,10 @@ public LoanWritePlatformService loanWritePlatformService(LoanRepaymentScheduleTr ExternalIdFactory externalIdFactory, ReplayedTransactionBusinessEventService replayedTransactionBusinessEventService, LoanAccrualTransactionBusinessEventService loanAccrualTransactionBusinessEventService, ErrorHandler errorHandler, LoanDownPaymentHandlerService loanDownPaymentHandlerService, AccountTransferRepository accountTransferRepository, - LoanTransactionAssembler loanTransactionAssembler, LoanAccrualsProcessingService loanAccrualsProcessingService) { + LoanTransactionAssembler loanTransactionAssembler, LoanAccrualsProcessingService loanAccrualsProcessingService, + LoanOfficerValidator loanOfficerValidator, LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, + LoanDisbursementService loanDisbursementService, LoanScheduleService loanScheduleService, + LoanChargeValidator loanChargeValidator, LoanOfficerService loanOfficerService) { return new LoanWritePlatformServiceJpaRepositoryImpl(transactionProcessorFactory, context, loanTransactionValidator, loanUpdateCommandFromApiJsonDeserializer, loanRepositoryWrapper, loanAccountDomainService, noteRepository, loanTransactionRepository, loanTransactionRelationRepository, loanAssembler, journalEntryWritePlatformService, @@ -376,7 +394,8 @@ public LoanWritePlatformService loanWritePlatformService(LoanRepaymentScheduleTr postDatedChecksRepository, loanRepaymentScheduleInstallmentRepository, defaultLoanLifecycleStateMachine, loanAccountLockService, externalIdFactory, replayedTransactionBusinessEventService, loanAccrualTransactionBusinessEventService, errorHandler, loanDownPaymentHandlerService, accountTransferRepository, - loanTransactionAssembler, loanAccrualsProcessingService); + loanTransactionAssembler, loanAccrualsProcessingService, loanOfficerValidator, loanDownPaymentTransactionValidator, + loanDisbursementService, loanScheduleService, loanChargeValidator, loanOfficerService); } @Bean @@ -389,8 +408,11 @@ public ReplayedTransactionBusinessEventService replayedTransactionBusinessEventS @Bean @ConditionalOnMissingBean(LoanDownPaymentHandlerService.class) public LoanDownPaymentHandlerService loanDownPaymentHandlerService(LoanTransactionRepository loanTransactionRepository, - BusinessEventNotifierService businessEventNotifierService) { - return new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService); + BusinessEventNotifierService businessEventNotifierService, + LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator, LoanScheduleService loanScheduleService, + LoanRefundService loanRefundService, LoanRefundValidator loanRefundValidator) { + return new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService, + loanDownPaymentTransactionValidator, loanScheduleService, loanRefundService, loanRefundValidator); } @Bean @@ -398,4 +420,35 @@ public LoanDownPaymentHandlerService loanDownPaymentHandlerService(LoanTransacti public LoanDisbursementDetailsAssembler loanDisbursementDetailsAssembler(FromJsonHelper fromApiJsonHelper) { return new LoanDisbursementDetailsAssembler(fromApiJsonHelper); } + + @Bean + @ConditionalOnMissingBean(LoanDisbursementService.class) + public LoanDisbursementService loanDisbursementService(LoanChargeValidator loanChargeValidator, + LoanDisbursementValidator loanDisbursementValidator) { + return new LoanDisbursementService(loanChargeValidator, loanDisbursementValidator); + } + + @Bean + @ConditionalOnMissingBean(LoanChargeService.class) + public LoanChargeService loanChargeService(LoanChargeValidator loanChargeValidator) { + return new LoanChargeService(loanChargeValidator); + } + + @Bean + @ConditionalOnMissingBean(LoanScheduleService.class) + public LoanScheduleService loanScheduleService(LoanChargeService loanChargeService) { + return new LoanScheduleService(loanChargeService); + } + + @Bean + @ConditionalOnMissingBean(LoanOfficerService.class) + public LoanOfficerService loanOfficerService(LoanOfficerValidator loanOfficerValidator) { + return new LoanOfficerService(loanOfficerValidator); + } + + @Bean + @ConditionalOnMissingBean(LoanRefundService.class) + public LoanRefundService loanRefundService(LoanRefundValidator loanRefundValidator) { + return new LoanRefundService(loanRefundValidator); + } } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/transfer/service/TransferWritePlatformServiceJpaRepositoryImpl.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/transfer/service/TransferWritePlatformServiceJpaRepositoryImpl.java index aea94a2a045..e0155dc74e1 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/transfer/service/TransferWritePlatformServiceJpaRepositoryImpl.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/transfer/service/TransferWritePlatformServiceJpaRepositoryImpl.java @@ -52,6 +52,7 @@ import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService; import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; import org.apache.fineract.portfolio.note.service.NoteWritePlatformService; import org.apache.fineract.portfolio.savings.domain.SavingsAccount; @@ -81,6 +82,7 @@ public class TransferWritePlatformServiceJpaRepositoryImpl implements TransferWr private final StaffRepositoryWrapper staffRepositoryWrapper; private final ClientTransferDetailsRepositoryWrapper clientTransferDetailsRepositoryWrapper; private final PlatformSecurityContext context; + private final LoanOfficerService loanOfficerService; @Override @Transactional @@ -216,9 +218,9 @@ else if (destinationGroupLoanOfficer != null) { loan.updateGroup(destinationGroup); if (inheritDestinationGroupLoanOfficer != null && inheritDestinationGroupLoanOfficer == true && destinationGroupLoanOfficer != null) { - loan.reassignLoanOfficer(destinationGroupLoanOfficer, DateUtils.getBusinessLocalDate()); + loanOfficerService.reassignLoanOfficer(loan, destinationGroupLoanOfficer, DateUtils.getBusinessLocalDate()); } else if (newLoanOfficer != null) { - loan.reassignLoanOfficer(newLoanOfficer, DateUtils.getBusinessLocalDate()); + loanOfficerService.reassignLoanOfficer(loan, newLoanOfficer, DateUtils.getBusinessLocalDate()); } this.loanRepositoryWrapper.saveAndFlush(loan); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/transfer/starter/TransferConfiguration.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/transfer/starter/TransferConfiguration.java index d83e630b528..9a0c666dc51 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/transfer/starter/TransferConfiguration.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/transfer/starter/TransferConfiguration.java @@ -26,6 +26,7 @@ import org.apache.fineract.portfolio.client.domain.ClientTransferDetailsRepositoryWrapper; import org.apache.fineract.portfolio.group.domain.GroupRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; +import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService; import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService; import org.apache.fineract.portfolio.note.service.NoteWritePlatformService; import org.apache.fineract.portfolio.savings.domain.SavingsAccountRepositoryWrapper; @@ -49,10 +50,11 @@ public TransferWritePlatformService transferWritePlatformService(ClientRepositor NoteWritePlatformService noteWritePlatformService, StaffRepositoryWrapper staffRepositoryWrapper, SavingsAccountRepositoryWrapper savingsAccountRepositoryWrapper, SavingsAccountWritePlatformService savingsAccountWritePlatformService, - ClientTransferDetailsRepositoryWrapper clientTransferDetailsRepositoryWrapper, PlatformSecurityContext context) { + ClientTransferDetailsRepositoryWrapper clientTransferDetailsRepositoryWrapper, PlatformSecurityContext context, + LoanOfficerService loanOfficerService) { return new TransferWritePlatformServiceJpaRepositoryImpl(clientRepositoryWrapper, officeRepository, calendarInstanceRepository, groupRepository, loanWritePlatformService, savingsAccountWritePlatformService, loanRepositoryWrapper, savingsAccountRepositoryWrapper, transfersDataValidator, noteWritePlatformService, staffRepositoryWrapper, - clientTransferDetailsRepositoryWrapper, context); + clientTransferDetailsRepositoryWrapper, context, loanOfficerService); } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImplTest.java index f2d74fbb991..b593f2b3a85 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanChargeWritePlatformServiceImplTest.java @@ -46,6 +46,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeApiJsonValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; @@ -124,6 +125,9 @@ class LoanChargeWritePlatformServiceImplTest { @Mock private JournalEntryWritePlatformService journalEntryWritePlatformService; + @Mock + private LoanChargeValidator loanChargeValidator; + @BeforeEach void setUp() { when(loanAssembler.assembleFrom(LOAN_ID)).thenReturn(loan); diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImplTest.java index 5670c7869bf..14c233033b6 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanDownPaymentHandlerServiceImplTest.java @@ -21,24 +21,38 @@ import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertNull; import static org.mockito.ArgumentMatchers.any; -import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import java.math.BigDecimal; import java.math.MathContext; import java.math.RoundingMode; +import java.time.LocalDate; +import org.apache.fineract.infrastructure.configuration.service.TemporaryConfigurationServiceContainer; import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.service.DateUtils; +import org.apache.fineract.infrastructure.core.service.MathUtil; import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionDownPaymentPostBusinessEvent; import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionDownPaymentPreBusinessEvent; import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService; import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency; +import org.apache.fineract.organisation.monetary.domain.Money; import org.apache.fineract.organisation.monetary.domain.MoneyHelper; +import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; +import org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail; import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; +import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; +import org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanDownPaymentTransactionValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanRefundValidator; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct; +import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail; import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -54,7 +68,11 @@ @MockitoSettings(strictness = Strictness.LENIENT) public class LoanDownPaymentHandlerServiceImplTest { - private final MockedStatic moneyHelper = Mockito.mockStatic(MoneyHelper.class); + private final MockedStatic moneyHelper = mockStatic(MoneyHelper.class); + private final MockedStatic mathUtilMock = mockStatic(MathUtil.class); + private final MockedStatic dateUtilsMock = mockStatic(DateUtils.class); + private final MockedStatic tempConfigServiceMock = mockStatic( + TemporaryConfigurationServiceContainer.class); @Mock private BusinessEventNotifierService businessEventNotifierService; @@ -66,42 +84,95 @@ public class LoanDownPaymentHandlerServiceImplTest { private LoanTransaction loanTransaction; @Mock - private ScheduleGeneratorDTO scheduleGeneratorDTO; + private JsonCommand command; @Mock - private JsonCommand command; + private LoanDownPaymentTransactionValidator loanDownPaymentTransactionValidator; + + @Mock + private LoanScheduleService loanScheduleService; + + @Mock + private LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor; + + @Mock + private LoanRefundService loanRefundService; + + @Mock + private LoanRefundValidator loanRefundValidator; private LoanDownPaymentHandlerServiceImpl underTest; @BeforeEach public void setUp() { - underTest = new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService); + underTest = new LoanDownPaymentHandlerServiceImpl(loanTransactionRepository, businessEventNotifierService, + loanDownPaymentTransactionValidator, loanScheduleService, loanRefundService, loanRefundValidator); moneyHelper.when(MoneyHelper::getMathContext).thenReturn(new MathContext(12, RoundingMode.UP)); moneyHelper.when(MoneyHelper::getRoundingMode).thenReturn(RoundingMode.UP); + tempConfigServiceMock.when(TemporaryConfigurationServiceContainer::isExternalIdAutoGenerationEnabled).thenReturn(true); } @AfterEach public void reset() { moneyHelper.close(); + tempConfigServiceMock.close(); + mathUtilMock.close(); + dateUtilsMock.close(); } @Test public void testDownPaymentHandler() { // given - Loan loanForProcessing = Mockito.mock(Loan.class); - LoanTransaction disbursement = Mockito.mock(LoanTransaction.class); - MonetaryCurrency loanCurrency = Mockito.mock(MonetaryCurrency.class); + final Loan loanForProcessing = Mockito.mock(Loan.class); + final LoanTransaction disbursement = Mockito.mock(LoanTransaction.class); + final Money mockAdjustedDownPaymentMoney = Mockito.mock(Money.class); + final LoanProductRelatedDetail loanRepaymentRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class); + final LoanProduct loanProduct = Mockito.mock(LoanProduct.class); + final LoanProductRelatedDetail loanProductRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class); + final ScheduleGeneratorDTO scheduleGeneratorDTO = Mockito.mock(ScheduleGeneratorDTO.class); + final HolidayDetailDTO holidayDetailDTO = Mockito.mock(HolidayDetailDTO.class); + final ChangedTransactionDetail changedTransactionDetail = Mockito.mock(ChangedTransactionDetail.class); + + final MonetaryCurrency loanCurrency = new MonetaryCurrency("USD", 2, 1); + + final Money downPaymentMoney = Mockito.mock(Money.class); + final Money overPaymentPortionMoney = Mockito.mock(Money.class); + final Money calculatedMoney = Money.of(loanCurrency, BigDecimal.valueOf(500)); + + when(downPaymentMoney.getCurrencyCode()).thenReturn(loanCurrency.getCode()); + when(overPaymentPortionMoney.getCurrencyCode()).thenReturn(loanCurrency.getCode()); + + when(loanForProcessing.getLoanRepaymentScheduleDetail()).thenReturn(loanRepaymentRelatedDetail); + when(loanForProcessing.repaymentScheduleDetail()).thenReturn(loanRepaymentRelatedDetail); + when(loanRepaymentRelatedDetail.isInterestRecalculationEnabled()).thenReturn(true); + when(loanRepaymentRelatedDetail.getDisbursedAmountPercentageForDownPayment()).thenReturn(BigDecimal.valueOf(10)); + when(loanForProcessing.getCurrency()).thenReturn(loanCurrency); + when(loanForProcessing.loanCurrency()).thenReturn(loanCurrency); + when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); + when(loanProduct.getInstallmentAmountInMultiplesOf()).thenReturn(10); + when(loanForProcessing.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail); + when(loanForProcessing.getTransactionProcessor()).thenReturn(loanRepaymentScheduleTransactionProcessor); + when(loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(any(), any(), any(), any(), any())) + .thenReturn(changedTransactionDetail); + when(loanProductRelatedDetail.getLoanScheduleType()).thenReturn(LoanScheduleType.PROGRESSIVE); + + when(disbursement.getOverPaymentPortion(loanCurrency)).thenReturn(overPaymentPortionMoney); + + when(downPaymentMoney.minus(overPaymentPortionMoney)).thenReturn(calculatedMoney); + mathUtilMock.when(() -> MathUtil.negativeToZero(any(Money.class))).thenReturn(mockAdjustedDownPaymentMoney); + when(mockAdjustedDownPaymentMoney.isGreaterThanZero()).thenReturn(true); + + dateUtilsMock.when(DateUtils::getBusinessLocalDate).thenReturn(LocalDate.of(2024, 11, 19)); + + when(scheduleGeneratorDTO.getHolidayDetailDTO()).thenReturn(holidayDetailDTO); + doNothing().when(businessEventNotifierService).notifyPreBusinessEvent(any(LoanTransactionDownPaymentPreBusinessEvent.class)); doNothing().when(businessEventNotifierService).notifyPostBusinessEvent(any(LoanTransactionDownPaymentPostBusinessEvent.class)); doNothing().when(businessEventNotifierService).notifyPostBusinessEvent(any(LoanBalanceChangedBusinessEvent.class)); when(loanTransactionRepository.saveAndFlush(any(LoanTransaction.class))).thenReturn(loanTransaction); - when(loanForProcessing.handleDownPayment(eq(disbursement), eq(command), eq(scheduleGeneratorDTO))).thenReturn(loanTransaction); - when(loanForProcessing.getCurrency()).thenReturn(loanCurrency); - when(loanCurrency.getCode()).thenReturn("CODE"); - when(loanCurrency.getCurrencyInMultiplesOf()).thenReturn(1); - when(loanCurrency.getDigitsAfterDecimal()).thenReturn(1); + // when - LoanTransaction actual = underTest.handleDownPayment(scheduleGeneratorDTO, command, disbursement, loanForProcessing); + final LoanTransaction actual = underTest.handleDownPayment(scheduleGeneratorDTO, command, disbursement, loanForProcessing); // then assertNotNull(actual); @@ -110,21 +181,48 @@ public void testDownPaymentHandler() { verify(businessEventNotifierService, Mockito.times(1)) .notifyPostBusinessEvent(Mockito.any(LoanTransactionDownPaymentPostBusinessEvent.class)); verify(businessEventNotifierService, Mockito.times(1)).notifyPostBusinessEvent(Mockito.any(LoanBalanceChangedBusinessEvent.class)); - verify(loanForProcessing, Mockito.times(1)).handleDownPayment(eq(disbursement), eq(command), eq(scheduleGeneratorDTO)); } @Test public void testDownPaymentHandlerNoNewTransaction() { // given - Loan loanForProcessing = Mockito.mock(Loan.class); - LoanTransaction disbursement = Mockito.mock(LoanTransaction.class); - MonetaryCurrency loanCurrency = Mockito.mock(MonetaryCurrency.class); - doNothing().when(businessEventNotifierService).notifyPreBusinessEvent(any(LoanTransactionDownPaymentPreBusinessEvent.class)); - when(loanForProcessing.handleDownPayment(eq(disbursement), eq(command), eq(scheduleGeneratorDTO))).thenReturn(null); + final Loan loanForProcessing = Mockito.mock(Loan.class); + final LoanTransaction disbursement = Mockito.mock(LoanTransaction.class); + final Money mockAdjustedDownPaymentMoney = Mockito.mock(Money.class); + final LoanProductRelatedDetail loanRepaymentRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class); + final LoanProduct loanProduct = Mockito.mock(LoanProduct.class); + final LoanProductRelatedDetail loanProductRelatedDetail = Mockito.mock(LoanProductRelatedDetail.class); + final ScheduleGeneratorDTO scheduleGeneratorDTO = Mockito.mock(ScheduleGeneratorDTO.class); + final HolidayDetailDTO holidayDetailDTO = Mockito.mock(HolidayDetailDTO.class); + + final MonetaryCurrency loanCurrency = new MonetaryCurrency("USD", 2, 1); + + final Money downPaymentMoney = Mockito.mock(Money.class); + final Money overPaymentPortionMoney = Mockito.mock(Money.class); + final Money calculatedMoney = Money.of(loanCurrency, BigDecimal.valueOf(500)); + + when(downPaymentMoney.getCurrencyCode()).thenReturn(loanCurrency.getCode()); + when(overPaymentPortionMoney.getCurrencyCode()).thenReturn(loanCurrency.getCode()); + + when(loanForProcessing.getLoanRepaymentScheduleDetail()).thenReturn(loanRepaymentRelatedDetail); + when(loanRepaymentRelatedDetail.getDisbursedAmountPercentageForDownPayment()).thenReturn(BigDecimal.valueOf(10)); when(loanForProcessing.getCurrency()).thenReturn(loanCurrency); - when(loanCurrency.getCode()).thenReturn("CODE"); - when(loanCurrency.getCurrencyInMultiplesOf()).thenReturn(1); - when(loanCurrency.getDigitsAfterDecimal()).thenReturn(1); + when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct); + when(loanForProcessing.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail); + when(loanProductRelatedDetail.getLoanScheduleType()).thenReturn(LoanScheduleType.PROGRESSIVE); + + when(disbursement.getOverPaymentPortion(loanCurrency)).thenReturn(overPaymentPortionMoney); + + when(downPaymentMoney.minus(overPaymentPortionMoney)).thenReturn(calculatedMoney); + mathUtilMock.when(() -> MathUtil.negativeToZero(any(Money.class))).thenReturn(mockAdjustedDownPaymentMoney); + when(mockAdjustedDownPaymentMoney.isGreaterThanZero()).thenReturn(false); + + dateUtilsMock.when(DateUtils::getBusinessLocalDate).thenReturn(LocalDate.of(2024, 11, 19)); + + when(scheduleGeneratorDTO.getHolidayDetailDTO()).thenReturn(holidayDetailDTO); + + doNothing().when(businessEventNotifierService).notifyPreBusinessEvent(any(LoanTransactionDownPaymentPreBusinessEvent.class)); + // when LoanTransaction actual = underTest.handleDownPayment(scheduleGeneratorDTO, command, disbursement, loanForProcessing); @@ -135,7 +233,6 @@ public void testDownPaymentHandlerNoNewTransaction() { verify(businessEventNotifierService, Mockito.never()) .notifyPostBusinessEvent(Mockito.any(LoanTransactionDownPaymentPostBusinessEvent.class)); verify(businessEventNotifierService, Mockito.never()).notifyPostBusinessEvent(Mockito.any(LoanBalanceChangedBusinessEvent.class)); - verify(loanForProcessing, Mockito.times(1)).handleDownPayment(eq(disbursement), eq(command), eq(scheduleGeneratorDTO)); verify(loanTransactionRepository, Mockito.never()).saveAndFlush(any(LoanTransaction.class)); } } diff --git a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImplTest.java b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImplTest.java index 828c608c5d7..3f3923e8e24 100644 --- a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImplTest.java +++ b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImplTest.java @@ -76,6 +76,7 @@ import org.apache.fineract.portfolio.loanaccount.guarantor.service.GuarantorDomainService; import org.apache.fineract.portfolio.loanaccount.loanschedule.service.LoanScheduleHistoryWritePlatformService; import org.apache.fineract.portfolio.loanaccount.serialization.LoanApplicationValidator; +import org.apache.fineract.portfolio.loanaccount.serialization.LoanChargeValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator; import org.apache.fineract.portfolio.loanaccount.serialization.LoanUpdateCommandFromApiJsonDeserializer; import org.apache.fineract.portfolio.note.domain.NoteRepository; @@ -193,6 +194,8 @@ class LoanWritePlatformServiceJpaRepositoryImplTest { private LoanTransactionAssembler loanTransactionAssembler; @Mock private LoanAccrualsProcessingService loanAccrualsProcessingService; + @Mock + private LoanChargeValidator loanChargeValidator; @Test void givenMerchantIssuedRefundTransactionWithRelatedTransactions_whenAdjustExistingTransaction_thenRelatedTransactionsAreReversedAndEventsTriggered() { @@ -235,7 +238,7 @@ void givenMerchantIssuedRefundTransactionWithRelatedTransactions_whenAdjustExist // Mock methods called inside adjustExistingTransaction when(loan.findExistingTransactionIds()).thenReturn(Collections.emptyList()); when(loan.findExistingReversedTransactionIds()).thenReturn(Collections.emptyList()); - doNothing().when(loan).validateActivityNotBeforeClientOrGroupTransferDate(any(), any()); + doNothing().when(loanTransactionValidator).validateActivityNotBeforeClientOrGroupTransferDate(any(), any(), any()); when(loan.isClosedWrittenOff()).thenReturn(false); when(loan.isClosedObligationsMet()).thenReturn(false); when(loan.isClosedWithOutstandingAmountMarkedForReschedule()).thenReturn(false); @@ -282,7 +285,7 @@ void givenNonMerchantIssuedRefundTransaction_whenAdjustExistingTransaction_thenN // Mock methods called inside adjustExistingTransaction when(loan.findExistingTransactionIds()).thenReturn(Collections.emptyList()); when(loan.findExistingReversedTransactionIds()).thenReturn(Collections.emptyList()); - doNothing().when(loan).validateActivityNotBeforeClientOrGroupTransferDate(any(), any()); + doNothing().when(loanTransactionValidator).validateActivityNotBeforeClientOrGroupTransferDate(any(), any(), any()); when(loan.isClosedWrittenOff()).thenReturn(false); when(loan.isClosedObligationsMet()).thenReturn(false); when(loan.isClosedWithOutstandingAmountMarkedForReschedule()).thenReturn(false);