Skip to content

Commit

Permalink
FINERACT-2107: Interest Refund - Allocation, Business Event & Full Re…
Browse files Browse the repository at this point in the history
…payment
  • Loading branch information
somasorosdpc authored and adamsaghy committed Oct 21, 2024
1 parent 84d6065 commit 5722891
Show file tree
Hide file tree
Showing 18 changed files with 966 additions and 74 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.event.business.domain.loan.transaction;

import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;

public class LoanTransactionInterestRefundPostBusinessEvent extends LoanTransactionBusinessEvent {

private static final String TYPE = "LoanTransactionInterestRefundPostBusinessEvent";

public LoanTransactionInterestRefundPostBusinessEvent(LoanTransaction value) {
super(value);
}

@Override
public String getType() {
return TYPE;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.infrastructure.event.business.domain.loan.transaction;

import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBusinessEvent;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;

public class LoanTransactionInterestRefundPreBusinessEvent extends LoanBusinessEvent {

private static final String TYPE = "LoanTransactionInterestRefundPreBusinessEvent";

public LoanTransactionInterestRefundPreBusinessEvent(Loan value) {
super(value);
}

@Override
public String getType() {
return TYPE;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -1718,13 +1718,15 @@ public Map<String, Object> undoApproval(final LoanLifecycleStateMachine loanLife

public List<Long> findExistingTransactionIds() {
return getLoanTransactions().stream() //
.filter(loanTransaction -> loanTransaction.getId() != null) //
.map(LoanTransaction::getId) //
.collect(Collectors.toList());
}

public List<Long> findExistingReversedTransactionIds() {
return getLoanTransactions().stream() //
.filter(LoanTransaction::isReversed) //
.filter(loanTransaction -> loanTransaction.getId() != null) //
.map(LoanTransaction::getId) //
.collect(Collectors.toList());
}
Expand Down Expand Up @@ -2256,7 +2258,8 @@ public ChangedTransactionDetail makeRepayment(final LoanTransaction repaymentTra
private void validateRepaymentTypeAccountStatus(LoanTransaction repaymentTransaction, LoanEvent event) {
if (repaymentTransaction.isGoodwillCredit() || repaymentTransaction.isInterestPaymentWaiver()
|| repaymentTransaction.isMerchantIssuedRefund() || repaymentTransaction.isPayoutRefund()
|| repaymentTransaction.isChargeRefund() || repaymentTransaction.isRepayment() || repaymentTransaction.isDownPayment()) {
|| repaymentTransaction.isChargeRefund() || repaymentTransaction.isRepayment() || repaymentTransaction.isDownPayment()
|| repaymentTransaction.isInterestRefund()) {

if (!(isOpen() || isClosedObligationsMet() || isOverPaid())) {
final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -189,6 +189,11 @@ public static LoanTransaction repaymentType(final LoanTransactionType repaymentT
chargeRefundChargeType);
}

public static LoanTransaction interestRefund(final Loan loan, final BigDecimal amount, final LocalDate date,
final ExternalId externalId) {
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.INTEREST_REFUND, null, amount, date, externalId);
}

public static LoanTransaction chargeAdjustment(final Loan loan, final BigDecimal amount, final LocalDate transactionDate,
final ExternalId externalId, PaymentDetail paymentDetail) {
return new LoanTransaction(loan, loan.getOffice(), LoanTransactionType.CHARGE_ADJUSTMENT, paymentDetail, amount, transactionDate,
Expand Down Expand Up @@ -596,7 +601,7 @@ public void setManuallyAdjustedOrReversed() {

public boolean isRepaymentLikeType() {
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()
|| isChargeAdjustment() || isDownPayment() || isInterestPaymentWaiver();
|| isChargeAdjustment() || isDownPayment() || isInterestPaymentWaiver() || isInterestRefund();
}

public boolean isTypeAllowedForChargeback() {
Expand Down Expand Up @@ -1118,6 +1123,10 @@ public boolean isOverPaid() {
return MathUtil.isGreaterThanZero(overPaymentPortion);
}

public boolean isInterestRefund() {
return getTypeOf().isInterestRefund();
}

// TODO missing hashCode(), equals(Object obj), but probably OK as long as
// this is never stored in a Collection.
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.service;

import java.math.BigDecimal;
import java.time.LocalDate;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;

public interface InterestRefundService {

boolean canHandle(Loan loan);

BigDecimal calculateInterestRefundAmount(Long loanId, BigDecimal relatedRefundTransactionAmount,
LocalDate relatedRefundTransactionDate);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanaccount.service;

import java.util.List;
import lombok.RequiredArgsConstructor;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class InterestRefundServiceDelegate {

private final List<InterestRefundService> interestRefundService;

public InterestRefundService lookupInterestRefundService(final Loan loan) {
return interestRefundService.stream().filter(iRS -> iRS.canHandle(loan)).findFirst().orElse(null);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@
public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRepaymentScheduleTransactionProcessor {

public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY = "advanced-payment-allocation-strategy";
public static final String ADVANCED_PAYMENT_ALLOCATION_STRATEGY_NAME = "Advanced payment allocation strategy";

public final EMICalculator emiCalculator;

Expand All @@ -106,7 +107,7 @@ public String getCode() {

@Override
public String getName() {
return "Advanced payment allocation strategy";
return ADVANCED_PAYMENT_ALLOCATION_STRATEGY_NAME;
}

@Override
Expand Down Expand Up @@ -232,8 +233,8 @@ public void processLatestTransaction(LoanTransaction loanTransaction, Transactio
case CHARGEBACK -> handleChargeback(loanTransaction, ctx);
case CREDIT_BALANCE_REFUND ->
handleCreditBalanceRefund(loanTransaction, ctx.getCurrency(), ctx.getInstallments(), ctx.getOverpaymentHolder());
case REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT, DOWN_PAYMENT,
WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER ->
case INTEREST_REFUND, REPAYMENT, MERCHANT_ISSUED_REFUND, PAYOUT_REFUND, GOODWILL_CREDIT, CHARGE_REFUND, CHARGE_ADJUSTMENT,
DOWN_PAYMENT, WAIVE_INTEREST, RECOVERY_REPAYMENT, INTEREST_PAYMENT_WAIVER ->
handleRepayment(loanTransaction, ctx);
case CHARGE_OFF -> handleChargeOff(loanTransaction, ctx);
case CHARGE_PAYMENT -> handleChargePayment(loanTransaction, ctx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionGoodwillCreditPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestPaymentWaiverPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestRefundPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionInterestRefundPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMakeRepaymentPostBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMakeRepaymentPreBusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.transaction.LoanTransactionMerchantIssuedRefundPostBusinessEvent;
Expand Down Expand Up @@ -89,11 +91,14 @@
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.service.InterestRefundService;
import org.apache.fineract.portfolio.loanaccount.service.InterestRefundServiceDelegate;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualsProcessingService;
import org.apache.fineract.portfolio.loanaccount.service.LoanAssembler;
import org.apache.fineract.portfolio.loanaccount.service.LoanUtilService;
import org.apache.fineract.portfolio.loanaccount.service.ReplayedTransactionBusinessEventService;
import org.apache.fineract.portfolio.loanproduct.domain.LoanSupportedInterestRefundTypes;
import org.apache.fineract.portfolio.note.domain.Note;
import org.apache.fineract.portfolio.note.domain.NoteRepository;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
Expand Down Expand Up @@ -133,6 +138,7 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
private final DelinquencyEffectivePauseHelper delinquencyEffectivePauseHelper;
private final DelinquencyReadPlatformService delinquencyReadPlatformService;
private final LoanAccrualsProcessingService loanAccrualsProcessingService;
private final InterestRefundServiceDelegate interestRefundServiceDelegate;

@Transactional
@Override
Expand All @@ -159,6 +165,26 @@ public void updateLoanCollateralStatus(Set<LoanCollateralManagement> loanCollate
this.loanCollateralManagementRepository.saveAll(loanCollateralManagementSet);
}

@Transactional
public LoanTransaction createInterestRefundLoanTransaction(Loan loan, final LocalDate transactionDate,
BigDecimal relatedRefundTransactionAmount) {
InterestRefundService interestRefundService = interestRefundServiceDelegate.lookupInterestRefundService(loan);
if (interestRefundService == null) {
return null;
}
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;
}

@Transactional
@Override
public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransactionType, Loan loan, final LocalDate transactionDate,
Expand Down Expand Up @@ -199,6 +225,18 @@ public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransact
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);
Expand All @@ -209,6 +247,9 @@ public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransact
}

saveLoanTransactionWithDataIntegrityViolationChecks(newRepaymentTransaction);
if (newInterestRefundTransaction != null) {
saveLoanTransactionWithDataIntegrityViolationChecks(newInterestRefundTransaction);
}

/***
* TODO Vishwas Batch save is giving me a HibernateOptimisticLockingFailureException, looping and saving for the
Expand Down Expand Up @@ -243,7 +284,6 @@ public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransact
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
}

// disable all active standing orders linked to this loan if status
// changes to closed
disableStandingInstructionsLinkedToClosedLoan(loan);
Expand Down Expand Up @@ -276,7 +316,10 @@ public LoanTransaction makeRepayment(final LoanTransactionType repaymentTransact
}
}
}

if (shouldCreateInterestRefundTransaction && newInterestRefundTransaction != null) {
businessEventNotifierService
.notifyPostBusinessEvent(new LoanTransactionInterestRefundPostBusinessEvent(newInterestRefundTransaction));
}
return newRepaymentTransaction;
}

Expand All @@ -299,6 +342,8 @@ private LoanBusinessEvent getLoanRepaymentTypeBusinessEvent(LoanTransactionType
repaymentEvent = new LoanTransactionRecoveryPaymentPreBusinessEvent(loan);
} else if (repaymentTransactionType.isDownPayment()) {
repaymentEvent = new LoanTransactionDownPaymentPreBusinessEvent(loan);
} else if (repaymentTransactionType.isInterestRefund()) {
repaymentEvent = new LoanTransactionInterestRefundPreBusinessEvent(loan);
}
return repaymentEvent;
}
Expand All @@ -322,6 +367,8 @@ private LoanTransactionBusinessEvent getTransactionRepaymentTypeBusinessEvent(Lo
repaymentEvent = new LoanTransactionRecoveryPaymentPostBusinessEvent(transaction);
} else if (repaymentTransactionType.isDownPayment()) {
repaymentEvent = new LoanTransactionDownPaymentPostBusinessEvent(transaction);
} else if (repaymentTransactionType.isInterestRefund()) {
repaymentEvent = new LoanTransactionInterestRefundPostBusinessEvent(transaction);
}
return repaymentEvent;
}
Expand Down
Loading

0 comments on commit 5722891

Please sign in to comment.