From 4198b0bcf9a94e1c7aa0f386317ba71f40ceb1d9 Mon Sep 17 00:00:00 2001 From: Adam Saghy Date: Thu, 24 Oct 2024 11:52:59 +0200 Subject: [PATCH] FINERACT-2107: Extract Merchant issued refund and Payout refund handling --- .../portfolio/loanaccount/domain/Loan.java | 12 +- .../domain/LoanAccountDomainService.java | 11 +- .../loanaccount/domain/LoanTransaction.java | 6 + ...oanMerchantIssuedRefundCommandHandler.java | 14 +- .../LoanPayoutRefundCommandHandler.java | 14 +- .../service/LoanWritePlatformService.java | 2 + .../domain/LoanAccountDomainServiceJpa.java | 202 ++++++++++++--- .../LoanTransactionValidator.java | 107 ++++++-- .../LoanChargeWritePlatformServiceImpl.java | 18 +- ...WritePlatformServiceJpaRepositoryImpl.java | 236 ++++++++---------- 10 files changed, 386 insertions(+), 236 deletions(-) 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 38d5bc01e..45ecfe5a6 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 @@ -2352,7 +2352,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { loanTransaction.updateLoan(this); - final boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(loanTransaction, getLoanTransactions()); + final boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(loanTransaction); if (loanTransaction.isNotZero(loanCurrency())) { addLoanTransaction(loanTransaction); @@ -2396,7 +2396,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = this.transactionProcessorFactory .determineProcessor(this.transactionProcessingStrategyCode); - final LoanRepaymentScheduleInstallment currentInstallment = fetchLoanRepaymentScheduleInstallment( + final LoanRepaymentScheduleInstallment currentInstallment = fetchLoanRepaymentScheduleInstallmentByDueDate( loanTransaction.getTransactionDate()); boolean reprocess = isForeclosure() || !isTransactionChronologicallyLatest || adjustedTransaction != null @@ -2471,7 +2471,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { return loanTransactionDate; } - public LoanRepaymentScheduleInstallment fetchLoanRepaymentScheduleInstallment(LocalDate dueDate) { + public LoanRepaymentScheduleInstallment fetchLoanRepaymentScheduleInstallmentByDueDate(LocalDate dueDate) { return getRepaymentScheduleInstallments().stream() // .filter(installment -> dueDate.equals(installment.getDueDate())).findFirst() // .orElse(null); @@ -2504,8 +2504,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { .sorted(LoanTransactionComparator.INSTANCE).collect(Collectors.toList()); } - private boolean doPostLoanTransactionChecks(final LocalDate transactionDate, - final LoanLifecycleStateMachine loanLifecycleStateMachine) { + public boolean doPostLoanTransactionChecks(final LocalDate transactionDate, final LoanLifecycleStateMachine loanLifecycleStateMachine) { boolean statusChanged = false; boolean isOverpaid = MathUtil.isGreaterThanZero(totalOverpaid); if (isOverpaid) { @@ -2549,8 +2548,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom { this.actualMaturityDate = null; } - private boolean isChronologicallyLatestRepaymentOrWaiver(final LoanTransaction loanTransaction, - final List loanTransactions) { + public boolean isChronologicallyLatestRepaymentOrWaiver(final LoanTransaction loanTransaction) { boolean isChronologicallyLatestRepaymentOrWaiver = true; final LocalDate currentTransactionDate = loanTransaction.getTransactionDate(); diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java index 965ccf225..2644d5a2a 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java @@ -23,11 +23,13 @@ import java.time.LocalDate; import java.util.List; import java.util.Map; import java.util.Set; +import org.apache.commons.lang3.tuple.Pair; import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.portfolio.delinquency.validator.LoanDelinquencyActionData; import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO; import org.apache.fineract.portfolio.loanaccount.data.LoanRefundRequestData; +import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; public interface LoanAccountDomainService { @@ -54,8 +56,6 @@ public interface LoanAccountDomainService { LoanTransaction makeRefundForActiveLoan(Long accountId, CommandProcessingResultBuilder builderResult, LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, String noteText, ExternalId txnExternalId); - void updateLoanCollateralTransaction(Set loanCollateralManagementList); - void updateLoanCollateralStatus(Set loanCollateralManagementSet, boolean isReleased); /** @@ -94,6 +94,13 @@ public interface LoanAccountDomainService { LoanTransaction creditBalanceRefund(Loan loan, LocalDate transactionDate, BigDecimal transactionAmount, String noteText, ExternalId externalId, PaymentDetail paymentDetail); + Pair makeRefund(Loan loan, ScheduleGeneratorDTO scheduleGeneratorDTO, + LoanTransactionType loanTransactionType, LocalDate transactionDate, BigDecimal transactionAmount, PaymentDetail paymentDetail, + ExternalId txnExternalId); + + void updateAndSavePostDatedChecksForIndividualAccount(Loan loan, LoanTransaction transaction); + LoanTransaction applyInterestRefund(Loan loan, LoanRefundRequestData loanRefundRequest); + void updateAndSaveLoanCollateralTransactionsForIndividualAccounts(Loan loan, LoanTransaction transaction); } 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 469c0b527..7a416c567 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 @@ -206,6 +206,12 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom loanCollateralManagementSet) { - this.loanCollateralManagementRepository.saveAll(loanCollateralManagementSet); - } - @Transactional @Override public void updateLoanCollateralStatus(Set loanCollateralManagementSet, boolean isReleased) { @@ -165,8 +165,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { this.loanCollateralManagementRepository.saveAll(loanCollateralManagementSet); } - @Transactional - public LoanTransaction createInterestRefundLoanTransaction(Loan loan, final LocalDate transactionDate, + private LoanTransaction createInterestRefundLoanTransaction(Loan loan, final LocalDate transactionDate, BigDecimal relatedRefundTransactionAmount) { InterestRefundService interestRefundService = interestRefundServiceDelegate.lookupInterestRefundService(loan); if (interestRefundService == null) { @@ -174,15 +173,9 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { } BigDecimal interestRefundAmount = interestRefundService.calculateInterestRefundAmount(loan.getId(), relatedRefundTransactionAmount, transactionDate); - final ExternalId txnExternalId = externalIdFactory.create(); - businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionInterestRefundPreBusinessEvent(loan)); - - LoanTransaction newInterestRefundTransaction; - newInterestRefundTransaction = LoanTransaction.interestRefund(loan, interestRefundAmount, transactionDate, txnExternalId); - - return newInterestRefundTransaction; + return LoanTransaction.interestRefund(loan, interestRefundAmount, transactionDate, txnExternalId); } @Transactional @@ -225,18 +218,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom, holidayDetailDto); - boolean shouldCreateInterestRefundTransaction = loan.getLoanProductRelatedDetail().getSupportedInterestRefundTypes().stream() - .map(LoanSupportedInterestRefundTypes::getTransactionType) - .anyMatch(transactionType -> transactionType.equals(repaymentTransactionType)); - LoanTransaction newInterestRefundTransaction = null; - - if (shouldCreateInterestRefundTransaction) { - newInterestRefundTransaction = createInterestRefundLoanTransaction(loan, transactionDate, transactionAmount); - if (newInterestRefundTransaction != null) { - loan.addLoanTransaction(newInterestRefundTransaction); - } - } - final ChangedTransactionDetail changedTransactionDetail = loan.makeRepayment(newRepaymentTransaction, defaultLoanLifecycleStateMachine, existingTransactionIds, existingReversedTransactionIds, isRecoveryRepayment, scheduleGeneratorDTO, isHolidayValidationDone); @@ -247,9 +228,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { } saveLoanTransactionWithDataIntegrityViolationChecks(newRepaymentTransaction); - if (newInterestRefundTransaction != null) { - saveLoanTransactionWithDataIntegrityViolationChecks(newInterestRefundTransaction); - } /*** * TODO Vishwas Batch save is giving me a HibernateOptimisticLockingFailureException, looping and saving for the @@ -259,7 +237,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { if (changedTransactionDetail != null) { for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); - updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); + updateLoanTransferTransaction(mapEntry.getKey(), mapEntry.getValue()); } // Trigger transaction replayed event replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); @@ -316,10 +294,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { } } } - if (shouldCreateInterestRefundTransaction && newInterestRefundTransaction != null) { - businessEventNotifierService - .notifyPostBusinessEvent(new LoanTransactionInterestRefundPostBusinessEvent(newInterestRefundTransaction)); - } + return newRepaymentTransaction; } @@ -649,7 +624,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { } } - private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) { + private void updateLoanTransferTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) { final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId); if (transferTransaction != null) { transferTransaction.updateToLoanTransaction(newLoanTransaction); @@ -810,7 +785,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { if (changedTransactionDetail != null) { for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); - updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); + updateLoanTransferTransaction(mapEntry.getKey(), mapEntry.getValue()); } // Trigger transaction replayed event replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); @@ -847,6 +822,158 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { } } + @Override + public void updateAndSaveLoanCollateralTransactionsForIndividualAccounts(Loan loan, LoanTransaction loanTransaction) { + if (loan.getLoanType().isIndividualAccount()) { + Set loanCollateralManagements = loan.getLoanCollateralManagements(); + for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { + if (loanTransaction != null) { + loanCollateralManagement.setLoanTransactionData(loanTransaction); + } + ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); + + if (loan.getStatus().isClosed()) { + loanCollateralManagement.setIsReleased(true); + BigDecimal quantity = loanCollateralManagement.getQuantity(); + clientCollateralManagement.updateQuantity(clientCollateralManagement.getQuantity().add(quantity)); + loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement); + } + } + this.loanCollateralManagementRepository.saveAll(loanCollateralManagements); + } + } + + @Override + public Pair makeRefund(final Loan loan, final ScheduleGeneratorDTO scheduleGeneratorDTO, + final LoanTransactionType loanTransactionType, final LocalDate transactionDate, final BigDecimal transactionAmount, + final PaymentDetail paymentDetail, final ExternalId txnExternalId) { + // Pre-processing business event + switch (loanTransactionType) { + case MERCHANT_ISSUED_REFUND -> + businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionMerchantIssuedRefundPreBusinessEvent(loan)); + case PAYOUT_REFUND -> + businessEventNotifierService.notifyPreBusinessEvent(new LoanTransactionPayoutRefundPreBusinessEvent(loan)); + default -> + throw new UnsupportedOperationException(String.format("Not configured loan transaction type: %s!", loanTransactionType)); + } + LoanTransaction refundTransaction = LoanTransaction.refund(loan, loanTransactionType, transactionAmount, paymentDetail, + transactionDate, txnExternalId); + + final boolean isTransactionChronologicallyLatest = loan.isChronologicallyLatestRepaymentOrWaiver(refundTransaction); + loan.getLoanTransactions().add(refundTransaction); + + boolean shouldCreateInterestRefundTransaction = loan.getLoanProductRelatedDetail().getSupportedInterestRefundTypes().stream() + .map(LoanSupportedInterestRefundTypes::getTransactionType) + .anyMatch(transactionType -> transactionType.equals(loanTransactionType)); + LoanTransaction interestRefundTransaction = null; + + if (shouldCreateInterestRefundTransaction) { + interestRefundTransaction = createInterestRefundLoanTransaction(loan, transactionDate, transactionAmount); + if (interestRefundTransaction != null) { + loan.addLoanTransaction(interestRefundTransaction); + } + } + + final LoanRepaymentScheduleTransactionProcessor loanRepaymentScheduleTransactionProcessor = transactionProcessorFactory + .determineProcessor(loan.getTransactionProcessingStrategyCode()); + + final LoanRepaymentScheduleInstallment currentInstallment = loan.fetchLoanRepaymentScheduleInstallmentByDueDate(transactionDate); + + boolean reprocess = loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled() // + || loan.isForeclosure() // + || !isTransactionChronologicallyLatest // + || !DateUtils.isEqualBusinessDate(transactionDate) // + || currentInstallment == null // + || !currentInstallment.getTotalOutstanding(loan.getCurrency()).isEqualTo(refundTransaction.getAmount(loan.getCurrency())); // + + if (!reprocess) { + loanRepaymentScheduleTransactionProcessor.processLatestTransaction(refundTransaction, + new TransactionCtx(loan.getCurrency(), loan.getRepaymentScheduleInstallments(), loan.getActiveCharges(), + new MoneyHolder(loan.getTotalOverpaidAsMoney()), null)); + } else { + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { + loan.regenerateRepaymentScheduleWithInterestRecalculation(scheduleGeneratorDTO); + } + final List allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing(); + ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions( + loan.getDisbursementDate(), allNonContraTransactionsPostDisbursement, loan.getCurrency(), + loan.getRepaymentScheduleInstallments(), loan.getActiveCharges()); + for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + mapEntry.getValue().updateLoan(loan); + } + + loan.getLoanTransactions().addAll(changedTransactionDetail.getNewTransactionMappings().values()); + + // Store and flush newly created transaction to generate PK + saveLoanTransactionWithDataIntegrityViolationChecks(refundTransaction); + if (interestRefundTransaction != null) { + saveLoanTransactionWithDataIntegrityViolationChecks(interestRefundTransaction); + } + /* + * TODO Vishwas Batch save is giving me a HibernateOptimisticLockingFailureException, looping and saving for + * the time being, not a major issue for now as this loop is entered only in edge cases (when a payment is + * made before the latest payment recorded against the loan) + */ + // Apply reprocessed transactions (if applicable) + for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { + saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); + updateLoanTransferTransaction(mapEntry.getKey(), mapEntry.getValue()); + } + // Trigger transaction replayed event + replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); + } + + loan.updateLoanSummaryDerivedFields(); + loan.doPostLoanTransactionChecks(transactionDate, defaultLoanLifecycleStateMachine); + + switch (loanTransactionType) { + case MERCHANT_ISSUED_REFUND -> businessEventNotifierService + .notifyPostBusinessEvent(new LoanTransactionMerchantIssuedRefundPostBusinessEvent(refundTransaction)); + case PAYOUT_REFUND -> + businessEventNotifierService.notifyPostBusinessEvent(new LoanTransactionPayoutRefundPostBusinessEvent(refundTransaction)); + default -> + throw new UnsupportedOperationException(String.format("Not configured loan transaction type: %s!", loanTransactionType)); + } + + if (interestRefundTransaction != null) { + businessEventNotifierService + .notifyPostBusinessEvent(new LoanTransactionInterestRefundPostBusinessEvent(interestRefundTransaction)); + } + return Pair.of(refundTransaction, interestRefundTransaction); + } + + @Override + public void updateAndSavePostDatedChecksForIndividualAccount(final Loan loan, final LoanTransaction transaction) { + if (loan.getLoanType().isIndividualAccount()) { + // Mark Post Dated Check as paid. + final Set loanTransactionToRepaymentScheduleMappings = transaction + .getLoanTransactionToRepaymentScheduleMappings(); + + if (loanTransactionToRepaymentScheduleMappings != null) { + for (LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping : loanTransactionToRepaymentScheduleMappings) { + LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loanTransactionToRepaymentScheduleMapping + .getLoanRepaymentScheduleInstallment(); + if (loanRepaymentScheduleInstallment != null) { + final boolean isPaid = loanRepaymentScheduleInstallment.isNotFullyPaidOff(); + PostDatedChecks postDatedChecks = this.postDatedChecksRepository + .getPendingPostDatedCheck(loanRepaymentScheduleInstallment); + + if (postDatedChecks != null) { + if (!isPaid) { + postDatedChecks.setStatus(PostDatedChecksStatus.POST_DATED_CHECKS_PAID); + } else { + postDatedChecks.setStatus(PostDatedChecksStatus.POST_DATED_CHECKS_PENDING); + } + this.postDatedChecksRepository.saveAndFlush(postDatedChecks); + } else { + break; + } + } + } + } + } + } + @Override public LoanTransaction applyInterestRefund(final Loan loan, final LoanRefundRequestData loanRefundRequest) { final PaymentDetail paymentDetail = null; @@ -868,5 +995,4 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService { return interestRefundTransaction; } - } 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 6ee3a812e..317babae0 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 @@ -60,7 +60,6 @@ import org.apache.fineract.portfolio.calendar.domain.CalendarEntityType; import org.apache.fineract.portfolio.calendar.domain.CalendarInstance; import org.apache.fineract.portfolio.calendar.domain.CalendarInstanceRepository; import org.apache.fineract.portfolio.calendar.exception.NotValidRecurringDateException; -import org.apache.fineract.portfolio.calendar.service.CalendarUtils; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; import org.apache.fineract.portfolio.collateralmanagement.exception.LoanCollateralAmountNotSufficientException; @@ -77,6 +76,7 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleIns import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.exception.DateMismatchException; import org.apache.fineract.portfolio.loanaccount.exception.InvalidLoanStateTransitionException; import org.apache.fineract.portfolio.loanaccount.exception.LoanApplicationDateException; @@ -410,7 +410,10 @@ public final class LoanTransactionValidator { } public void validateNewRepaymentTransaction(final String json) { + validatePaymentTransaction(json); + } + private void validatePaymentTransaction(String json) { if (StringUtils.isBlank(json)) { throw new InvalidJsonException(); } @@ -439,21 +442,6 @@ public final class LoanTransactionValidator { throwExceptionIfValidationWarningsExist(dataValidationErrors); } - public void validateRepaymentDateWithMeetingDate(final LocalDate repaymentDate, final CalendarInstance calendarInstance) { - if (null != calendarInstance) { - final Calendar calendar = calendarInstance.getCalendar(); - if (calendar != null && repaymentDate != null) { - // Disbursement date should fall on a meeting date - if (!CalendarUtils.isValidRecurringDate(calendar.getRecurrence(), calendar.getStartDateLocalDate(), repaymentDate)) { - final String errorMessage = "Transaction date '" + repaymentDate.toString() + "' does not fall on a meeting date."; - throw new NotValidRecurringDateException("loan.transaction.date", errorMessage, repaymentDate.toString(), - calendar.getTitle()); - } - - } - } - } - 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); @@ -874,4 +862,91 @@ public final class LoanTransactionValidator { } } } + + public void validateRefund(String json) { + validatePaymentTransaction(json); + } + + public void validateRefund(final Loan loan, LoanTransactionType loanTransactionType, final LocalDate transactionDate, + ScheduleGeneratorDTO scheduleGeneratorDTO) { + checkClientOrGroupActive(loan); + validateLoanStatusIsActiveOrFullyPaidOrOverpaid(loan); + validateActivityNotBeforeClientOrGroupTransferDate(loan, transactionDate); + validateRepaymentTypeTransactionNotBeforeAChargeRefund(loan, loanTransactionType, transactionDate); + validateTransactionNotBeforeLastTransactionDate(loan, loanTransactionType, transactionDate); + validateRepaymentDateIsOnHoliday(transactionDate, scheduleGeneratorDTO.getHolidayDetailDTO().isAllowTransactionsOnHoliday(), + scheduleGeneratorDTO.getHolidayDetailDTO().getHolidays()); + validateRepaymentDateIsOnNonWorkingDay(transactionDate, scheduleGeneratorDTO.getHolidayDetailDTO().getWorkingDays(), + scheduleGeneratorDTO.getHolidayDetailDTO().isAllowTransactionsOnNonWorkingDay()); + validateTransactionShouldNotBeInTheFuture(transactionDate); + validateTransactionAmountNotExceedThresholdForMultiDisburseLoan(loan); + } + + private void checkClientOrGroupActive(final Loan loan) { + final Client client = loan.client(); + if (client != null && client.isNotActive()) { + throw new ClientNotActiveException(client.getId()); + } + final Group group = loan.group(); + if (group != null && group.isNotActive()) { + throw new GroupNotActiveException(group.getId()); + } + } + + private void validateActivityNotBeforeClientOrGroupTransferDate(final Loan loan, final LocalDate transactionDate) { + if (loan.getClient() != null && loan.getClient().getOfficeJoiningDate() != null) { + final LocalDate clientOfficeJoiningDate = loan.getClient().getOfficeJoiningDate(); + if (DateUtils.isBefore(transactionDate, clientOfficeJoiningDate)) { + String errorMessage = "The date on which the transaction is made cannot be earlier than client's transfer date to this office"; + String action = "repayment.or.waiver"; + String postfix = "cannot.be.made.before.client.transfer.date"; + throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, clientOfficeJoiningDate); + } + } + } + + 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()) + && loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) + || loan.getLoanProduct().isHoldGuaranteeFunds())) { + return; + } + LocalDate lastTransactionDate = loan.getLastUserTransactionDate(); + + String humanReadable; + String action; + switch (loanTransactionType) { + case MERCHANT_ISSUED_REFUND -> { + humanReadable = "merchant issued refund"; + action = "merchant.issued.refund"; + } + case PAYOUT_REFUND -> { + humanReadable = "payout refund"; + action = "payout.refund"; + } + default -> { + humanReadable = "transaction"; + action = "transaction"; + } + } + if (DateUtils.isAfter(lastTransactionDate, transactionDate)) { + String errorMessage = "The date on which the " + humanReadable + " is made cannot be earlier than last transaction date"; + String postfix = "cannot.be.made.before.last.transaction.date"; + throw new InvalidLoanStateTransitionException(action, postfix, errorMessage, lastTransactionDate); + } + } } 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 642036280..1a42976b1 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 @@ -85,7 +85,6 @@ import org.apache.fineract.portfolio.charge.exception.LoanChargeNotFoundExceptio import org.apache.fineract.portfolio.charge.exception.LoanChargeWaiveCannotBeReversedException; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; -import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagement; import org.apache.fineract.portfolio.common.domain.PeriodFrequencyType; import org.apache.fineract.portfolio.group.domain.Group; import org.apache.fineract.portfolio.group.exception.GroupNotActiveException; @@ -98,7 +97,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService 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.LoanCollateralManagement; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; import org.apache.fineract.portfolio.loanaccount.domain.LoanInstallmentCharge; @@ -727,21 +725,7 @@ public class LoanChargeWritePlatformServiceImpl implements LoanChargeWritePlatfo paymentDetail); // Update loan transaction on repayment. - if (loan.getLoanType().isIndividualAccount()) { - Set loanCollateralManagements = loan.getLoanCollateralManagements(); - for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { - loanCollateralManagement.setLoanTransactionData(loanTransaction); - ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); - - if (loan.getStatus().isClosed()) { - loanCollateralManagement.setIsReleased(true); - BigDecimal quantity = loanCollateralManagement.getQuantity(); - clientCollateralManagement.updateQuantity(clientCollateralManagement.getQuantity().add(quantity)); - loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement); - } - } - this.loanAccountDomainService.updateLoanCollateralTransaction(loanCollateralManagements); - } + loanAccountDomainService.updateAndSaveLoanCollateralTransactionsForIndividualAccounts(loan, loanTransaction); final String noteText = command.stringValueOfParameterNamed("note"); if (StringUtils.isNotBlank(noteText)) { 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 b1ae7900e..0c24d03b9 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 @@ -40,6 +40,7 @@ import java.util.Set; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.apache.commons.lang3.StringUtils; +import org.apache.commons.lang3.tuple.Pair; import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService; import org.apache.fineract.cob.exceptions.LoanAccountLockCannotBeOverruledException; import org.apache.fineract.cob.service.LoanAccountLockService; @@ -134,7 +135,6 @@ import org.apache.fineract.portfolio.calendar.domain.CalendarType; import org.apache.fineract.portfolio.calendar.exception.CalendarParameterUpdateNotSupportedException; import org.apache.fineract.portfolio.client.domain.Client; import org.apache.fineract.portfolio.client.exception.ClientNotActiveException; -import org.apache.fineract.portfolio.collateralmanagement.domain.ClientCollateralManagement; import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkDisbursalCommand; import org.apache.fineract.portfolio.collectionsheet.command.CollectionSheetBulkRepaymentCommand; import org.apache.fineract.portfolio.collectionsheet.command.SingleDisbursalCommand; @@ -151,7 +151,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.GroupLoanIndividualMonit import org.apache.fineract.portfolio.loanaccount.domain.Loan; import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService; import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge; -import org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement; import org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails; import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent; import org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine; @@ -170,7 +169,6 @@ import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelationTypeEnum; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository; -import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor; import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder; @@ -202,7 +200,6 @@ import org.apache.fineract.portfolio.note.domain.Note; import org.apache.fineract.portfolio.note.domain.NoteRepository; import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail; import org.apache.fineract.portfolio.paymentdetail.service.PaymentDetailWritePlatformService; -import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.data.PostDatedChecksStatus; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecks; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.domain.PostDatedChecksRepository; import org.apache.fineract.portfolio.repaymentwithpostdatedchecks.service.RepaymentWithPostDatedChecksAssembler; @@ -477,11 +474,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan.updateLoanScheduleDependentDerivedFields(); loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.loanNote(loan, noteText); - this.noteRepository.save(note); - } + createNote(loan, command, changes); // auto create standing instruction createStandingInstruction(loan); @@ -550,6 +543,15 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .build(); } + private void createNote(Loan loan, JsonCommand command, Map changes) { + final String noteText = command.stringValueOfParameterNamed("note"); + if (StringUtils.isNotBlank(noteText)) { + changes.put("note", noteText); + final Note note = Note.loanNote(loan, noteText); + this.noteRepository.save(note); + } + } + private ChangedTransactionDetail disburseLoan(JsonCommand command, boolean isPaymentTypeApplicableForDisbursementCharge, PaymentDetail paymentDetail, Loan loan, AppUser currentUser, Map changes, ScheduleGeneratorDTO scheduleGeneratorDTO) { @@ -813,11 +815,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } if (!changes.isEmpty()) { - final String noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.loanNote(loan, noteText); - this.noteRepository.save(note); - } + createNote(loan, command, changes); if (changedTransactionDetail != null) { for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings() .entrySet()) { @@ -929,13 +927,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); this.accountTransfersWritePlatformService.reverseAllTransactions(loanId, PortfolioAccountType.LOAN); String noteText; - if (command.hasParameter("note")) { - noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.loanNote(loan, noteText); - this.noteRepository.save(note); - } - } + createNote(loan, command, changes); boolean isAccountTransfer = false; final Map accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); @@ -1043,7 +1035,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf return statusChanged; } - private void updateLoanTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) { + private void updateLoanTransferTransaction(final Long loanTransactionId, final LoanTransaction newLoanTransaction) { final AccountTransferTransaction transferTransaction = this.accountTransferRepository.findByToLoanTransactionId(loanTransactionId); if (transferTransaction != null) { transferTransaction.updateToLoanTransaction(newLoanTransaction); @@ -1052,7 +1044,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf } // TODO check - public boolean isChronologicallyLatestRepaymentOrWaiver(final LoanTransaction loanTransaction, + public boolean isChronologicallyTheLatestTransaction(final LoanTransaction loanTransaction, final List loanTransactions) { boolean isChronologicallyLatestRepaymentOrWaiver = true; @@ -1082,8 +1074,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf ChangedTransactionDetail changedTransactionDetail = null; newInterestPaymentWaiverTransaction.updateLoan(loan); - final boolean isTransactionChronologicallyLatest = isChronologicallyLatestRepaymentOrWaiver(newInterestPaymentWaiverTransaction, - loan.getLoanTransactions()); + final boolean isTransactionChronologicallyLatest = loan + .isChronologicallyLatestRepaymentOrWaiver(newInterestPaymentWaiverTransaction); if (newInterestPaymentWaiverTransaction.isNotZero(loan.getLoanRepaymentScheduleDetail().getCurrency())) { loan.addLoanTransaction(newInterestPaymentWaiverTransaction); @@ -1093,7 +1085,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .determineProcessor(loan.getTransactionProcessingStrategyCode()); final LoanRepaymentScheduleInstallment currentInstallment = loan - .fetchLoanRepaymentScheduleInstallment(newInterestPaymentWaiverTransaction.getTransactionDate()); + .fetchLoanRepaymentScheduleInstallmentByDueDate(newInterestPaymentWaiverTransaction.getTransactionDate()); boolean reprocess = true; @@ -1254,8 +1246,8 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf // disable all active standing orders linked to this loan if status changes to closed loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan); - updateAndSavePostDatedChecksForIndividualAccount(loan, newInterestPaymentWaiverTransaction); - updateAndSaveLoanCollateralTransactionsForIndividualAccounts(loan, newInterestPaymentWaiverTransaction); + loanAccountDomainService.updateAndSavePostDatedChecksForIndividualAccount(loan, newInterestPaymentWaiverTransaction); + loanAccountDomainService.updateAndSaveLoanCollateralTransactionsForIndividualAccounts(loan, newInterestPaymentWaiverTransaction); // Finished Notification @@ -1286,62 +1278,13 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (changedTransactionDetail != null) { for (final Map.Entry mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) { loanAccountDomainService.saveLoanTransactionWithDataIntegrityViolationChecks(mapEntry.getValue()); - updateLoanTransaction(mapEntry.getKey(), mapEntry.getValue()); + updateLoanTransferTransaction(mapEntry.getKey(), mapEntry.getValue()); } // Trigger transaction replayed event replayedTransactionBusinessEventService.raiseTransactionReplayedEvents(changedTransactionDetail); } } - private void updateAndSavePostDatedChecksForIndividualAccount(Loan loan, LoanTransaction loanTransaction) { - if (loan.getLoanType().isIndividualAccount()) { - // Mark Post Dated Check as paid. - final Set loanTransactionToRepaymentScheduleMappings = loanTransaction - .getLoanTransactionToRepaymentScheduleMappings(); - - if (loanTransactionToRepaymentScheduleMappings != null) { - for (LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping : loanTransactionToRepaymentScheduleMappings) { - LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loanTransactionToRepaymentScheduleMapping - .getLoanRepaymentScheduleInstallment(); - if (loanRepaymentScheduleInstallment != null) { - final boolean isPaid = loanRepaymentScheduleInstallment.isNotFullyPaidOff(); - PostDatedChecks postDatedChecks = this.postDatedChecksRepository - .getPendingPostDatedCheck(loanRepaymentScheduleInstallment); - - if (postDatedChecks != null) { - if (!isPaid) { - postDatedChecks.setStatus(PostDatedChecksStatus.POST_DATED_CHECKS_PAID); - } else { - postDatedChecks.setStatus(PostDatedChecksStatus.POST_DATED_CHECKS_PENDING); - } - this.postDatedChecksRepository.saveAndFlush(postDatedChecks); - } else { - break; - } - } - } - } - } - } - - private void updateAndSaveLoanCollateralTransactionsForIndividualAccounts(Loan loan, LoanTransaction loanTransaction) { - if (loan.getLoanType().isIndividualAccount()) { - Set loanCollateralManagements = loan.getLoanCollateralManagements(); - for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { - loanCollateralManagement.setLoanTransactionData(loanTransaction); - ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); - - if (loan.getStatus().isClosed()) { - loanCollateralManagement.setIsReleased(true); - BigDecimal quantity = loanCollateralManagement.getQuantity(); - clientCollateralManagement.updateQuantity(clientCollateralManagement.getQuantity().add(quantity)); - loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement); - } - } - this.loanAccountDomainService.updateLoanCollateralTransaction(loanCollateralManagements); - } - } - @Transactional @Override public CommandProcessingResult makeLoanRepaymentWithChargeRefundChargeType(final LoanTransactionType repaymentTransactionType, @@ -1378,22 +1321,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf transactionAmount, paymentDetail, noteText, txnExternalId, isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone); loan = loanTransaction.getLoan(); - // Update loan transaction on repayment. - if (loan.getLoanType().isIndividualAccount()) { - Set loanCollateralManagements = loan.getLoanCollateralManagements(); - for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { - loanCollateralManagement.setLoanTransactionData(loanTransaction); - ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); - - if (loan.getStatus().isClosed()) { - loanCollateralManagement.setIsReleased(true); - BigDecimal quantity = loanCollateralManagement.getQuantity(); - clientCollateralManagement.updateQuantity(clientCollateralManagement.getQuantity().add(quantity)); - loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement); - } - } - this.loanAccountDomainService.updateLoanCollateralTransaction(loanCollateralManagements); - } + this.loanAccountDomainService.updateAndSaveLoanCollateralTransactionsForIndividualAccounts(loan, loanTransaction); return new CommandProcessingResultBuilder().withCommandId(command.commandId()) // .withLoanId(loan.getId()) // @@ -2018,20 +1946,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf businessEventNotifierService.notifyPostBusinessEvent(new LoanCloseBusinessEvent(loan)); // Update loan transaction on repayment. - if (loan.getLoanType().isIndividualAccount()) { - Set loanCollateralManagements = loan.getLoanCollateralManagements(); - for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { - ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); - - if (loan.getStatus().isClosed()) { - loanCollateralManagement.setIsReleased(true); - BigDecimal quantity = loanCollateralManagement.getQuantity(); - clientCollateralManagement.updateQuantity(clientCollateralManagement.getQuantity().add(quantity)); - loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement); - } - } - this.loanAccountDomainService.updateLoanCollateralTransaction(loanCollateralManagements); - } + this.loanAccountDomainService.updateAndSaveLoanCollateralTransactionsForIndividualAccounts(loan, null); // disable all active standing instructions linked to the loan this.loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan); @@ -2099,20 +2014,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf this.loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan); // Update loan transaction on repayment. - if (loan.getLoanType().isIndividualAccount()) { - Set loanCollateralManagements = loan.getLoanCollateralManagements(); - for (LoanCollateralManagement loanCollateralManagement : loanCollateralManagements) { - ClientCollateralManagement clientCollateralManagement = loanCollateralManagement.getClientCollateralManagement(); - - if (loan.getStatus().isClosed()) { - loanCollateralManagement.setIsReleased(true); - BigDecimal quantity = loanCollateralManagement.getQuantity(); - clientCollateralManagement.updateQuantity(clientCollateralManagement.getQuantity().add(quantity)); - loanCollateralManagement.setClientCollateralManagement(clientCollateralManagement); - } - } - this.loanAccountDomainService.updateLoanCollateralTransaction(loanCollateralManagements); - } + this.loanAccountDomainService.updateAndSaveLoanCollateralTransactionsForIndividualAccounts(loan, null); return new CommandProcessingResultBuilder() // .withCommandId(command.commandId()) // @@ -3157,14 +3059,7 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf if (!changes.isEmpty()) { loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); - String noteText; - if (command.hasParameter("note")) { - noteText = command.stringValueOfParameterNamed("note"); - if (StringUtils.isNotBlank(noteText)) { - final Note note = Note.loanNote(loan, noteText); - this.noteRepository.save(note); - } - } + createNote(loan, command, changes); boolean isAccountTransfer = false; final Map accountingBridgeData = loan.deriveAccountingBridgeData(currency.getCode(), existingTransactionIds, existingReversedTransactionIds, isAccountTransfer); @@ -3351,6 +3246,87 @@ public class LoanWritePlatformServiceJpaRepositoryImpl implements LoanWritePlatf .build(); } + @Override + public CommandProcessingResult makeRefund(final Long loanId, final LoanTransactionType loanTransactionType, final JsonCommand command) { + // API rule validations + this.loanTransactionValidator.validateRefund(command.json()); + + final LocalDate transactionDate = command.localDateValueOfParameterNamed("transactionDate"); + final BigDecimal transactionAmount = command.bigDecimalValueOfParameterNamed("transactionAmount"); + final ExternalId txnExternalId = externalIdFactory.createFromCommand(command, LoanApiConstants.externalIdParameterName); + + final Map changes = new LinkedHashMap<>(); + changes.put("transactionDate", command.stringValueOfParameterNamed("transactionDate")); + changes.put("transactionAmount", command.stringValueOfParameterNamed("transactionAmount")); + changes.put("locale", command.locale()); + changes.put("dateFormat", command.dateFormat()); + changes.put(LoanApiConstants.externalIdParameterName, txnExternalId); + + Loan loan = this.loanAssembler.assembleFrom(loanId); + // Build loan schedule generator dto + LocalDate recalculateFrom = loan.repaymentScheduleDetail().isInterestRecalculationEnabled() ? transactionDate : null; + final ScheduleGeneratorDTO scheduleGeneratorDTO = this.loanUtilService.buildScheduleGeneratorDTO(loan, recalculateFrom, null); + // Domain rule validations + this.loanTransactionValidator.validateRefund(loan, loanTransactionType, transactionDate, scheduleGeneratorDTO); + // Create payment details + final PaymentDetail paymentDetail = this.paymentDetailWritePlatformService.createAndPersistPaymentDetail(command, changes); + // Create note + createNote(loan, command, changes); + // Initial transaction ids for journal entry generation + final List existingTransactionIds = loan.findExistingTransactionIds(); + final List existingReversedTransactionIds = loan.findExistingReversedTransactionIds(); + // Create refund transaction(s) + Pair refundTransactions = loanAccountDomainService.makeRefund(loan, scheduleGeneratorDTO, + loanTransactionType, transactionDate, transactionAmount, paymentDetail, txnExternalId); + LoanTransaction refundTransaction = refundTransactions.getLeft(); + LoanTransaction interestRefundTransaction = refundTransactions.getRight(); + // Accrual reprocessing + if (loan.getLoanRepaymentScheduleDetail().isInterestRecalculationEnabled()) { + loanAccrualsProcessingService.reprocessExistingAccruals(loan); + loanAccrualsProcessingService.processIncomePostingAndAccruals(loan); + } + + loan = saveAndFlushLoanWithDataIntegrityViolationChecks(loan); + // Create journal entries for the new transaction(s) + postJournalEntries(loan, existingTransactionIds, existingReversedTransactionIds); + // Calculate and apply delinquency tag (if applicable) + loanAccountDomainService.setLoanDelinquencyTag(loan, transactionDate); + // disable all active standing orders linked to this loan if status + // changes to closed + loanAccountDomainService.disableStandingInstructionsLinkedToClosedLoan(loan); + //// Mark Post Dated Check as paid., Do we need this for refund? + loanAccountDomainService.updateAndSavePostDatedChecksForIndividualAccount(loan, refundTransaction); + if (interestRefundTransaction != null) { + loanAccountDomainService.updateAndSavePostDatedChecksForIndividualAccount(loan, refundTransaction); + } + // Collateral management + loanAccountDomainService.updateAndSaveLoanCollateralTransactionsForIndividualAccounts(loan, refundTransaction); + if (interestRefundTransaction != null) { + loanAccountDomainService.updateAndSaveLoanCollateralTransactionsForIndividualAccounts(loan, refundTransaction); + } + // Raise business events + loanAccrualTransactionBusinessEventService.raiseBusinessEventForAccrualTransactions(loan, existingTransactionIds); + loanAccrualsProcessingService.processAccrualsForInterestRecalculation(loan, + loan.repaymentScheduleDetail().isInterestRecalculationEnabled()); + businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan)); + + Long entityId = refundTransaction.getId(); + ExternalId entityExternalId = refundTransaction.getExternalId(); + Long subEntityId = interestRefundTransaction != null ? interestRefundTransaction.getId() : null; + ExternalId subEntityExternalId = interestRefundTransaction != null ? interestRefundTransaction.getExternalId() : null; + return new CommandProcessingResultBuilder().withCommandId(command.commandId()) // + .withLoanId(loan.getId()) // + .withEntityId(entityId) // + .withEntityExternalId(entityExternalId) // + .withSubEntityId(subEntityId)// + .withSubEntityExternalId(subEntityExternalId)// + .withOfficeId(loan.getOfficeId()) // + .withClientId(loan.getClientId()) // + .withGroupId(loan.getGroupId()) // + .with(changes) // + .build(); + } + private void validateIsMultiDisbursalLoanAndDisbursedMoreThanOneTranche(Loan loan) { if (!loan.isMultiDisburmentLoan()) { final String errorMessage = "loan.product.does.not.support.multiple.disbursals.cannot.undo.last.disbursal";