FINERACT-2107: Interest Refund - Allocation, Business Event & Full Repayment
This commit is contained in:
+35
@@ -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;
|
||||
}
|
||||
}
|
||||
+36
@@ -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;
|
||||
}
|
||||
}
|
||||
+4
-1
@@ -1718,6 +1718,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
|
||||
public List<Long> findExistingTransactionIds() {
|
||||
return getLoanTransactions().stream() //
|
||||
.filter(loanTransaction -> loanTransaction.getId() != null) //
|
||||
.map(LoanTransaction::getId) //
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
@@ -1725,6 +1726,7 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
public List<Long> findExistingReversedTransactionIds() {
|
||||
return getLoanTransactions().stream() //
|
||||
.filter(LoanTransaction::isReversed) //
|
||||
.filter(loanTransaction -> loanTransaction.getId() != null) //
|
||||
.map(LoanTransaction::getId) //
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
@@ -2256,7 +2258,8 @@ public class Loan extends AbstractAuditableWithUTCDateTimeCustom<Long> {
|
||||
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<>();
|
||||
|
||||
+10
-1
@@ -189,6 +189,11 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
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,
|
||||
@@ -596,7 +601,7 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
|
||||
public boolean isRepaymentLikeType() {
|
||||
return isRepayment() || isMerchantIssuedRefund() || isPayoutRefund() || isGoodwillCredit() || isChargeRefund()
|
||||
|| isChargeAdjustment() || isDownPayment() || isInterestPaymentWaiver();
|
||||
|| isChargeAdjustment() || isDownPayment() || isInterestPaymentWaiver() || isInterestRefund();
|
||||
}
|
||||
|
||||
public boolean isTypeAllowedForChargeback() {
|
||||
@@ -1118,6 +1123,10 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
||||
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.
|
||||
}
|
||||
|
||||
+32
@@ -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);
|
||||
|
||||
}
|
||||
+35
@@ -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);
|
||||
}
|
||||
}
|
||||
+4
-3
@@ -96,6 +96,7 @@ import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
|
||||
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;
|
||||
|
||||
@@ -106,7 +107,7 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
|
||||
@Override
|
||||
public String getName() {
|
||||
return "Advanced payment allocation strategy";
|
||||
return ADVANCED_PAYMENT_ALLOCATION_STRATEGY_NAME;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -232,8 +233,8 @@ public class AdvancedPaymentScheduleTransactionProcessor extends AbstractLoanRep
|
||||
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);
|
||||
|
||||
+49
-2
@@ -55,6 +55,8 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.transaction
|
||||
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;
|
||||
@@ -89,11 +91,14 @@ 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.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;
|
||||
@@ -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
|
||||
@@ -159,6 +165,26 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
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,
|
||||
@@ -199,6 +225,18 @@ 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);
|
||||
@@ -209,6 +247,9 @@ 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
|
||||
@@ -243,7 +284,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
businessEventNotifierService.notifyPostBusinessEvent(new LoanBalanceChangedBusinessEvent(loan));
|
||||
businessEventNotifierService.notifyPostBusinessEvent(transactionRepaymentEvent);
|
||||
}
|
||||
|
||||
// disable all active standing orders linked to this loan if status
|
||||
// changes to closed
|
||||
disableStandingInstructionsLinkedToClosedLoan(loan);
|
||||
@@ -276,7 +316,10 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCreateInterestRefundTransaction && newInterestRefundTransaction != null) {
|
||||
businessEventNotifierService
|
||||
.notifyPostBusinessEvent(new LoanTransactionInterestRefundPostBusinessEvent(newInterestRefundTransaction));
|
||||
}
|
||||
return newRepaymentTransaction;
|
||||
}
|
||||
|
||||
@@ -299,6 +342,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
repaymentEvent = new LoanTransactionRecoveryPaymentPreBusinessEvent(loan);
|
||||
} else if (repaymentTransactionType.isDownPayment()) {
|
||||
repaymentEvent = new LoanTransactionDownPaymentPreBusinessEvent(loan);
|
||||
} else if (repaymentTransactionType.isInterestRefund()) {
|
||||
repaymentEvent = new LoanTransactionInterestRefundPreBusinessEvent(loan);
|
||||
}
|
||||
return repaymentEvent;
|
||||
}
|
||||
@@ -322,6 +367,8 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
||||
repaymentEvent = new LoanTransactionRecoveryPaymentPostBusinessEvent(transaction);
|
||||
} else if (repaymentTransactionType.isDownPayment()) {
|
||||
repaymentEvent = new LoanTransactionDownPaymentPostBusinessEvent(transaction);
|
||||
} else if (repaymentTransactionType.isInterestRefund()) {
|
||||
repaymentEvent = new LoanTransactionInterestRefundPostBusinessEvent(transaction);
|
||||
}
|
||||
return repaymentEvent;
|
||||
}
|
||||
|
||||
+115
@@ -0,0 +1,115 @@
|
||||
/**
|
||||
* 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 java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.loanschedule.data.ProgressiveLoanInterestScheduleModel;
|
||||
import org.apache.fineract.portfolio.loanaccount.starter.AdvancedPaymentScheduleTransactionProcessorCondition;
|
||||
import org.apache.fineract.portfolio.loanproduct.calc.EMICalculator;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Propagation;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
@Slf4j
|
||||
@RequiredArgsConstructor
|
||||
@Conditional(AdvancedPaymentScheduleTransactionProcessorCondition.class)
|
||||
@Service
|
||||
public class ProgressiveLoanInterestRefundServiceImpl implements InterestRefundService {
|
||||
|
||||
private final AdvancedPaymentScheduleTransactionProcessor processor;
|
||||
private final EMICalculator emiCalculator;
|
||||
private final LoanAssembler loanAssembler;
|
||||
|
||||
@Override
|
||||
public boolean canHandle(Loan loan) {
|
||||
return loan != null && loan.isInterestBearing() && processor.accept(loan.getTransactionProcessingStrategyCode());
|
||||
}
|
||||
|
||||
private static boolean omitDisbursements(LoanTransaction lt, final AtomicReference<BigDecimal> refundFinal) {
|
||||
if (lt.getTypeOf().isDisbursement() && refundFinal.get().compareTo(BigDecimal.ZERO) > 0) {
|
||||
if (lt.getAmount().compareTo(refundFinal.get()) <= 0) {
|
||||
refundFinal.set(refundFinal.get().subtract(lt.getAmount()));
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
private static LoanTransaction calculateReducedAmountDisbursements(LoanTransaction lt, final AtomicReference<BigDecimal> refundFinal) {
|
||||
if (lt.getTypeOf().isDisbursement() && refundFinal.get().compareTo(BigDecimal.ZERO) > 0) {
|
||||
LoanTransaction result = new LoanTransaction(lt.getLoan(), lt.getLoan().getOffice(), lt.getTypeOf().getValue(), lt.getDateOf(),
|
||||
lt.getAmount().subtract(refundFinal.get()), lt.getPrincipalPortion(), lt.getInterestPortion(),
|
||||
lt.getFeeChargesPortion(), lt.getPenaltyChargesPortion(),
|
||||
lt.getOverPaymentPortion(lt.getLoan().getCurrency()).getAmount(), lt.isReversed(), lt.getPaymentDetail(),
|
||||
lt.getExternalId());
|
||||
refundFinal.set(BigDecimal.ZERO);
|
||||
return result;
|
||||
}
|
||||
return lt;
|
||||
}
|
||||
|
||||
private BigDecimal totalInterest(final Loan loan, BigDecimal refundAmount, LocalDate relatedRefundTransactionDate) {
|
||||
final AtomicReference<BigDecimal> refundFinal = new AtomicReference<>(refundAmount);
|
||||
List<LoanTransaction> transactionsToReprocess = loan.getLoanTransactions().stream().filter(lt -> !lt.isReversed()) //
|
||||
.filter(lt -> !lt.isAccrual() && !lt.isAccrualActivity()) //
|
||||
.filter(lt -> omitDisbursements(lt, refundFinal)) //
|
||||
.map(lt -> calculateReducedAmountDisbursements(lt, refundFinal)).toList();
|
||||
|
||||
List<LoanRepaymentScheduleInstallment> installmentsToReprocess = new ArrayList<>(
|
||||
loan.getRepaymentScheduleInstallments().stream().filter(i -> !i.isReAged() && !i.isAdditional()).toList());
|
||||
|
||||
ProgressiveLoanInterestScheduleModel modelAfter = processor.reprocessProgressiveLoanTransactions(loan.getDisbursementDate(),
|
||||
transactionsToReprocess, loan.getCurrency(), installmentsToReprocess, loan.getActiveCharges()).getRight();
|
||||
BigDecimal payableInterest = BigDecimal.ZERO;
|
||||
if (modelAfter != null && loan.getStatus().isActive()) {
|
||||
LoanRepaymentScheduleInstallment actualInstallment = loan.getRelatedRepaymentScheduleInstallment(relatedRefundTransactionDate);
|
||||
if (actualInstallment == null) {
|
||||
actualInstallment = loan.getLastLoanRepaymentScheduleInstallment();
|
||||
}
|
||||
payableInterest = emiCalculator.getPayableDetails(modelAfter, actualInstallment.getDueDate(), relatedRefundTransactionDate)
|
||||
.getPayableInterest().getAmount();
|
||||
}
|
||||
BigDecimal paidInterest = installmentsToReprocess.stream().map(i -> i.getInterestPaid(loan.getCurrency())).filter(Objects::nonNull)
|
||||
.map(Money::getAmount).reduce(BigDecimal.ZERO, BigDecimal::add);
|
||||
return payableInterest.add(paidInterest);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional(readOnly = true, propagation = Propagation.REQUIRES_NEW)
|
||||
public BigDecimal calculateInterestRefundAmount(Long loanId, BigDecimal relatedRefundTransactionAmount,
|
||||
LocalDate relatedRefundTransactionDate) {
|
||||
Loan loan = loanAssembler.assembleFrom(loanId);
|
||||
BigDecimal totalInterestBeforeRefund = totalInterest(loan, BigDecimal.ZERO, relatedRefundTransactionDate);
|
||||
BigDecimal totalInterestAfterRefund = totalInterest(loan, relatedRefundTransactionAmount, relatedRefundTransactionDate);
|
||||
return totalInterestBeforeRefund.subtract(totalInterestAfterRefund);
|
||||
}
|
||||
}
|
||||
@@ -169,4 +169,5 @@
|
||||
<include file="parts/0148_transaction_summary_with_asset_owner_report_performance_improvement.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0149_update_global_configuration_names.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0150_transaction_summary_with_asset_owner_report_interest_waiver_interest_refund_added.xml" relativeToChangelogFile="true" />
|
||||
<include file="parts/0151_interest_refund_business_events.xml" relativeToChangelogFile="true" />
|
||||
</databaseChangeLog>
|
||||
|
||||
+35
@@ -0,0 +1,35 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!--
|
||||
|
||||
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.
|
||||
|
||||
-->
|
||||
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.3.xsd">
|
||||
<changeSet author="fineract" id="1">
|
||||
<insert tableName="m_external_event_configuration">
|
||||
<column name="type" value="LoanTransactionInterestRefundPostBusinessEvent"/>
|
||||
<column name="enabled" valueBoolean="false"/>
|
||||
</insert>
|
||||
<insert tableName="m_external_event_configuration">
|
||||
<column name="type" value="LoanTransactionInterestRefundPreBusinessEvent"/>
|
||||
<column name="enabled" valueBoolean="false"/>
|
||||
</insert>
|
||||
</changeSet>
|
||||
</databaseChangeLog>
|
||||
+4
-2
@@ -104,7 +104,8 @@ public class ExternalEventConfigurationValidationServiceTest {
|
||||
"LoanUndoReAmortizeTransactionBusinessEvent", "LoanReAgeBusinessEvent", "LoanUndoReAgeBusinessEvent",
|
||||
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent",
|
||||
"LoanTransactionInterestPaymentWaiverPostBusinessEvent", "LoanTransactionAccrualActivityPostBusinessEvent",
|
||||
"LoanTransactionAccrualActivityPreBusinessEvent");
|
||||
"LoanTransactionAccrualActivityPreBusinessEvent", "LoanTransactionInterestRefundPostBusinessEvent",
|
||||
"LoanTransactionInterestRefundPreBusinessEvent");
|
||||
|
||||
List<FineractPlatformTenant> tenants = Arrays
|
||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||
@@ -189,7 +190,8 @@ public class ExternalEventConfigurationValidationServiceTest {
|
||||
"LoanUndoReAmortizeTransactionBusinessEvent", "LoanReAgeBusinessEvent", "LoanUndoReAgeBusinessEvent",
|
||||
"LoanReAmortizeBusinessEvent", "LoanUndoReAmortizeBusinessEvent", "LoanTransactionInterestPaymentWaiverPreBusinessEvent",
|
||||
"LoanTransactionInterestPaymentWaiverPostBusinessEvent", "LoanTransactionAccrualActivityPostBusinessEvent",
|
||||
"LoanTransactionAccrualActivityPreBusinessEvent");
|
||||
"LoanTransactionAccrualActivityPreBusinessEvent", "LoanTransactionInterestRefundPostBusinessEvent",
|
||||
"LoanTransactionInterestRefundPreBusinessEvent");
|
||||
|
||||
List<FineractPlatformTenant> tenants = Arrays
|
||||
.asList(new FineractPlatformTenant(1L, "default", "Default Tenant", "Europe/Budapest", null));
|
||||
|
||||
+12
-13
@@ -76,7 +76,6 @@ import org.apache.fineract.integrationtests.common.accounting.PeriodicAccrualAcc
|
||||
import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
|
||||
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
|
||||
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
|
||||
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.EarlyPaymentLoanRepaymentScheduleTransactionProcessor;
|
||||
import org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.FineractStyleLoanRepaymentScheduleTransactionProcessor;
|
||||
@@ -4426,7 +4425,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||
.interestRatePerPeriod(5.0).interestCalculationPeriodType(DAYS).interestRateFrequencyType(YEARS)
|
||||
.daysInMonthType(DaysInMonthType.DAYS_30.getValue()).daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(5)//
|
||||
.daysInMonthType(DaysInMonthType.DAYS_30).daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(5)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(2L)//
|
||||
.enableDownPayment(true)//
|
||||
@@ -4488,7 +4487,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||
.interestRatePerPeriod(12.3).interestCalculationPeriodType(RepaymentFrequencyType.DAYS).interestRateFrequencyType(YEARS)
|
||||
.daysInMonthType(DaysInMonthType.DAYS_30.getValue()).daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(5)//
|
||||
.daysInMonthType(DaysInMonthType.DAYS_30).daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(5)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(2L)//
|
||||
.enableDownPayment(true)//
|
||||
@@ -4551,7 +4550,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||
.interestRatePerPeriod(12.3).interestCalculationPeriodType(RepaymentFrequencyType.DAYS).interestRateFrequencyType(YEARS)
|
||||
.daysInMonthType(DaysInMonthType.DAYS_30.getValue()).daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(5)//
|
||||
.daysInMonthType(DaysInMonthType.DAYS_30).daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(5)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(2L)//
|
||||
.enableDownPayment(true)//
|
||||
@@ -4614,7 +4613,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||
.interestRatePerPeriod(12.3).interestCalculationPeriodType(RepaymentFrequencyType.DAYS).interestRateFrequencyType(YEARS)
|
||||
.daysInMonthType(DaysInMonthType.ACTUAL.getValue()).daysInYearType(DaysInYearType.DAYS_365).numberOfRepayments(4)//
|
||||
.daysInMonthType(DaysInMonthType.ACTUAL).daysInYearType(DaysInYearType.DAYS_365).numberOfRepayments(4)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(2L)//
|
||||
.allowPartialPeriodInterestCalcualtion(false)//
|
||||
@@ -4692,8 +4691,8 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||
.interestRatePerPeriod(108.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
|
||||
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.ACTUAL.getValue())
|
||||
.daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(4)//
|
||||
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.ACTUAL).daysInYearType(DaysInYearType.DAYS_360)
|
||||
.numberOfRepayments(4)//
|
||||
.maxInterestRatePerPeriod((double) 110)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(1L)//
|
||||
@@ -4873,7 +4872,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
|
||||
interestRefundTypes.add("MERCHANT_ISSUED_REFUND");
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||
.interestRatePerPeriod(12.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS).interestRateFrequencyType(YEARS)
|
||||
.daysInMonthType(DaysInMonthType.ACTUAL.getValue()).daysInYearType(DaysInYearType.DAYS_365).numberOfRepayments(4)//
|
||||
.daysInMonthType(DaysInMonthType.ACTUAL).daysInYearType(DaysInYearType.DAYS_365).numberOfRepayments(4)//
|
||||
.repaymentEvery(5)//
|
||||
.repaymentFrequencyType(0L)//
|
||||
.allowPartialPeriodInterestCalcualtion(false)//
|
||||
@@ -4935,8 +4934,8 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||
.interestRatePerPeriod(interestRatePerPeriod.doubleValue()).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
|
||||
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.DAYS_30.getValue())
|
||||
.daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(6)//
|
||||
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.DAYS_30).daysInYearType(DaysInYearType.DAYS_360)
|
||||
.numberOfRepayments(6)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(2L)//
|
||||
.enableDownPayment(false)//
|
||||
@@ -5017,8 +5016,8 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||
.interestRatePerPeriod(interestRatePerPeriod.doubleValue()).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
|
||||
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.DAYS_30.getValue())
|
||||
.daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(6)//
|
||||
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.DAYS_30).daysInYearType(DaysInYearType.DAYS_360)
|
||||
.numberOfRepayments(6)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(2L)//
|
||||
.enableDownPayment(false)//
|
||||
@@ -5114,7 +5113,7 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan
|
||||
.interestRatePerPeriod(interestRatePerPeriod.doubleValue())//
|
||||
.interestCalculationPeriodType(RepaymentFrequencyType.DAYS)//
|
||||
.interestRateFrequencyType(YEARS)//
|
||||
.daysInMonthType(DaysInMonthType.DAYS_30.getValue())//
|
||||
.daysInMonthType(DaysInMonthType.DAYS_30)//
|
||||
.daysInYearType(DaysInYearType.DAYS_360)//
|
||||
.numberOfRepayments(6)//
|
||||
.repaymentEvery(1)//
|
||||
|
||||
+7
@@ -1281,6 +1281,13 @@ public abstract class BaseLoanIntegrationTest {
|
||||
public static final Integer DAYS_365 = 365;
|
||||
}
|
||||
|
||||
public static class DaysInMonthType {
|
||||
|
||||
public static final Integer INVALID = 0;
|
||||
public static final Integer ACTUAL = 1;
|
||||
public static final Integer DAYS_30 = 30;
|
||||
}
|
||||
|
||||
public static class FuturePaymentAllocationRule {
|
||||
|
||||
public static final String LAST_INSTALLMENT = "LAST_INSTALLMENT";
|
||||
|
||||
+164
-50
@@ -25,9 +25,11 @@ import io.restassured.builder.ResponseSpecBuilder;
|
||||
import io.restassured.http.ContentType;
|
||||
import io.restassured.specification.RequestSpecification;
|
||||
import io.restassured.specification.ResponseSpecification;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import java.math.BigDecimal;
|
||||
import java.time.LocalDate;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
@@ -35,6 +37,8 @@ import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.collections4.CollectionUtils;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
@@ -42,6 +46,8 @@ import org.apache.fineract.client.models.PostClientsResponse;
|
||||
import org.apache.fineract.client.models.PostCreateRescheduleLoansRequest;
|
||||
import org.apache.fineract.client.models.PostCreateRescheduleLoansResponse;
|
||||
import org.apache.fineract.client.models.PostLoanProductsRequest;
|
||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
|
||||
import org.apache.fineract.client.models.PostLoansRequest;
|
||||
import org.apache.fineract.client.models.PostUpdateRescheduleLoansRequest;
|
||||
import org.apache.fineract.infrastructure.event.external.service.validation.ExternalEventDTO;
|
||||
@@ -111,7 +117,7 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
|
||||
|
||||
loanTransactionHelper.makeLoanRepayment("15 March 2023", 125.0F, loanIdRef.get().intValue());
|
||||
|
||||
verifyBusinessEvents(new BusinessEvent("LoanBalanceChangedBusinessEvent", "15 March 2023", 300, 400.0, 291.04));
|
||||
verifyBusinessEvents(new LoanBusinessEvent("LoanBalanceChangedBusinessEvent", "15 March 2023", 300, 400.0, 291.04));
|
||||
});
|
||||
runAt("1 April 2023", () -> {
|
||||
|
||||
@@ -123,7 +129,7 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
|
||||
|
||||
loanTransactionHelper.makeLoanRepayment("15 April 2023", 125.0F, loanIdRef.get().intValue());
|
||||
|
||||
verifyBusinessEvents(new BusinessEvent("LoanBalanceChangedBusinessEvent", "15 April 2023", 300, 1000.0, 770.06));
|
||||
verifyBusinessEvents(new LoanBusinessEvent("LoanBalanceChangedBusinessEvent", "15 April 2023", 300, 1000.0, 770.06));
|
||||
|
||||
deleteAllExternalEvents();
|
||||
|
||||
@@ -131,24 +137,57 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
|
||||
.getResourceId();
|
||||
Assertions.assertNotNull(transactionId);
|
||||
|
||||
verifyBusinessEvents(new BusinessEvent("LoanBalanceChangedBusinessEvent", "15 April 2023", 700, 1000.0, 0.0));
|
||||
verifyBusinessEvents(new LoanBusinessEvent("LoanBalanceChangedBusinessEvent", "15 April 2023", 700, 1000.0, 0.0));
|
||||
|
||||
deleteAllExternalEvents();
|
||||
|
||||
loanTransactionHelper.reverseRepayment(loanIdRef.get().intValue(), transactionId.intValue(), "15 April 2023");
|
||||
|
||||
verifyBusinessEvents(new BusinessEvent("LoanBalanceChangedBusinessEvent", "15 April 2023", 300, 1000.0, 770.06));
|
||||
verifyBusinessEvents(new LoanBusinessEvent("LoanBalanceChangedBusinessEvent", "15 April 2023", 300, 1000.0, 770.06));
|
||||
|
||||
deleteAllExternalEvents();
|
||||
|
||||
loanTransactionHelper.makeLoanRepayment("15 April 2023", 830.22F, loanIdRef.get().intValue());
|
||||
|
||||
verifyBusinessEvents(new BusinessEvent("LoanBalanceChangedBusinessEvent", "15 April 2023", 600, 1000.0, 0.0));
|
||||
verifyBusinessEvents(new LoanBusinessEvent("LoanBalanceChangedBusinessEvent", "15 April 2023", 600, 1000.0, 0.0));
|
||||
|
||||
disableLoanBalanceChangedBusinessEvent();
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyInterestRefundPostBusinessEventCreatedForMerchantIssuedRefundWithInterestRefund() {
|
||||
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||
enableLoanInterestRefundPstBusinessEvent(true);
|
||||
runAt("1 January 2021", () -> {
|
||||
PostLoanProductsResponse loanProduct = loanProductHelper
|
||||
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
|
||||
.daysInYearType(DaysInYearType.ACTUAL) //
|
||||
.supportedInterestRefundTypes(new ArrayList<>()).addSupportedInterestRefundTypesItem("MERCHANT_ISSUED_REFUND") //
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
|
||||
);
|
||||
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 9.99,
|
||||
12, null);
|
||||
Assertions.assertNotNull(loanId);
|
||||
loanIdRef.set(loanId);
|
||||
disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021");
|
||||
});
|
||||
runAt("22 January 2021", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
|
||||
deleteAllExternalEvents();
|
||||
|
||||
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper
|
||||
.makeLoanRepayment("MerchantIssuedRefund", "22 January 2021", 1000F, loanId.intValue());
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
|
||||
|
||||
verifyBusinessEvents(new LoanTransactionBusinessEvent("LoanTransactionInterestRefundPostBusinessEvent", "22 January 2021", 5.75,
|
||||
994.25, 5.75, 0.0, 0.0, 0.0));
|
||||
});
|
||||
enableLoanInterestRefundPstBusinessEvent(false);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testExternalBusinessEventLoanRescheduledDueAdjustScheduleBusinessEventInterestChange() {
|
||||
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||
@@ -171,7 +210,7 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
|
||||
loanRescheduleRequestHelper.approveLoanRescheduleRequest(rescheduleLoansResponse.getResourceId(),
|
||||
new PostUpdateRescheduleLoansRequest().approvedOnDate("1 March 2024").locale("en").dateFormat(DATETIME_PATTERN));
|
||||
|
||||
verifyBusinessEvents(new BusinessEvent("LoanRescheduledDueAdjustScheduleBusinessEvent", "01 March 2024", 300, 400.0, 400.0,
|
||||
verifyBusinessEvents(new LoanBusinessEvent("LoanRescheduledDueAdjustScheduleBusinessEvent", "01 March 2024", 300, 400.0, 400.0,
|
||||
List.of("interestRateForInstallment")));
|
||||
});
|
||||
}
|
||||
@@ -260,11 +299,28 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
|
||||
|
||||
private void logBusinessEvents(List<ExternalEventDTO> allExternalEvents) {
|
||||
allExternalEvents.forEach(externalEventDTO -> {
|
||||
log.info("Event Received { type:'{}' businessDate:'{}' }", externalEventDTO.getType(), externalEventDTO.getBusinessDate());
|
||||
log.debug(externalEventDTO.toString());
|
||||
Object amount = externalEventDTO.getPayLoad().get("amount");
|
||||
Object outstandingLoanBalance = externalEventDTO.getPayLoad().get("outstandingLoanBalance");
|
||||
Object principalPortion = externalEventDTO.getPayLoad().get("principalPortion");
|
||||
Object interestPortion = externalEventDTO.getPayLoad().get("interestPortion");
|
||||
Object feePortion = externalEventDTO.getPayLoad().get("feeChargesPortion");
|
||||
Object penaltyPortion = externalEventDTO.getPayLoad().get("penaltyChargesPortion");
|
||||
log.info("Event Received\n type:'{}'\n businessDate:'{}'", externalEventDTO.getType(), externalEventDTO.getBusinessDate());
|
||||
log.info(
|
||||
"Values\n amount: {}\n outstandingLoanBalance: {}\n principalPortion: {}\n interestPortion: {}\n feePortion: {}\n penaltyPortion: {}",
|
||||
amount, outstandingLoanBalance, principalPortion, interestPortion, feePortion, penaltyPortion);
|
||||
});
|
||||
}
|
||||
|
||||
private static void enableLoanInterestRefundPstBusinessEvent(boolean enabled) {
|
||||
final Map<String, Boolean> updatedConfigurations = ExternalEventConfigurationHelper.updateExternalEventConfigurations(requestSpec,
|
||||
responseSpec, "{\"externalEventConfigurations\":{\"LoanTransactionInterestRefundPostBusinessEvent\":"
|
||||
+ (enabled ? "true" : "false") + "}}\n");
|
||||
Assertions.assertEquals(updatedConfigurations.size(), 1);
|
||||
Assertions.assertTrue(updatedConfigurations.containsKey("LoanTransactionInterestRefundPostBusinessEvent"));
|
||||
Assertions.assertEquals(enabled, updatedConfigurations.get("LoanTransactionInterestRefundPostBusinessEvent"));
|
||||
}
|
||||
|
||||
public void verifyBusinessEvents(BusinessEvent... businessEvents) {
|
||||
List<ExternalEventDTO> allExternalEvents = ExternalEventHelper.getAllExternalEvents(requestSpec, responseSpec);
|
||||
logBusinessEvents(allExternalEvents);
|
||||
@@ -273,61 +329,119 @@ public class ExternalBusinessEventTest extends BaseLoanIntegrationTest {
|
||||
Assertions.assertTrue(businessEvents.length <= allExternalEvents.size(),
|
||||
"Expected business event count is less than actual. Expected: " + businessEvents.length + " Actual: "
|
||||
+ allExternalEvents.size());
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN, Locale.ENGLISH);
|
||||
final DateTimeFormatter formatter = DateTimeFormatter.ofPattern(DATETIME_PATTERN, Locale.ENGLISH);
|
||||
for (BusinessEvent businessEvent : businessEvents) {
|
||||
long count = allExternalEvents.stream().filter(externalEvent -> {
|
||||
Object summaryRes = externalEvent.getPayLoad().get("summary");
|
||||
Object statusRes = externalEvent.getPayLoad().get("status");
|
||||
Map<String, Object> summary = summaryRes instanceof Map ? (Map<String, Object>) summaryRes : Map.of();
|
||||
Map<String, Object> status = statusRes instanceof Map ? (Map<String, Object>) statusRes : Map.of();
|
||||
var principalDisbursed = summary.get("principalDisbursed");
|
||||
|
||||
var principalOutstanding = summary.get("principalOutstanding");
|
||||
var businessDate = LocalDate.parse(businessEvent.getBusinessDate(), formatter);
|
||||
Double statusId = (Double) status.get("id");
|
||||
return Objects.equals(externalEvent.getType(), businessEvent.getType())
|
||||
&& Objects.equals(externalEvent.getBusinessDate(), businessDate)
|
||||
&& Objects.equals(statusId, businessEvent.getStatusId().doubleValue())
|
||||
&& Objects.equals(principalDisbursed, businessEvent.getPrincipalDisbursed())
|
||||
&& Objects.equals(principalOutstanding, businessEvent.getPrincipalOutstanding())
|
||||
&& loanTermVariationsMatch((List<Map<String, Object>>) externalEvent.getPayLoad().get("loanTermVariations"),
|
||||
businessEvent.loanTermVariationType);
|
||||
}).count();
|
||||
long count = allExternalEvents.stream().filter(externalEvent -> businessEvent.verify(externalEvent, formatter)).count();
|
||||
Assertions.assertEquals(1, count, "Expected business event not found " + businessEvent);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean loanTermVariationsMatch(final List<Map<String, Object>> loanTermVariations, final List<String> expectedTypes) {
|
||||
if (CollectionUtils.isEmpty(expectedTypes)) {
|
||||
return true;
|
||||
}
|
||||
final long numberOfMatches = expectedTypes
|
||||
.stream().filter(
|
||||
expectedType -> loanTermVariations.stream()
|
||||
.anyMatch(variation -> StringUtils
|
||||
.equals((String) ((Map<String, Object>) variation.get("termType")).get("value"), expectedType)))
|
||||
.count();
|
||||
|
||||
return numberOfMatches == expectedTypes.size();
|
||||
}
|
||||
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
@NoArgsConstructor
|
||||
public static class BusinessEvent {
|
||||
|
||||
public BusinessEvent(String type, String businessDate, Integer statusId, Double principalDisbursed, Double principalOutstanding) {
|
||||
this.type = type;
|
||||
this.businessDate = businessDate;
|
||||
String type;
|
||||
String businessDate;
|
||||
|
||||
boolean verify(@NotNull ExternalEventDTO externalEvent, DateTimeFormatter formatter) {
|
||||
var businessDate = LocalDate.parse(getBusinessDate(), formatter);
|
||||
|
||||
return Objects.equals(externalEvent.getType(), getType()) && Objects.equals(externalEvent.getBusinessDate(), businessDate);
|
||||
}
|
||||
}
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public static class LoanTransactionBusinessEvent extends BusinessEvent {
|
||||
|
||||
private Double amount;
|
||||
private Double outstandingLoanBalance;
|
||||
private Double principalPortion;
|
||||
private Double interestPortion;
|
||||
private Double feeChargesPortion;
|
||||
private Double penaltyChargesPortion;
|
||||
|
||||
public LoanTransactionBusinessEvent(String type, String businessDate, Double amount, Double outstandingLoanBalance,
|
||||
Double principalPortion, Double interestPortion, Double feeChargesPortion, Double penaltyChargesPortion) {
|
||||
super(type, businessDate);
|
||||
this.amount = amount;
|
||||
this.outstandingLoanBalance = outstandingLoanBalance;
|
||||
this.principalPortion = principalPortion;
|
||||
this.interestPortion = interestPortion;
|
||||
this.feeChargesPortion = feeChargesPortion;
|
||||
this.penaltyChargesPortion = penaltyChargesPortion;
|
||||
}
|
||||
|
||||
@Override
|
||||
boolean verify(ExternalEventDTO externalEvent, DateTimeFormatter formatter) {
|
||||
Object amount = externalEvent.getPayLoad().get("amount");
|
||||
Object outstandingLoanBalance = externalEvent.getPayLoad().get("outstandingLoanBalance");
|
||||
Object principalPortion = externalEvent.getPayLoad().get("principalPortion");
|
||||
Object interestPortion = externalEvent.getPayLoad().get("interestPortion");
|
||||
Object feePortion = externalEvent.getPayLoad().get("feeChargesPortion");
|
||||
Object penaltyPortion = externalEvent.getPayLoad().get("penaltyChargesPortion");
|
||||
|
||||
return super.verify(externalEvent, formatter) && Objects.equals(amount, getAmount())
|
||||
&& Objects.equals(outstandingLoanBalance, getOutstandingLoanBalance())
|
||||
&& Objects.equals(principalPortion, getPrincipalPortion()) && Objects.equals(interestPortion, getInterestPortion())
|
||||
&& Objects.equals(feePortion, getFeeChargesPortion()) && Objects.equals(penaltyPortion, getPenaltyChargesPortion());
|
||||
}
|
||||
}
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
@AllArgsConstructor
|
||||
public static class LoanBusinessEvent extends BusinessEvent {
|
||||
|
||||
private Integer statusId;
|
||||
private Double principalDisbursed;
|
||||
private Double principalOutstanding;
|
||||
private List<String> loanTermVariationType;
|
||||
|
||||
public LoanBusinessEvent(String type, String businessDate, Integer statusId, Double principalDisbursed,
|
||||
Double principalOutstanding) {
|
||||
super(type, businessDate);
|
||||
this.statusId = statusId;
|
||||
this.principalDisbursed = principalDisbursed;
|
||||
this.principalOutstanding = principalOutstanding;
|
||||
}
|
||||
|
||||
private String type;
|
||||
private String businessDate;
|
||||
private Integer statusId;
|
||||
private Double principalDisbursed;
|
||||
private Double principalOutstanding;
|
||||
private List<String> loanTermVariationType;
|
||||
public LoanBusinessEvent(String type, String businessDate, Integer statusId, Double principalDisbursed, Double principalOutstanding,
|
||||
List<String> loanTermVariationType) {
|
||||
super(type, businessDate);
|
||||
this.statusId = statusId;
|
||||
this.principalDisbursed = principalDisbursed;
|
||||
this.principalOutstanding = principalOutstanding;
|
||||
this.loanTermVariationType = loanTermVariationType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean verify(ExternalEventDTO externalEvent, DateTimeFormatter formatter) {
|
||||
Object summaryRes = externalEvent.getPayLoad().get("summary");
|
||||
Object statusRes = externalEvent.getPayLoad().get("status");
|
||||
Map<String, Object> summary = summaryRes instanceof Map ? (Map<String, Object>) summaryRes : Map.of();
|
||||
Map<String, Object> status = statusRes instanceof Map ? (Map<String, Object>) statusRes : Map.of();
|
||||
var principalDisbursed = summary.get("principalDisbursed");
|
||||
|
||||
var principalOutstanding = summary.get("principalOutstanding");
|
||||
Double statusId = (Double) status.get("id");
|
||||
return super.verify(externalEvent, formatter) && Objects.equals(statusId, getStatusId().doubleValue())
|
||||
&& Objects.equals(principalDisbursed, getPrincipalDisbursed())
|
||||
&& Objects.equals(principalOutstanding, getPrincipalOutstanding()) && loanTermVariationsMatch(
|
||||
(List<Map<String, Object>>) externalEvent.getPayLoad().get("loanTermVariations"), loanTermVariationType);
|
||||
}
|
||||
|
||||
private boolean loanTermVariationsMatch(final List<Map<String, Object>> loanTermVariations, final List<String> expectedTypes) {
|
||||
if (CollectionUtils.isEmpty(expectedTypes)) {
|
||||
return true;
|
||||
}
|
||||
final long numberOfMatches = expectedTypes.stream().filter(expectedType -> loanTermVariations.stream().anyMatch(
|
||||
variation -> StringUtils.equals((String) ((Map<String, Object>) variation.get("termType")).get("value"), expectedType)))
|
||||
.count();
|
||||
|
||||
return numberOfMatches == expectedTypes.size();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+412
@@ -0,0 +1,412 @@
|
||||
/**
|
||||
* 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.integrationtests;
|
||||
|
||||
import io.restassured.builder.RequestSpecBuilder;
|
||||
import io.restassured.builder.ResponseSpecBuilder;
|
||||
import io.restassured.http.ContentType;
|
||||
import io.restassured.specification.RequestSpecification;
|
||||
import io.restassured.specification.ResponseSpecification;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
|
||||
import org.apache.fineract.client.models.PostClientsResponse;
|
||||
import org.apache.fineract.client.models.PostLoanProductsResponse;
|
||||
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
|
||||
import org.apache.fineract.integrationtests.common.BusinessStepHelper;
|
||||
import org.apache.fineract.integrationtests.common.ClientHelper;
|
||||
import org.apache.fineract.integrationtests.common.Utils;
|
||||
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
|
||||
import org.junit.jupiter.api.Assertions;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@Slf4j
|
||||
public class LoanInterestRefundTest extends BaseLoanIntegrationTest {
|
||||
|
||||
private static ResponseSpecification responseSpec;
|
||||
private static RequestSpecification requestSpec;
|
||||
private static LoanTransactionHelper loanTransactionHelper;
|
||||
private static PostClientsResponse client;
|
||||
private static BusinessStepHelper businessStepHelper;
|
||||
|
||||
@BeforeAll
|
||||
public static void setup() {
|
||||
Utils.initializeRESTAssured();
|
||||
requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build();
|
||||
requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
|
||||
requestSpec.header("Fineract-Platform-TenantId", "default");
|
||||
responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
|
||||
loanTransactionHelper = new LoanTransactionHelper(requestSpec, responseSpec);
|
||||
ClientHelper clientHelper = new ClientHelper(requestSpec, responseSpec);
|
||||
client = clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyInterestRefundNotCreatedForPayoutRefundWhenTypesAreEmpty() {
|
||||
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||
runAt("1 January 2021", () -> {
|
||||
PostLoanProductsResponse loanProduct = loanProductHelper
|
||||
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
|
||||
.daysInYearType(DaysInYearType.ACTUAL) //
|
||||
.supportedInterestRefundTypes(new ArrayList<>()) //
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
|
||||
);
|
||||
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 9.9,
|
||||
12, null);
|
||||
Assertions.assertNotNull(loanId);
|
||||
loanIdRef.set(loanId);
|
||||
disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021");
|
||||
});
|
||||
runAt("22 January 2021", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper
|
||||
.makeLoanRepayment("PayoutRefund", "22 January 2021", 1000F, loanId.intValue());
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
|
||||
|
||||
logTransactions(loanId);
|
||||
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"),
|
||||
transaction(1000.0, "Payout Refund", "22 January 2021"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyInterestRefundNotCreatedForMerchantIssuedRefundWhenTypesAreEmpty() {
|
||||
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||
runAt("1 January 2021", () -> {
|
||||
PostLoanProductsResponse loanProduct = loanProductHelper
|
||||
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
|
||||
.daysInYearType(DaysInYearType.ACTUAL) //
|
||||
.supportedInterestRefundTypes(new ArrayList<>())//
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
|
||||
);
|
||||
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 9.9,
|
||||
12, null);
|
||||
Assertions.assertNotNull(loanId);
|
||||
loanIdRef.set(loanId);
|
||||
disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021");
|
||||
});
|
||||
runAt("22 January 2021", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper
|
||||
.makeLoanRepayment("MerchantIssuedRefund", "22 January 2021", 1000F, loanId.intValue());
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
|
||||
|
||||
logTransactions(loanId);
|
||||
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"),
|
||||
transaction(1000.0, "Merchant Issued Refund", "22 January 2021"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyInterestRefundCreatedForPayoutRefund() {
|
||||
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||
runAt("1 January 2021", () -> {
|
||||
PostLoanProductsResponse loanProduct = loanProductHelper
|
||||
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
|
||||
.daysInYearType(DaysInYearType.ACTUAL) //
|
||||
.supportedInterestRefundTypes(new ArrayList<>()).addSupportedInterestRefundTypesItem("PAYOUT_REFUND") //
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
|
||||
);
|
||||
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 9.99,
|
||||
12, null);
|
||||
Assertions.assertNotNull(loanId);
|
||||
loanIdRef.set(loanId);
|
||||
disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021");
|
||||
});
|
||||
runAt("22 January 2021", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper
|
||||
.makeLoanRepayment("PayoutRefund", "22 January 2021", 1000F, loanId.intValue());
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
|
||||
|
||||
logTransactions(loanId);
|
||||
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"),
|
||||
transaction(1000.0, "Payout Refund", "22 January 2021"), transaction(5.75, "Accrual", "22 January 2021"),
|
||||
transaction(5.75, "Interest Refund", "22 January 2021"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyInterestRefundCreatedForMerchantIssuedRefund() {
|
||||
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||
runAt("1 January 2021", () -> {
|
||||
PostLoanProductsResponse loanProduct = loanProductHelper
|
||||
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
|
||||
.daysInYearType(DaysInYearType.ACTUAL) //
|
||||
.supportedInterestRefundTypes(new ArrayList<>()).addSupportedInterestRefundTypesItem("MERCHANT_ISSUED_REFUND") //
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
|
||||
);
|
||||
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 9.99,
|
||||
12, null);
|
||||
Assertions.assertNotNull(loanId);
|
||||
loanIdRef.set(loanId);
|
||||
disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021");
|
||||
});
|
||||
runAt("22 January 2021", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper
|
||||
.makeLoanRepayment("MerchantIssuedRefund", "22 January 2021", 1000F, loanId.intValue());
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
|
||||
|
||||
logTransactions(loanId);
|
||||
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"),
|
||||
transaction(1000.0, "Merchant Issued Refund", "22 January 2021"), transaction(5.75, "Accrual", "22 January 2021"),
|
||||
transaction(5.75, "Interest Refund", "22 January 2021"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyUC01() {
|
||||
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||
runAt("1 January 2021", () -> {
|
||||
PostLoanProductsResponse loanProduct = loanProductHelper
|
||||
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
|
||||
.daysInYearType(DaysInYearType.ACTUAL) //
|
||||
.supportedInterestRefundTypes(new ArrayList<>()).addSupportedInterestRefundTypesItem("PAYOUT_REFUND") //
|
||||
.addSupportedInterestRefundTypesItem("MERCHANT_ISSUED_REFUND") //
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
|
||||
);
|
||||
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 9.99,
|
||||
12, null);
|
||||
Assertions.assertNotNull(loanId);
|
||||
loanIdRef.set(loanId);
|
||||
disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021");
|
||||
});
|
||||
runAt("22 January 2021", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper
|
||||
.makeLoanRepayment("PayoutRefund", "22 January 2021", 1000F, loanId.intValue());
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
|
||||
|
||||
logTransactions(loanId);
|
||||
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"),
|
||||
transaction(1000.0, "Payout Refund", "22 January 2021"), transaction(5.75, "Accrual", "22 January 2021"),
|
||||
transaction(5.75, "Interest Refund", "22 January 2021"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyUC02a() {
|
||||
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||
runAt("1 January 2021", () -> {
|
||||
PostLoanProductsResponse loanProduct = loanProductHelper
|
||||
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
|
||||
.daysInYearType(DaysInYearType.ACTUAL) //
|
||||
.supportedInterestRefundTypes(new ArrayList<>()).addSupportedInterestRefundTypesItem("PAYOUT_REFUND") //
|
||||
.addSupportedInterestRefundTypesItem("MERCHANT_ISSUED_REFUND") //
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
|
||||
);
|
||||
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 9.99,
|
||||
12, null);
|
||||
Assertions.assertNotNull(loanId);
|
||||
loanIdRef.set(loanId);
|
||||
disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021");
|
||||
});
|
||||
runAt("1 February 2021", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper
|
||||
.makeLoanRepayment("PayoutRefund", "1 February 2021", 1000F, loanId.intValue());
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
|
||||
|
||||
logTransactions(loanId);
|
||||
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"),
|
||||
transaction(1000.0, "Payout Refund", "01 February 2021"), transaction(8.48, "Accrual", "01 February 2021"),
|
||||
transaction(8.48, "Interest Refund", "01 February 2021"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyUC02b() {
|
||||
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||
runAt("1 January 2021", () -> {
|
||||
PostLoanProductsResponse loanProduct = loanProductHelper
|
||||
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
|
||||
.daysInYearType(DaysInYearType.ACTUAL) //
|
||||
.supportedInterestRefundTypes(new ArrayList<>()).addSupportedInterestRefundTypesItem("PAYOUT_REFUND") //
|
||||
.addSupportedInterestRefundTypesItem("MERCHANT_ISSUED_REFUND") //
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
|
||||
);
|
||||
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 9.99,
|
||||
12, null);
|
||||
Assertions.assertNotNull(loanId);
|
||||
loanIdRef.set(loanId);
|
||||
disburseLoan(loanId, BigDecimal.valueOf(1000), "1 January 2021");
|
||||
});
|
||||
runAt("1 February 2021", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment("Repayment",
|
||||
"1 February 2021", 87.89F, loanId.intValue());
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
|
||||
|
||||
logTransactions(loanId);
|
||||
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"),
|
||||
transaction(87.89, "Repayment", "01 February 2021"));
|
||||
});
|
||||
|
||||
runAt("9 February 2021", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper
|
||||
.makeLoanRepayment("PayoutRefund", "9 February 2021", 1000F, loanId.intValue());
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
|
||||
|
||||
logTransactions(loanId);
|
||||
verifyTransactions(loanId, transaction(1000.0, "Disbursement", "01 January 2021"),
|
||||
transaction(1000.0, "Payout Refund", "09 February 2021"), transaction(87.89, "Repayment", "01 February 2021"),
|
||||
transaction(10.50, "Interest Refund", "09 February 2021"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyUC03() {
|
||||
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||
runAt("1 January 2021", () -> {
|
||||
PostLoanProductsResponse loanProduct = loanProductHelper
|
||||
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
|
||||
.daysInYearType(DaysInYearType.ACTUAL) //
|
||||
.supportedInterestRefundTypes(new ArrayList<>())//
|
||||
.disallowExpectedDisbursements(true)//
|
||||
.multiDisburseLoan(true)//
|
||||
.maxTrancheCount(2).addSupportedInterestRefundTypesItem("PAYOUT_REFUND") //
|
||||
.addSupportedInterestRefundTypesItem("MERCHANT_ISSUED_REFUND") //
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
|
||||
);
|
||||
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 9.99,
|
||||
12, null);
|
||||
Assertions.assertNotNull(loanId);
|
||||
loanIdRef.set(loanId);
|
||||
disburseLoan(loanId, BigDecimal.valueOf(750), "1 January 2021");
|
||||
disburseLoan(loanId, BigDecimal.valueOf(250), "1 January 2021");
|
||||
});
|
||||
runAt("22 January 2021", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper
|
||||
.makeLoanRepayment("PayoutRefund", "22 January 2021", 1000F, loanId.intValue());
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
|
||||
|
||||
logTransactions(loanId);
|
||||
verifyTransactions(loanId, transaction(750.0, "Disbursement", "01 January 2021"),
|
||||
transaction(250.0, "Disbursement", "01 January 2021"), transaction(1000.0, "Payout Refund", "22 January 2021"),
|
||||
transaction(5.75, "Accrual", "22 January 2021"), transaction(5.75, "Interest Refund", "22 January 2021"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyUC04() {
|
||||
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||
runAt("1 January 2021", () -> {
|
||||
PostLoanProductsResponse loanProduct = loanProductHelper
|
||||
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
|
||||
.daysInYearType(DaysInYearType.ACTUAL) //
|
||||
.supportedInterestRefundTypes(new ArrayList<>()).disallowExpectedDisbursements(true).multiDisburseLoan(true)
|
||||
.maxTrancheCount(2).addSupportedInterestRefundTypesItem("PAYOUT_REFUND") //
|
||||
.addSupportedInterestRefundTypesItem("MERCHANT_ISSUED_REFUND") //
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
|
||||
);
|
||||
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 9.99,
|
||||
12, null);
|
||||
Assertions.assertNotNull(loanId);
|
||||
loanIdRef.set(loanId);
|
||||
disburseLoan(loanId, BigDecimal.valueOf(250), "1 January 2021");
|
||||
});
|
||||
runAt("4 January 2021", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
disburseLoan(loanId, BigDecimal.valueOf(750), "4 January 2021");
|
||||
});
|
||||
runAt("22 January 2021", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper
|
||||
.makeLoanRepayment("PayoutRefund", "22 January 2021", 1000F, loanId.intValue());
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
|
||||
|
||||
logTransactions(loanId);
|
||||
verifyTransactions(loanId, transaction(750.0, "Disbursement", "04 January 2021"),
|
||||
transaction(250.0, "Disbursement", "01 January 2021"), transaction(1000.0, "Payout Refund", "22 January 2021"),
|
||||
transaction(5.14, "Accrual", "22 January 2021"), transaction(5.14, "Interest Refund", "22 January 2021"));
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
public void verifyUC05() {
|
||||
AtomicReference<Long> loanIdRef = new AtomicReference<>();
|
||||
runAt("1 January 2021", () -> {
|
||||
PostLoanProductsResponse loanProduct = loanProductHelper
|
||||
.createLoanProduct(create4IProgressive().daysInMonthType(DaysInMonthType.ACTUAL) //
|
||||
.daysInYearType(DaysInYearType.ACTUAL) //
|
||||
.supportedInterestRefundTypes(new ArrayList<>()).disallowExpectedDisbursements(true).multiDisburseLoan(true)
|
||||
.maxTrancheCount(2).addSupportedInterestRefundTypesItem("PAYOUT_REFUND") //
|
||||
.addSupportedInterestRefundTypesItem("MERCHANT_ISSUED_REFUND") //
|
||||
.recalculationRestFrequencyType(RecalculationRestFrequencyType.DAILY) //
|
||||
);
|
||||
Long loanId = applyAndApproveProgressiveLoan(client.getClientId(), loanProduct.getResourceId(), "1 January 2021", 1000.0, 9.99,
|
||||
12, null);
|
||||
Assertions.assertNotNull(loanId);
|
||||
loanIdRef.set(loanId);
|
||||
disburseLoan(loanId, BigDecimal.valueOf(500), "1 January 2021");
|
||||
});
|
||||
runAt("7 January 2021", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
disburseLoan(loanId, BigDecimal.valueOf(500), "7 January 2021");
|
||||
});
|
||||
runAt("1 February 2021", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper.makeLoanRepayment("Repayment",
|
||||
"1 February 2021", 87.82F, loanId.intValue());
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
|
||||
|
||||
logTransactions(loanId);
|
||||
verifyTransactions(loanId, transaction(500.0, "Disbursement", "01 January 2021"),
|
||||
transaction(500.0, "Disbursement", "07 January 2021"), transaction(87.82, "Repayment", "01 February 2021"));
|
||||
});
|
||||
|
||||
runAt("9 February 2021", () -> {
|
||||
Long loanId = loanIdRef.get();
|
||||
PostLoansLoanIdTransactionsResponse postLoansLoanIdTransactionsResponse = loanTransactionHelper
|
||||
.makeLoanRepayment("PayoutRefund", "9 February 2021", 1000F, loanId.intValue());
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse);
|
||||
Assertions.assertNotNull(postLoansLoanIdTransactionsResponse.getResourceId());
|
||||
|
||||
logTransactions(loanId);
|
||||
verifyTransactions(loanId, transaction(500.0, "Disbursement", "01 January 2021"),
|
||||
transaction(500.0, "Disbursement", "07 January 2021"), transaction(1000.0, "Payout Refund", "09 February 2021"),
|
||||
transaction(87.82, "Repayment", "01 February 2021"), transaction(9.67, "Interest Refund", "09 February 2021"));
|
||||
});
|
||||
}
|
||||
|
||||
private void logTransactions(Long loanId) {
|
||||
GetLoansLoanIdResponse loanDetails = loanTransactionHelper.getLoan(requestSpec, responseSpec, loanId.intValue());
|
||||
assert loanDetails.getTransactions() != null;
|
||||
loanDetails.getTransactions()
|
||||
.forEach(tr -> log.info("Transaction {} {} {} ", tr.getType().getValue(), tr.getDate(), tr.getAmount()));
|
||||
}
|
||||
|
||||
}
|
||||
+4
-5
@@ -44,7 +44,6 @@ import org.apache.fineract.integrationtests.common.Utils;
|
||||
import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
|
||||
import org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
|
||||
import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
|
||||
import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.slf4j.Logger;
|
||||
@@ -89,8 +88,8 @@ public class LoanRefundTransactionTest extends BaseLoanIntegrationTest {
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||
.interestRatePerPeriod(108.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
|
||||
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.ACTUAL.getValue())
|
||||
.daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(4)//
|
||||
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.ACTUAL).daysInYearType(DaysInYearType.DAYS_360)
|
||||
.numberOfRepayments(4)//
|
||||
.maxInterestRatePerPeriod((double) 110)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(1L)//
|
||||
@@ -161,8 +160,8 @@ public class LoanRefundTransactionTest extends BaseLoanIntegrationTest {
|
||||
Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
|
||||
PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
|
||||
.interestRatePerPeriod(108.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
|
||||
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.ACTUAL.getValue())
|
||||
.daysInYearType(DaysInYearType.DAYS_360).numberOfRepayments(4)//
|
||||
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.ACTUAL).daysInYearType(DaysInYearType.DAYS_360)
|
||||
.numberOfRepayments(4)//
|
||||
.maxInterestRatePerPeriod((double) 110)//
|
||||
.repaymentEvery(1)//
|
||||
.repaymentFrequencyType(1L)//
|
||||
|
||||
+10
@@ -560,6 +560,16 @@ public class ExternalEventConfigurationHelper {
|
||||
loanTransactionAccrualActivityPreBusinessEvent.put("enabled", false);
|
||||
defaults.add(loanTransactionAccrualActivityPreBusinessEvent);
|
||||
|
||||
Map<String, Object> loanTransactionInterestRefundPostBusinessEvent = new HashMap<>();
|
||||
loanTransactionInterestRefundPostBusinessEvent.put("type", "LoanTransactionInterestRefundPostBusinessEvent");
|
||||
loanTransactionInterestRefundPostBusinessEvent.put("enabled", false);
|
||||
defaults.add(loanTransactionInterestRefundPostBusinessEvent);
|
||||
|
||||
Map<String, Object> loanTransactionInterestRefundPreBusinessEvent = new HashMap<>();
|
||||
loanTransactionInterestRefundPreBusinessEvent.put("type", "LoanTransactionInterestRefundPreBusinessEvent");
|
||||
loanTransactionInterestRefundPreBusinessEvent.put("enabled", false);
|
||||
defaults.add(loanTransactionInterestRefundPreBusinessEvent);
|
||||
|
||||
return defaults;
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user