FINERACT-2107: Extract Merchant issued refund and Payout refund handling

This commit is contained in:
Adam Saghy
2024-10-24 15:51:41 +02:00
parent 393f4398ac
commit 4198b0bcf9
10 changed files with 386 additions and 236 deletions
@@ -2352,7 +2352,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
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<Long> {
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<Long> {
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<Long> {
.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<Long> {
this.actualMaturityDate = null;
}
private boolean isChronologicallyLatestRepaymentOrWaiver(final LoanTransaction loanTransaction,
final List<LoanTransaction> loanTransactions) {
public boolean isChronologicallyLatestRepaymentOrWaiver(final LoanTransaction loanTransaction) {
boolean isChronologicallyLatestRepaymentOrWaiver = true;
final LocalDate currentTransactionDate = loanTransaction.getTransactionDate();
@@ -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<LoanCollateralManagement> loanCollateralManagementList);
void updateLoanCollateralStatus(Set<LoanCollateralManagement> 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<LoanTransaction, LoanTransaction> 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);
}
@@ -206,6 +206,12 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
externalId);
}
public static LoanTransaction refund(Loan loan, LoanTransactionType loanTransactionType, BigDecimal transactionAmount,
PaymentDetail paymentDetail, LocalDate transactionDate, ExternalId txnExternalId) {
return new LoanTransaction(loan, loan.getOffice(), loanTransactionType, paymentDetail, transactionAmount, transactionDate,
txnExternalId);
}
public void setLoanTransactionToRepaymentScheduleMappings(final Integer installmentId, final BigDecimal chargePerInstallment) {
for (LoanTransactionToRepaymentScheduleMapping loanTransactionToRepaymentScheduleMapping : this.loanTransactionToRepaymentScheduleMappings) {
final LoanRepaymentScheduleInstallment loanRepaymentScheduleInstallment = loanTransactionToRepaymentScheduleMapping
@@ -21,13 +21,10 @@ package org.apache.fineract.portfolio.loanaccount.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.DataIntegrityErrorHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -37,19 +34,10 @@ import org.springframework.transaction.annotation.Transactional;
public class LoanMerchantIssuedRefundCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
try {
boolean isRecoveryRepayment = false;
return this.writePlatformService.makeLoanRepayment(LoanTransactionType.MERCHANT_ISSUED_REFUND, command.getLoanId(), command,
isRecoveryRepayment);
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.merchantIssuedRefund",
"Merchant Issued Refund");
return CommandProcessingResult.empty();
}
return this.writePlatformService.makeRefund(command.getLoanId(), LoanTransactionType.MERCHANT_ISSUED_REFUND, command);
}
}
@@ -21,13 +21,10 @@ package org.apache.fineract.portfolio.loanaccount.handler;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.commands.annotation.CommandType;
import org.apache.fineract.commands.handler.NewCommandSourceHandler;
import org.apache.fineract.infrastructure.DataIntegrityErrorHandler;
import org.apache.fineract.infrastructure.core.api.JsonCommand;
import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import org.apache.fineract.portfolio.loanaccount.service.LoanWritePlatformService;
import org.springframework.dao.DataIntegrityViolationException;
import org.springframework.orm.jpa.JpaSystemException;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -37,19 +34,10 @@ import org.springframework.transaction.annotation.Transactional;
public class LoanPayoutRefundCommandHandler implements NewCommandSourceHandler {
private final LoanWritePlatformService writePlatformService;
private final DataIntegrityErrorHandler dataIntegrityErrorHandler;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
try {
boolean isRecoveryRepayment = false;
return this.writePlatformService.makeLoanRepayment(LoanTransactionType.PAYOUT_REFUND, command.getLoanId(), command,
isRecoveryRepayment);
} catch (final JpaSystemException | DataIntegrityViolationException dve) {
dataIntegrityErrorHandler.handleDataIntegrityIssues(command, dve.getMostSpecificCause(), dve, "loan.payoutRefund",
"Payout Refund");
return CommandProcessingResult.empty();
}
return this.writePlatformService.makeRefund(command.getLoanId(), LoanTransactionType.PAYOUT_REFUND, command);
}
}
@@ -134,4 +134,6 @@ public interface LoanWritePlatformService {
@Transactional
CommandProcessingResult undoChargeOff(JsonCommand command);
CommandProcessingResult makeRefund(Long loanId, LoanTransactionType loanTransactionType, JsonCommand command);
}
@@ -28,6 +28,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.infrastructure.configuration.domain.ConfigurationDomainService;
import org.apache.fineract.infrastructure.core.data.ApiParameterError;
@@ -80,6 +81,7 @@ import org.apache.fineract.portfolio.account.domain.StandingInstructionRepositor
import org.apache.fineract.portfolio.account.domain.StandingInstructionStatus;
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.delinquency.domain.LoanDelinquencyAction;
import org.apache.fineract.portfolio.delinquency.helper.DelinquencyEffectivePauseHelper;
import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService;
@@ -91,6 +93,9 @@ import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
import org.apache.fineract.portfolio.loanaccount.data.LoanRefundRequestData;
import org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
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.service.InterestRefundService;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundServiceDelegate;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
@@ -139,6 +144,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final DelinquencyReadPlatformService delinquencyReadPlatformService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final InterestRefundServiceDelegate interestRefundServiceDelegate;
private final LoanRepaymentScheduleTransactionProcessorFactory transactionProcessorFactory;
@Transactional
@Override
@@ -150,12 +156,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
isRecoveryRepayment, chargeRefundChargeType, isAccountTransfer, holidayDetailDto, isHolidayValidationDone, false);
}
@Transactional
@Override
public void updateLoanCollateralTransaction(Set<LoanCollateralManagement> loanCollateralManagementSet) {
this.loanCollateralManagementRepository.saveAll(loanCollateralManagementSet);
}
@Transactional
@Override
public void updateLoanCollateralStatus(Set<LoanCollateralManagement> 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<Long, LoanTransaction> 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<Long, LoanTransaction> 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<LoanCollateralManagement> 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<LoanTransaction, LoanTransaction> 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<LoanTransaction> allNonContraTransactionsPostDisbursement = loan.retrieveListOfTransactionsForReprocessing();
ChangedTransactionDetail changedTransactionDetail = loanRepaymentScheduleTransactionProcessor.reprocessLoanTransactions(
loan.getDisbursementDate(), allNonContraTransactionsPostDisbursement, loan.getCurrency(),
loan.getRepaymentScheduleInstallments(), loan.getActiveCharges());
for (final Map.Entry<Long, LoanTransaction> mapEntry : changedTransactionDetail.getNewTransactionMappings().entrySet()) {
mapEntry.getValue().updateLoan(loan);
}
loan.getLoanTransactions().addAll(changedTransactionDetail.getNewTransactionMappings().values());
// 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<Long, LoanTransaction> 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<LoanTransactionToRepaymentScheduleMapping> 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;
}
}
@@ -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);
}
}
}
@@ -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<LoanCollateralManagement> 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)) {
@@ -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<String, Object> 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<String, Object> 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<Long, LoanTransaction> 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<String, Object> 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<LoanTransaction> 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<Long, LoanTransaction> 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<LoanTransactionToRepaymentScheduleMapping> 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<LoanCollateralManagement> 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<LoanCollateralManagement> 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<LoanCollateralManagement> 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<LoanCollateralManagement> 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<String, Object> 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<String, Object> 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<Long> existingTransactionIds = loan.findExistingTransactionIds();
final List<Long> existingReversedTransactionIds = loan.findExistingReversedTransactionIds();
// Create refund transaction(s)
Pair<LoanTransaction, LoanTransaction> 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";