This is an automated email from the ASF dual-hosted git repository.
bagrijp pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new c504996c1 FINERACT-2042 Credit Allocation for Principal and Interest
components
c504996c1 is described below
commit c504996c1d6cc4dd73a6eff2a91f43eec6e42bcf
Author: Peter Bagrij <[email protected]>
AuthorDate: Wed Feb 7 11:56:01 2024 +0100
FINERACT-2042 Credit Allocation for Principal and Interest components
---
.../domain/LoanTransactionRelation.java | 4 +-
...tLoanRepaymentScheduleTransactionProcessor.java | 2 +-
...dvancedPaymentScheduleTransactionProcessor.java | 189 ++++++++++
.../AccrualBasedAccountingProcessorForLoan.java | 5 +-
.../loanaccount/service/LoanAssembler.java | 7 +
...cedPaymentScheduleTransactionProcessorTest.java | 249 +++++++++++-
.../integrationtests/BaseLoanIntegrationTest.java | 37 +-
...ebackWithCreditAllocationsIntegrationTests.java | 418 +++++++++++++++++++++
.../common/loans/LoanTransactionHelper.java | 5 +
9 files changed, 891 insertions(+), 25 deletions(-)
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java
index 84ee76108..c76f73b81 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransactionRelation.java
@@ -62,7 +62,9 @@ public class LoanTransactionRelation extends
AbstractAuditableWithUTCDateTimeCus
public static LoanTransactionRelation linkToTransaction(@NotNull
LoanTransaction fromTransaction,
@NotNull LoanTransaction toTransaction,
LoanTransactionRelationTypeEnum relation) {
- return new LoanTransactionRelation(fromTransaction, toTransaction,
null, relation);
+ LoanTransactionRelation loanTransactionRelation = new
LoanTransactionRelation(fromTransaction, toTransaction, null, relation);
+
fromTransaction.getLoanTransactionRelations().add(loanTransactionRelation);
+ return loanTransactionRelation;
}
public static LoanTransactionRelation linkToCharge(@NotNull
LoanTransaction fromTransaction, @NotNull LoanCharge loanCharge,
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
index 89a2ec7b4..583dc130b 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/AbstractLoanRepaymentScheduleTransactionProcessor.java
@@ -474,7 +474,7 @@ public abstract class
AbstractLoanRepaymentScheduleTransactionProcessor implemen
}
- private void processCreditTransaction(LoanTransaction loanTransaction,
MoneyHolder overpaymentHolder, MonetaryCurrency currency,
+ protected void processCreditTransaction(LoanTransaction loanTransaction,
MoneyHolder overpaymentHolder, MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments) {
loanTransaction.resetDerivedComponents();
List<LoanTransactionToRepaymentScheduleMapping> transactionMappings =
new ArrayList<>();
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
index 37d43be4d..a8cc27c0d 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
@@ -20,16 +20,23 @@ package
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.im
import static java.util.stream.Collectors.mapping;
import static java.util.stream.Collectors.toList;
+import static
org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE;
+import static
org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST;
+import static
org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
+import static
org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL;
import java.math.BigDecimal;
import java.math.MathContext;
import java.time.LocalDate;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Comparator;
+import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
+import java.util.Objects;
import java.util.Optional;
import java.util.Set;
import java.util.function.Function;
@@ -41,22 +48,28 @@ import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.NotImplementedException;
import org.apache.commons.lang3.ObjectUtils;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
import org.apache.fineract.infrastructure.core.service.MathUtil;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import
org.apache.fineract.portfolio.loanaccount.domain.ChangedTransactionDetail;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
+import
org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleProcessingWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+import
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRelation;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionToRepaymentScheduleMapping;
import
org.apache.fineract.portfolio.loanaccount.domain.SingleLoanChargeRepaymentScheduleProcessingWrapper;
import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.AbstractLoanRepaymentScheduleTransactionProcessor;
import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
+import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
+import
org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.DueType;
import
org.apache.fineract.portfolio.loanproduct.domain.FutureInstallmentAllocationRule;
import
org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
@@ -172,6 +185,182 @@ public class AdvancedPaymentScheduleTransactionProcessor
extends AbstractLoanRep
}
}
+ private boolean hasNoCustomCreditAllocationRule(LoanTransaction
loanTransaction) {
+ return (loanTransaction.getLoan().getCreditAllocationRules() == null
|| !loanTransaction.getLoan().getCreditAllocationRules()
+ .stream().anyMatch(e ->
e.getTransactionType().getLoanTransactionType().equals(loanTransaction.getTypeOf())));
+ }
+
+ @Override
+ protected void processCreditTransaction(LoanTransaction loanTransaction,
MoneyHolder overpaymentHolder, MonetaryCurrency currency,
+ List<LoanRepaymentScheduleInstallment> installments) {
+ if (hasNoCustomCreditAllocationRule(loanTransaction)) {
+ super.processCreditTransaction(loanTransaction, overpaymentHolder,
currency, installments);
+ } else {
+ log.debug("Processing credit transaction with custom credit
allocation rules");
+
+ loanTransaction.resetDerivedComponents();
+ List<LoanTransactionToRepaymentScheduleMapping>
transactionMappings = new ArrayList<>();
+ final Comparator<LoanRepaymentScheduleInstallment> byDate =
Comparator.comparing(LoanRepaymentScheduleInstallment::getDueDate);
+ installments.sort(byDate);
+ final Money zeroMoney = Money.zero(currency);
+ Money transactionAmount = loanTransaction.getAmount(currency);
+ Money amountToDistribute = MathUtil
+
.negativeToZero(loanTransaction.getAmount(currency).minus(overpaymentHolder.getMoneyObject()));
+ Money repaidAmount =
MathUtil.negativeToZero(transactionAmount.minus(amountToDistribute));
+ loanTransaction.setOverPayments(repaidAmount);
+
overpaymentHolder.setMoneyObject(overpaymentHolder.getMoneyObject().minus(repaidAmount));
+
+ if (amountToDistribute.isGreaterThanZero()) {
+ if (loanTransaction.isChargeback()) {
+ Optional<LoanTransaction> originalTransaction =
loanTransaction.getLoan().getLoanTransactions(
+ tr ->
tr.getLoanTransactionRelations().stream().anyMatch(this.hasMatchingToLoanTransaction(loanTransaction)))
+ .stream().findFirst();
+ if (originalTransaction.isEmpty()) {
+ throw new RuntimeException("Chargeback transaction
must have an original transaction");
+ }
+
+ Map<AllocationType, BigDecimal> originalAllocation =
getOriginalAllocation(originalTransaction.get());
+ LoanCreditAllocationRule chargeBackAllocationRule =
getChargebackAllocationRules(loanTransaction);
+ Map<AllocationType, Money> chargebackAllocation =
calculateChargebackAllocationMap(originalAllocation,
+ amountToDistribute.getAmount(),
chargeBackAllocationRule.getAllocationTypes(), currency);
+
+
loanTransaction.updateComponents(chargebackAllocation.get(PRINCIPAL),
chargebackAllocation.get(INTEREST),
+ chargebackAllocation.get(FEE),
chargebackAllocation.get(PENALTY));
+
+ final LocalDate transactionDate =
loanTransaction.getTransactionDate();
+ boolean loanTransactionMapped = false;
+ LocalDate pastDueDate = null;
+ for (final LoanRepaymentScheduleInstallment
currentInstallment : installments) {
+ pastDueDate = currentInstallment.getDueDate();
+ if (!currentInstallment.isAdditional() &&
DateUtils.isAfter(currentInstallment.getDueDate(), transactionDate)) {
+
+
currentInstallment.addToCredits(transactionAmount.getAmount());
+ currentInstallment.addToPrincipal(transactionDate,
chargebackAllocation.get(PRINCIPAL));
+ Money originalInterest =
currentInstallment.getInterestCharged(currency);
+ currentInstallment.updateInterestCharged(
+
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
+
+ if (repaidAmount.isGreaterThanZero()) {
+
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(),
repaidAmount);
+
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
+ currentInstallment, repaidAmount,
zeroMoney, zeroMoney, zeroMoney));
+ }
+ loanTransactionMapped = true;
+ break;
+
+ // If already exists an additional installment
just update the due date and
+ // principal from the Loan chargeback / CBR
transaction
+ } else if (currentInstallment.isAdditional()) {
+ if (DateUtils.isAfter(transactionDate,
currentInstallment.getDueDate())) {
+
currentInstallment.updateDueDate(transactionDate);
+ }
+
currentInstallment.addToCredits(transactionAmount.getAmount());
+ currentInstallment.addToPrincipal(transactionDate,
chargebackAllocation.get(PRINCIPAL));
+ Money originalInterest =
currentInstallment.getInterestCharged(currency);
+ currentInstallment.updateInterestCharged(
+
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
+ if (repaidAmount.isGreaterThanZero()) {
+
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(),
repaidAmount);
+
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
+ currentInstallment, repaidAmount,
zeroMoney, zeroMoney, zeroMoney));
+ }
+ loanTransactionMapped = true;
+ break;
+ }
+ }
+
+ // New installment will be added (N+1 scenario)
+ if (!loanTransactionMapped) {
+ if
(loanTransaction.getTransactionDate().equals(pastDueDate)) {
+ LoanRepaymentScheduleInstallment
currentInstallment = installments.get(installments.size() - 1);
+
currentInstallment.addToCredits(transactionAmount.getAmount());
+ currentInstallment.addToPrincipal(transactionDate,
chargebackAllocation.get(PRINCIPAL));
+ Money originalInterest =
currentInstallment.getInterestCharged(currency);
+ currentInstallment.updateInterestCharged(
+
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
+ if (repaidAmount.isGreaterThanZero()) {
+
currentInstallment.payPrincipalComponent(loanTransaction.getTransactionDate(),
repaidAmount);
+
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
+ currentInstallment, repaidAmount,
zeroMoney, zeroMoney, zeroMoney));
+ }
+ } else {
+ Loan loan = loanTransaction.getLoan();
+ LoanRepaymentScheduleInstallment installment = new
LoanRepaymentScheduleInstallment(loan,
+ (installments.size() + 1), pastDueDate,
transactionDate, zeroMoney.getAmount(), zeroMoney.getAmount(),
+ zeroMoney.getAmount(),
zeroMoney.getAmount(), false, null);
+ installment.markAsAdditional();
+
installment.addToCredits(transactionAmount.getAmount());
+ installment.addToPrincipal(transactionDate,
chargebackAllocation.get(PRINCIPAL));
+ Money originalInterest =
installment.getInterestCharged(currency);
+ installment.updateInterestCharged(
+
originalInterest.plus(chargebackAllocation.get(INTEREST)).getAmountDefaultedToNullIfZero());
+
loan.addLoanRepaymentScheduleInstallment(installment);
+ if (repaidAmount.isGreaterThanZero()) {
+
installment.payPrincipalComponent(loanTransaction.getTransactionDate(),
repaidAmount);
+
transactionMappings.add(LoanTransactionToRepaymentScheduleMapping.createFrom(loanTransaction,
installment,
+ repaidAmount, zeroMoney, zeroMoney,
zeroMoney));
+ }
+ }
+ }
+
+
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
+ }
+ }
+ }
+ }
+
+ @NotNull
+ private LoanCreditAllocationRule
getChargebackAllocationRules(LoanTransaction loanTransaction) {
+ LoanCreditAllocationRule chargeBackAllocationRule =
loanTransaction.getLoan().getCreditAllocationRules().stream()
+ .filter(tr ->
tr.getTransactionType().equals(CreditAllocationTransactionType.CHARGEBACK)).findFirst().orElseThrow();
+ return chargeBackAllocationRule;
+ }
+
+ @NotNull
+ private Map<AllocationType, BigDecimal>
getOriginalAllocation(LoanTransaction originalLoanTransaction) {
+ Map<AllocationType, BigDecimal> originalAllocation = new HashMap<>();
+ originalAllocation.put(PRINCIPAL,
originalLoanTransaction.getPrincipalPortion());
+ originalAllocation.put(INTEREST,
originalLoanTransaction.getInterestPortion());
+ originalAllocation.put(PENALTY,
originalLoanTransaction.getPenaltyChargesPortion());
+ originalAllocation.put(FEE,
originalLoanTransaction.getFeeChargesPortion());
+ return originalAllocation;
+ }
+
+ protected Map<AllocationType, Money>
calculateChargebackAllocationMap(Map<AllocationType, BigDecimal>
originalAllocation,
+ BigDecimal amountToDistribute, List<AllocationType>
allocationTypes, MonetaryCurrency currency) {
+ BigDecimal remainingAmount = amountToDistribute;
+ Map<AllocationType, Money> result = new HashMap<>();
+ Arrays.stream(AllocationType.values()).forEach(allocationType ->
result.put(allocationType, Money.of(currency, BigDecimal.ZERO)));
+ for (AllocationType allocationType : allocationTypes) {
+ if (remainingAmount.compareTo(BigDecimal.ZERO) > 0) {
+ BigDecimal originalAmount =
originalAllocation.get(allocationType);
+ if (originalAmount != null &&
remainingAmount.compareTo(originalAmount) > 0
+ && originalAmount.compareTo(BigDecimal.ZERO) > 0) {
+ result.put(allocationType, Money.of(currency,
originalAmount));
+ remainingAmount = remainingAmount.subtract(originalAmount);
+ } else if (originalAmount != null &&
remainingAmount.compareTo(originalAmount) <= 0
+ && originalAmount.compareTo(BigDecimal.ZERO) > 0) {
+ result.put(allocationType, Money.of(currency,
remainingAmount));
+ remainingAmount = BigDecimal.ZERO;
+ }
+ }
+ }
+ return result;
+ }
+
+ private Predicate<LoanTransactionRelation>
hasMatchingToLoanTransaction(LoanTransaction loanTransaction) {
+ return relation -> {
+ if (loanTransaction.getId() != null &&
relation.getToTransaction().getId() != null) {
+ return Objects.equals(relation.getToTransaction().getId(),
loanTransaction.getId());
+ } else {
+ return
relation.getToTransaction().getTypeOf().equals(loanTransaction.getTypeOf())
+ &&
relation.getToTransaction().getAmount().compareTo(loanTransaction.getAmount())
== 0
+ && relation.getToTransaction().isReversed() ==
loanTransaction.isReversed()
+ &&
relation.getToTransaction().getTransactionDate().compareTo(loanTransaction.getTransactionDate())
== 0;
+ }
+ };
+ }
+
@Override
protected void handleRefund(LoanTransaction loanTransaction,
MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges) {
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
index d9351deee..8ba14d78f 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/accounting/journalentry/service/AccrualBasedAccountingProcessorForLoan.java
@@ -430,11 +430,12 @@ public class AccrualBasedAccountingProcessorForLoan
implements AccountingProcess
final BigDecimal overpaidAmount =
Objects.isNull(loanTransactionDTO.getOverPayment()) ? BigDecimal.ZERO
: loanTransactionDTO.getOverPayment();
- if (BigDecimal.ZERO.compareTo(overpaidAmount) == 0) {
+ if (BigDecimal.ZERO.compareTo(overpaidAmount) == 0) { // when no
overpay
helper.createJournalEntriesAndReversalsForLoan(office,
currencyCode, AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue(),
AccrualAccountsForLoan.FUND_SOURCE.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate,
amount, isReversal);
- } else if (overpaidAmount.compareTo(amount) >= 0) {
+ } else if (overpaidAmount.compareTo(amount) >= 0) { // when the
overpay amount is matching with the normal
+ // amount
helper.createJournalEntriesAndReversalsForLoan(office,
currencyCode, AccrualAccountsForLoan.OVERPAYMENT.getValue(),
AccrualAccountsForLoan.FUND_SOURCE.getValue(),
loanProductId, paymentTypeId, loanId, transactionId, transactionDate,
amount, isReversal);
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java
index 0201eaea9..36b9e86de 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanAssembler.java
@@ -66,6 +66,7 @@ import
org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanCollateralManagement;
+import
org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanDisbursementDetails;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
@@ -390,6 +391,12 @@ public class LoanAssembler {
r.getFutureInstallmentAllocationRule()))
.toList();
loanApplication.setPaymentAllocationRules(loanPaymentAllocationRules);
+
+ if (loanProduct.getCreditAllocationRules() != null &&
loanProduct.getCreditAllocationRules().size() > 0) {
+ List<LoanCreditAllocationRule> loanCreditAllocationRules =
loanProduct.getCreditAllocationRules().stream()
+ .map(r -> new
LoanCreditAllocationRule(loanApplication, r.getTransactionType(),
r.getAllocationTypes())).toList();
+
loanApplication.setCreditAllocationRules(loanCreditAllocationRules);
+ }
}
}
}
diff --git
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java
index e2ae5fa40..6facec110 100644
---
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java
+++
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessorTest.java
@@ -18,13 +18,25 @@
*/
package
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl;
+import static
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.CHARGEBACK;
+import static
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType.REPAYMENT;
+import static
org.apache.fineract.portfolio.loanproduct.domain.AllocationType.FEE;
+import static
org.apache.fineract.portfolio.loanproduct.domain.AllocationType.INTEREST;
+import static
org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PENALTY;
+import static
org.apache.fineract.portfolio.loanproduct.domain.AllocationType.PRINCIPAL;
import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.refEq;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.times;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
+import java.util.ArrayList;
+import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -39,21 +51,27 @@ import
org.apache.fineract.organisation.monetary.domain.MoneyHelper;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
import org.apache.fineract.portfolio.loanaccount.domain.LoanChargePaidBy;
+import
org.apache.fineract.portfolio.loanaccount.domain.LoanCreditAllocationRule;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanPaymentAllocationRule;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallment;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType;
import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.MoneyHolder;
import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
+import org.apache.fineract.portfolio.loanproduct.domain.AllocationType;
+import
org.apache.fineract.portfolio.loanproduct.domain.CreditAllocationTransactionType;
import
org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import
org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationTransactionType;
import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
+import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterAll;
import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeAll;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.ArgumentCaptor;
import org.mockito.MockedStatic;
import org.mockito.Mockito;
import org.mockito.junit.jupiter.MockitoExtension;
@@ -95,13 +113,13 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
final BigDecimal chargeAmount = BigDecimal.valueOf(100);
LocalDate disbursementDate = LocalDate.of(2023, 1, 1);
LocalDate transactionDate = LocalDate.of(2023, 1, 5);
- LoanTransaction loanTransaction = Mockito.mock(LoanTransaction.class);
+ LoanTransaction loanTransaction = mock(LoanTransaction.class);
MonetaryCurrency currency = MONETARY_CURRENCY;
- LoanCharge charge = Mockito.mock(LoanCharge.class);
- LoanChargePaidBy chargePaidBy = Mockito.mock(LoanChargePaidBy.class);
+ LoanCharge charge = mock(LoanCharge.class);
+ LoanChargePaidBy chargePaidBy = mock(LoanChargePaidBy.class);
Money overpaidAmount = Money.zero(currency);
Money zero = Money.zero(currency);
- Loan loan = Mockito.mock(Loan.class);
+ Loan loan = mock(Loan.class);
Money chargeAmountMoney = Money.of(currency, chargeAmount);
LoanRepaymentScheduleInstallment installment = Mockito
.spy(new LoanRepaymentScheduleInstallment(loan, 1,
disbursementDate, disbursementDate.plusMonths(1), BigDecimal.valueOf(0L),
@@ -138,13 +156,13 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
BigDecimal chargeAmount = BigDecimal.valueOf(100.00);
LocalDate disbursementDate = LocalDate.of(2023, 1, 1);
LocalDate transactionDate = LocalDate.of(2023, 1, 5);
- LoanTransaction loanTransaction = Mockito.mock(LoanTransaction.class);
+ LoanTransaction loanTransaction = mock(LoanTransaction.class);
MonetaryCurrency currency = MONETARY_CURRENCY;
- LoanCharge charge = Mockito.mock(LoanCharge.class);
- LoanChargePaidBy chargePaidBy = Mockito.mock(LoanChargePaidBy.class);
+ LoanCharge charge = mock(LoanCharge.class);
+ LoanChargePaidBy chargePaidBy = mock(LoanChargePaidBy.class);
Money overpaidAmount = Money.zero(currency);
Money zero = Money.zero(currency);
- Loan loan = Mockito.mock(Loan.class);
+ Loan loan = mock(Loan.class);
Money chargeAmountMoney = Money.of(currency, chargeAmount);
BigDecimal transactionAmount = BigDecimal.valueOf(20.00);
Money transactionAmountMoney = Money.of(currency, transactionAmount);
@@ -184,18 +202,18 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
BigDecimal chargeAmount = BigDecimal.valueOf(100.00);
LocalDate disbursementDate = LocalDate.of(2023, 1, 1);
LocalDate transactionDate = disbursementDate.plusMonths(1);
- LoanTransaction loanTransaction = Mockito.mock(LoanTransaction.class);
+ LoanTransaction loanTransaction = mock(LoanTransaction.class);
MonetaryCurrency currency = MONETARY_CURRENCY;
- LoanCharge charge = Mockito.mock(LoanCharge.class);
- LoanChargePaidBy chargePaidBy = Mockito.mock(LoanChargePaidBy.class);
+ LoanCharge charge = mock(LoanCharge.class);
+ LoanChargePaidBy chargePaidBy = mock(LoanChargePaidBy.class);
Money overpaidAmount = Money.zero(currency);
Money zero = Money.zero(currency);
- Loan loan = Mockito.mock(Loan.class);
- LoanProductRelatedDetail loanProductRelatedDetail =
Mockito.mock(LoanProductRelatedDetail.class);
+ Loan loan = mock(Loan.class);
+ LoanProductRelatedDetail loanProductRelatedDetail =
mock(LoanProductRelatedDetail.class);
Money chargeAmountMoney = Money.of(currency, chargeAmount);
BigDecimal transactionAmount = BigDecimal.valueOf(120.00);
Money transactionAmountMoney = Money.of(currency, transactionAmount);
- LoanPaymentAllocationRule loanPaymentAllocationRule =
Mockito.mock(LoanPaymentAllocationRule.class);
+ LoanPaymentAllocationRule loanPaymentAllocationRule =
mock(LoanPaymentAllocationRule.class);
LoanRepaymentScheduleInstallment installment = Mockito
.spy(new LoanRepaymentScheduleInstallment(loan, 1,
disbursementDate, transactionDate, BigDecimal.valueOf(100L),
BigDecimal.valueOf(0L), chargeAmount,
BigDecimal.valueOf(0L), false, null, BigDecimal.ZERO));
@@ -232,4 +250,207 @@ class AdvancedPaymentScheduleTransactionProcessorTest {
assertEquals(0,
BigDecimal.valueOf(80).compareTo(installment.getPrincipalOutstanding(currency).getAmount()));
Mockito.verify(loan, Mockito.times(1)).getPaymentAllocationRules();
}
+
+ @Test
+ public void
testProcessCreditTransactionWithAllocationRuleInterestAndPrincipal() {
+ // given
+ Loan loan = mock(Loan.class);
+ LoanCreditAllocationRule mockCreditAllocationRule =
createMockCreditAllocationRule(INTEREST, PRINCIPAL, PENALTY, FEE);
+
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
+ LoanTransaction repayment = createRepayment(loan);
+
lenient().when(loan.getLoanTransactions(any())).thenReturn(List.of(repayment));
+
+ LoanTransaction chargeBackTransaction =
createChargebackTransaction(loan);
+ MoneyHolder overpaymentHolder = new
MoneyHolder(Money.zero(MONETARY_CURRENCY));
+ List<LoanRepaymentScheduleInstallment> installments = new
ArrayList<>();
+ LoanRepaymentScheduleInstallment installment =
createMockInstallment(LocalDate.of(2023, 1, 31), false);
+ installments.add(installment);
+
+ // when
+ underTest.processCreditTransaction(chargeBackTransaction,
overpaymentHolder, MONETARY_CURRENCY, installments);
+
+ // then
+ Mockito.verify(installment, Mockito.times(1)).addToCredits(new
BigDecimal("25.00"));
+ Mockito.verify(installment,
Mockito.times(1)).updateInterestCharged(new BigDecimal("20.00"));
+ ArgumentCaptor<LocalDate> localDateArgumentCaptor =
ArgumentCaptor.forClass(LocalDate.class);
+ ArgumentCaptor<Money> moneyCaptor =
ArgumentCaptor.forClass(Money.class);
+ Mockito.verify(installment,
Mockito.times(1)).addToPrincipal(localDateArgumentCaptor.capture(),
moneyCaptor.capture());
+ Assertions.assertEquals(LocalDate.of(2023, 1, 1),
localDateArgumentCaptor.getValue());
+ assertEquals(0,
moneyCaptor.getValue().getAmount().compareTo(BigDecimal.valueOf(5.0)));
+
+ ArgumentCaptor<Money> principal = ArgumentCaptor.forClass(Money.class);
+ ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
+ ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
+ ArgumentCaptor<Money> penalty = ArgumentCaptor.forClass(Money.class);
+ Mockito.verify(chargeBackTransaction,
times(1)).updateComponents(principal.capture(), interest.capture(),
fee.capture(),
+ penalty.capture());
+ assertEquals(0,
principal.getValue().getAmount().compareTo(BigDecimal.valueOf(5.0)));
+ assertEquals(0,
interest.getValue().getAmount().compareTo(BigDecimal.valueOf(20.0)));
+ assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.ZERO));
+ assertEquals(0,
penalty.getValue().getAmount().compareTo(BigDecimal.ZERO));
+ }
+
+ @Test
+ public void
testProcessCreditTransactionWithAllocationRulePrincipalAndInterest() {
+ // given
+ Loan loan = mock(Loan.class);
+ LoanCreditAllocationRule mockCreditAllocationRule =
createMockCreditAllocationRule(PRINCIPAL, INTEREST, PENALTY, FEE);
+
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
+ LoanTransaction repayment = createRepayment(loan);
+
lenient().when(loan.getLoanTransactions(any())).thenReturn(List.of(repayment));
+
+ LoanTransaction chargeBackTransaction =
createChargebackTransaction(loan);
+ MoneyHolder overpaymentHolder = new
MoneyHolder(Money.zero(MONETARY_CURRENCY));
+ List<LoanRepaymentScheduleInstallment> installments = new
ArrayList<>();
+ LoanRepaymentScheduleInstallment installment =
createMockInstallment(LocalDate.of(2023, 1, 31), false);
+ installments.add(installment);
+
+ // when
+ underTest.processCreditTransaction(chargeBackTransaction,
overpaymentHolder, MONETARY_CURRENCY, installments);
+
+ // then
+ Mockito.verify(installment, Mockito.times(1)).addToCredits(new
BigDecimal("25.00"));
+ Mockito.verify(installment,
Mockito.times(1)).updateInterestCharged(new BigDecimal("15.00"));
+ ArgumentCaptor<LocalDate> localDateArgumentCaptor =
ArgumentCaptor.forClass(LocalDate.class);
+ ArgumentCaptor<Money> moneyCaptor =
ArgumentCaptor.forClass(Money.class);
+ Mockito.verify(installment,
Mockito.times(1)).addToPrincipal(localDateArgumentCaptor.capture(),
moneyCaptor.capture());
+ Assertions.assertEquals(LocalDate.of(2023, 1, 1),
localDateArgumentCaptor.getValue());
+ assertEquals(0,
moneyCaptor.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
+
+ ArgumentCaptor<Money> principal = ArgumentCaptor.forClass(Money.class);
+ ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
+ ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
+ ArgumentCaptor<Money> penalty = ArgumentCaptor.forClass(Money.class);
+ Mockito.verify(chargeBackTransaction,
times(1)).updateComponents(principal.capture(), interest.capture(),
fee.capture(),
+ penalty.capture());
+ assertEquals(0,
principal.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
+ assertEquals(0,
interest.getValue().getAmount().compareTo(BigDecimal.valueOf(15.0)));
+ assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.ZERO));
+ assertEquals(0,
penalty.getValue().getAmount().compareTo(BigDecimal.ZERO));
+ }
+
+ @Test
+ public void
testProcessCreditTransactionWithAllocationRulePrincipalAndInterestWithAdditionalInstallment()
{
+ // given
+ Loan loan = mock(Loan.class);
+ LoanCreditAllocationRule mockCreditAllocationRule =
createMockCreditAllocationRule(PRINCIPAL, INTEREST, PENALTY, FEE);
+
Mockito.when(loan.getCreditAllocationRules()).thenReturn(List.of(mockCreditAllocationRule));
+ LoanTransaction repayment = createRepayment(loan);
+
lenient().when(loan.getLoanTransactions(any())).thenReturn(List.of(repayment));
+
+ LoanTransaction chargeBackTransaction =
createChargebackTransaction(loan);
+ MoneyHolder overpaymentHolder = new
MoneyHolder(Money.zero(MONETARY_CURRENCY));
+ List<LoanRepaymentScheduleInstallment> installments = new
ArrayList<>();
+ LoanRepaymentScheduleInstallment installment1 =
createMockInstallment(LocalDate.of(2022, 12, 20), false);
+ LoanRepaymentScheduleInstallment installment2 =
createMockInstallment(LocalDate.of(2022, 12, 27), true);
+ installments.add(installment1);
+ installments.add(installment2);
+
+ // when
+ underTest.processCreditTransaction(chargeBackTransaction,
overpaymentHolder, MONETARY_CURRENCY, installments);
+
+ // then
+ Mockito.verify(installment2, Mockito.times(1)).addToCredits(new
BigDecimal("25.00"));
+ Mockito.verify(installment2,
Mockito.times(1)).updateInterestCharged(new BigDecimal("15.00"));
+ ArgumentCaptor<LocalDate> localDateArgumentCaptor =
ArgumentCaptor.forClass(LocalDate.class);
+ ArgumentCaptor<Money> moneyCaptor =
ArgumentCaptor.forClass(Money.class);
+ Mockito.verify(installment2,
Mockito.times(1)).addToPrincipal(localDateArgumentCaptor.capture(),
moneyCaptor.capture());
+ Assertions.assertEquals(LocalDate.of(2023, 1, 1),
localDateArgumentCaptor.getValue());
+ assertEquals(0,
moneyCaptor.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
+
+ ArgumentCaptor<Money> principal = ArgumentCaptor.forClass(Money.class);
+ ArgumentCaptor<Money> interest = ArgumentCaptor.forClass(Money.class);
+ ArgumentCaptor<Money> fee = ArgumentCaptor.forClass(Money.class);
+ ArgumentCaptor<Money> penalty = ArgumentCaptor.forClass(Money.class);
+ Mockito.verify(chargeBackTransaction,
times(1)).updateComponents(principal.capture(), interest.capture(),
fee.capture(),
+ penalty.capture());
+ assertEquals(0,
principal.getValue().getAmount().compareTo(BigDecimal.valueOf(10.0)));
+ assertEquals(0,
interest.getValue().getAmount().compareTo(BigDecimal.valueOf(15.0)));
+ assertEquals(0, fee.getValue().getAmount().compareTo(BigDecimal.ZERO));
+ assertEquals(0,
penalty.getValue().getAmount().compareTo(BigDecimal.ZERO));
+ }
+
+ private LoanRepaymentScheduleInstallment createMockInstallment(LocalDate
localDate, boolean isAdditional) {
+ LoanRepaymentScheduleInstallment installment =
mock(LoanRepaymentScheduleInstallment.class);
+ lenient().when(installment.isAdditional()).thenReturn(isAdditional);
+ lenient().when(installment.getDueDate()).thenReturn(localDate);
+ Money interestCharged = Money.of(MONETARY_CURRENCY, BigDecimal.ZERO);
+
lenient().when(installment.getInterestCharged(MONETARY_CURRENCY)).thenReturn(interestCharged);
+ return installment;
+ }
+
+ @NotNull
+ private LoanCreditAllocationRule
createMockCreditAllocationRule(AllocationType... allocationTypes) {
+ LoanCreditAllocationRule mockCreditAllocationRule =
mock(LoanCreditAllocationRule.class);
+
lenient().when(mockCreditAllocationRule.getTransactionType()).thenReturn(CreditAllocationTransactionType.CHARGEBACK);
+
lenient().when(mockCreditAllocationRule.getAllocationTypes()).thenReturn(Arrays.asList(allocationTypes));
+ return mockCreditAllocationRule;
+ }
+
+ private LoanTransaction createRepayment(Loan loan) {
+ LoanTransaction repayment = mock(LoanTransaction.class);
+ lenient().when(repayment.getLoan()).thenReturn(loan);
+ lenient().when(repayment.isRepayment()).thenReturn(true);
+ lenient().when(repayment.getTypeOf()).thenReturn(REPAYMENT);
+
lenient().when(repayment.getPrincipalPortion()).thenReturn(BigDecimal.valueOf(10));
+
lenient().when(repayment.getInterestPortion()).thenReturn(BigDecimal.valueOf(20));
+
lenient().when(repayment.getFeeChargesPortion()).thenReturn(BigDecimal.ZERO);
+
lenient().when(repayment.getPenaltyChargesPortion()).thenReturn(BigDecimal.ZERO);
+ return repayment;
+ }
+
+ private LoanTransaction createChargebackTransaction(Loan loan) {
+ LoanTransaction chargeback = mock(LoanTransaction.class);
+ lenient().when(chargeback.isChargeback()).thenReturn(true);
+ lenient().when(chargeback.getTypeOf()).thenReturn(CHARGEBACK);
+ lenient().when(chargeback.getLoan()).thenReturn(loan);
+
lenient().when(chargeback.getAmount()).thenReturn(BigDecimal.valueOf(25));
+ Money amount = Money.of(MONETARY_CURRENCY, BigDecimal.valueOf(25));
+
lenient().when(chargeback.getAmount(MONETARY_CURRENCY)).thenReturn(amount);
+
lenient().when(chargeback.getTransactionDate()).thenReturn(LocalDate.of(2023,
1, 1));
+ return chargeback;
+ }
+
+ @Test
+ public void calculateChargebackAllocationMap() {
+ Map<AllocationType, Money> result;
+ MonetaryCurrency currency = mock(MonetaryCurrency.class);
+
+ result =
underTest.calculateChargebackAllocationMap(allocationMap(50.0, 100.0, 200.0,
12.0), BigDecimal.valueOf(50.0),
+ List.of(PRINCIPAL, INTEREST, FEE, PENALTY), currency);
+ verify(allocationMap(50.0, 0, 0, 0), result);
+
+ result =
underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0,
12.0), BigDecimal.valueOf(50.0),
+ List.of(PRINCIPAL, INTEREST, FEE, PENALTY), currency);
+ verify(allocationMap(40.0, 10, 0, 0), result);
+
+ result =
underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0,
12.0), BigDecimal.valueOf(50.0),
+ List.of(PRINCIPAL, FEE, PENALTY, INTEREST), currency);
+ verify(allocationMap(40.0, 0, 10, 0), result);
+
+ result =
underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0,
12.0), BigDecimal.valueOf(340.0),
+ List.of(PRINCIPAL, FEE, PENALTY, INTEREST), currency);
+ verify(allocationMap(40.0, 88.0, 200.0, 12.0), result);
+
+ result =
underTest.calculateChargebackAllocationMap(allocationMap(40.0, 100.0, 200.0,
12.0), BigDecimal.valueOf(352.0),
+ List.of(PRINCIPAL, FEE, PENALTY, INTEREST), currency);
+ verify(allocationMap(40.0, 100.0, 200.0, 12.0), result);
+ }
+
+ private void verify(Map<AllocationType, BigDecimal> expected,
Map<AllocationType, Money> actual) {
+ Assertions.assertEquals(expected.size(), actual.size());
+ expected.forEach((k, v) -> {
+ Assertions.assertEquals(0, v.compareTo(actual.get(k).getAmount()),
"Not matching for " + k);
+ });
+ }
+
+ private Map<AllocationType, BigDecimal> allocationMap(double principal,
double interest, double fee, double penalty) {
+ Map<AllocationType, BigDecimal> allocationMap = new HashMap<>();
+ allocationMap.put(AllocationType.PRINCIPAL,
BigDecimal.valueOf(principal));
+ allocationMap.put(AllocationType.INTEREST,
BigDecimal.valueOf(interest));
+ allocationMap.put(AllocationType.FEE, BigDecimal.valueOf(fee));
+ allocationMap.put(AllocationType.PENALTY, BigDecimal.valueOf(penalty));
+ return allocationMap;
+ }
+
}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
index a406ca770..1352ff0cb 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/BaseLoanIntegrationTest.java
@@ -61,6 +61,8 @@ import
org.apache.fineract.client.models.PostLoansLoanIdChargesResponse;
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
+import org.apache.fineract.client.models.PostLoansLoanIdTransactionsResponse;
+import
org.apache.fineract.client.models.PostLoansLoanIdTransactionsTransactionIdRequest;
import org.apache.fineract.client.models.PostLoansRequest;
import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.client.util.CallFailedRuntimeException;
@@ -335,7 +337,7 @@ public abstract class BaseLoanIntegrationTest {
&& Objects.equals(item.getPenaltyChargesPortion(),
tr.penaltyPortion) //
&& Objects.equals(item.getUnrecognizedIncomePortion(),
tr.unrecognizedPortion) //
);
- Assertions.assertTrue(found, "Required transaction not found:
" + tr);
+ Assertions.assertTrue(found, "Required transaction not found:
" + tr);
});
}
}
@@ -414,6 +416,14 @@ public abstract class BaseLoanIntegrationTest {
"%d. installment's fee charges due is different,
expected: %.2f, actual: %.2f".formatted(i, feeAmount, feeDue));
}
+ Double penaltyAmount = installments[i].penaltyAmount;
+ Double penaltyDue = period.getPenaltyChargesDue();
+ if (penaltyAmount != null) {
+ Assertions.assertEquals(penaltyAmount, penaltyDue,
+ "%d. installment's penalty charges due is
different, expected: %.2f, actual: %.2f".formatted(i, penaltyAmount,
+ penaltyDue));
+ }
+
Double outstandingAmount =
installments[i].totalOutstandingAmount;
Double totalOutstanding =
period.getTotalOutstandingForPeriod();
if (outstandingAmount != null) {
@@ -497,10 +507,17 @@ public abstract class BaseLoanIntegrationTest {
return applyAndApproveLoan(clientId, loanProductId,
loanDisbursementDate, amount, 1);
}
- protected void addRepaymentForLoan(Long loanId, Double amount, String
date) {
+ protected Long addRepaymentForLoan(Long loanId, Double amount, String
date) {
String firstRepaymentUUID = UUID.randomUUID().toString();
- loanTransactionHelper.makeLoanRepayment(loanId, new
PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN)
-
.transactionDate(date).locale("en").transactionAmount(amount).externalId(firstRepaymentUUID));
+ PostLoansLoanIdTransactionsResponse response =
loanTransactionHelper.makeLoanRepayment(loanId,
+ new
PostLoansLoanIdTransactionsRequest().dateFormat(DATETIME_PATTERN).transactionDate(date).locale("en")
+
.transactionAmount(amount).externalId(firstRepaymentUUID));
+ return response.getResourceId();
+ }
+
+ protected void addChargebackForLoan(Long loanId, Long transactionId,
Double amount) {
+ loanTransactionHelper.chargebackLoanTransaction(loanId, transactionId,
+ new
PostLoansLoanIdTransactionsTransactionIdRequest().locale("en").transactionAmount(amount).paymentTypeId(1L));
}
protected PostChargesResponse createCharge(Double amount) {
@@ -538,17 +555,22 @@ public abstract class BaseLoanIntegrationTest {
}
protected Installment installment(double principalAmount, Boolean
completed, String dueDate) {
- return new Installment(principalAmount, null, null, null, completed,
dueDate);
+ return new Installment(principalAmount, null, null, null, null,
completed, dueDate);
}
protected Installment installment(double principalAmount, double
interestAmount, double totalOutstandingAmount, Boolean completed,
String dueDate) {
- return new Installment(principalAmount, interestAmount, null,
totalOutstandingAmount, completed, dueDate);
+ return new Installment(principalAmount, interestAmount, null, null,
totalOutstandingAmount, completed, dueDate);
}
protected Installment installment(double principalAmount, double
interestAmount, double feeAmount, double totalOutstandingAmount,
Boolean completed, String dueDate) {
- return new Installment(principalAmount, interestAmount, feeAmount,
totalOutstandingAmount, completed, dueDate);
+ return new Installment(principalAmount, interestAmount, feeAmount,
null, totalOutstandingAmount, completed, dueDate);
+ }
+
+ protected Installment installment(double principalAmount, double
interestAmount, double feeAmount, double penaltyAmount,
+ double totalOutstandingAmount, Boolean completed, String dueDate) {
+ return new Installment(principalAmount, interestAmount, feeAmount,
penaltyAmount, totalOutstandingAmount, completed, dueDate);
}
protected BatchRequestBuilder batchRequest() {
@@ -655,6 +677,7 @@ public abstract class BaseLoanIntegrationTest {
Double principalAmount;
Double interestAmount;
Double feeAmount;
+ Double penaltyAmount;
Double totalOutstandingAmount;
Boolean completed;
String dueDate;
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java
new file mode 100644
index 000000000..620a1bb0d
--- /dev/null
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanChargebackWithCreditAllocationsIntegrationTests.java
@@ -0,0 +1,418 @@
+/**
+ * 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 java.math.BigDecimal;
+import java.util.Arrays;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicInteger;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.client.models.AdvancedPaymentData;
+import org.apache.fineract.client.models.CreditAllocationData;
+import org.apache.fineract.client.models.CreditAllocationOrder;
+import org.apache.fineract.client.models.PaymentAllocationOrder;
+import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.client.models.PostLoanProductsResponse;
+import org.apache.fineract.client.models.PostLoansLoanIdResponse;
+import org.apache.fineract.client.models.PostLoansRequest;
+import org.apache.fineract.client.models.PostLoansResponse;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import
org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
+import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleProcessingType;
+import
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleType;
+import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
+import org.jetbrains.annotations.Nullable;
+import org.junit.jupiter.api.Disabled;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@Slf4j
+@ExtendWith(LoanTestLifecycleExtension.class)
+public class LoanChargebackWithCreditAllocationsIntegrationTests extends
BaseLoanIntegrationTest {
+
+ @Test
+ public void
createLoanWithCreditAllocationAndChargebackPenaltyFeeInterestAndPrincipal() {
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ // Create Loan Product
+ Long loanProductId =
createLoanProduct(charbackAllocation("PENALTY", "FEE", "INTEREST",
"PRINCIPAL"));
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId);
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January
2023");
+
+ // Add Charges
+ Long feeId = addCharge(loanId, false, 50, "15 January 2023");
+ Long penaltyId = addCharge(loanId, true, 20, "20 January 2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(313.0, 0, 50, 20, 383.0, false, "01 February
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 March
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 April
2023"), //
+ installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
+ );
+
+ // Update Business Date
+ updateBusinessDate("20 January 2023");
+
+ // Add Repayment
+ Long repaymentTransaction = addRepaymentForLoan(loanId, 383.0, "20
January 2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(313.0, 0, 50, 20, 0.0, true, "01 February
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 March
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 April
2023"), //
+ installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
+ );
+
+ // Add Chargeback
+ addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 20
penalty + 50 fee + 0 interest + 30
+ //
principal
+
+ verifyTransactions(loanId, //
+ transaction(1250.0, "Disbursement", "01 January 2023",
1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+ transaction(383.0, "Repayment", "20 January 2023", 937.0,
313.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
+ transaction(100.0, "Chargeback", "20 January 2023",
1037.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(343.0, 0, 50, 20, 30.0, false, "01 February
2023"), // TODO: we still need to add the
+
// fee and the penalty to the
+
// outstanding
+ installment(313.0, 0, 0, 0, 313.0, false, "01 March
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 April
2023"), //
+ installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
+ );
+ });
+ }
+
+ @Test
+ public void
createLoanWithCreditAllocationAndChargebackPenaltyFeeInterestAndPrincipalOnNPlusOneInstallment()
{
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ // Create Loan Product
+ Long loanProductId =
createLoanProduct(charbackAllocation("PENALTY", "FEE", "INTEREST",
"PRINCIPAL"));
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId);
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January
2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 February
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 March
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 April
2023"), //
+ installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
+ );
+
+ // Update Business Date + and make a full repayment for the first
installment
+ updateBusinessDate("20 January 2023");
+ addRepaymentForLoan(loanId, 313.0, "20 January 2023");
+
+ // Update Business Date + and make a full repayment for the second
installment
+ updateBusinessDate("20 February 2023");
+ addRepaymentForLoan(loanId, 313.0, "20 February 2023");
+
+ // Update Business Date + and make a full repayment for the third
installment
+ updateBusinessDate("20 March 2023");
+ addRepaymentForLoan(loanId, 313.0, "20 March 2023");
+
+ // Add some charges Update Business Date + and make a full
repayment for the fourth installment
+ updateBusinessDate("20 April 2023");
+ addCharge(loanId, false, 50, "20 April 2023");
+ addCharge(loanId, true, 20, "20 April 2023");
+ Long repaymentTransaction = addRepaymentForLoan(loanId, 381.0, "20
April 2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(313.0, 0, 0, 0, 0.0, true, "01 February
2023"), //
+ installment(313.0, 0, 0, 0, 0.0, true, "01 March 2023"), //
+ installment(313.0, 0, 0, 0, 0.0, true, "01 April 2023"), //
+ installment(311.0, 0, 50, 20, 0.0, true, "01 May 2023") //
+ );
+
+ // Let's move over the maturity date and chargeback some money
+ updateBusinessDate("02 May 2023");
+
+ // Add Chargeback, 20 penalty + 50 fee + 0 interest + 30 principal
+ addChargebackForLoan(loanId, repaymentTransaction, 100.0);
+
+ verifyTransactions(loanId, //
+ transaction(1250.0, "Disbursement", "01 January 2023",
1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+ transaction(313.0, "Repayment", "20 January 2023", 937.0,
313.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+ transaction(313.0, "Repayment", "20 February 2023", 624.0,
313.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+ transaction(313.0, "Repayment", "20 March 2023", 311.0,
313.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+ transaction(381.0, "Repayment", "20 April 2023", 0.0,
311.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
+ transaction(70.0, "Accrual", "20 April 2023", 0.0, 0.0,
0.0, 50.0, 20.0, 0.0, 0.0), //
+ transaction(100.0, "Chargeback", "02 May 2023", 100.0,
30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
+ );
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(313.0, 0, 0, 0, 0.0, true, "01 February
2023"), //
+ installment(313.0, 0, 0, 0, 0.0, true, "01 March 2023"), //
+ installment(313.0, 0, 0, 0, 0.0, true, "01 April 2023"), //
+ installment(311.0, 0, 50, 20, 0.0, true, "01 May 2023"), //
+ installment(30.0, 0, 0, 0, 30.0, false, "02 May 2023") //
TODO: fee and penalty must be added here
+ //
after chargeback
+ );
+ });
+ }
+
+ @Test
+ @Disabled
+ public void
createLoanWithCreditAllocationAndChargebackReverseReplayWithBackdatedPayment() {
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ // Create Loan Product
+ Long loanProductId =
createLoanProduct(charbackAllocation("PENALTY", "FEE", "INTEREST",
"PRINCIPAL"));
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId);
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January
2023");
+
+ // Add Charges
+ Long feeId = addCharge(loanId, false, 50, "15 January 2023");
+ Long penaltyId = addCharge(loanId, true, 20, "15 January 2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(313.0, 0, 50, 20, 383.0, false, "01 February
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 March
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 April
2023"), //
+ installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
+ );
+
+ // Update Business Date
+ updateBusinessDate("20 January 2023");
+
+ // Add Repayment
+ Long repaymentTransaction = addRepaymentForLoan(loanId, 383.0, "20
January 2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(313.0, 0, 50, 20, 0.0, true, "01 February
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 March
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 April
2023"), //
+ installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
+ );
+
+ updateBusinessDate("21 January 2023");
+
+ // Add Chargeback20 penalty + 50 fee + 0 interest + 30 principal
+ addChargebackForLoan(loanId, repaymentTransaction, 100.0);
+
+ verifyTransactions(loanId, //
+ transaction(1250.0, "Disbursement", "01 January 2023",
1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+ transaction(383.0, "Repayment", "20 January 2023", 937.0,
313.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
+ transaction(100.0, "Chargeback", "21 January 2023",
1037.0, 30.0, 0.0, 50.0, 20.0, 0.0, 0.0) //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(343.0, 0, 50, 20, 30.0, false, "01 February
2023"), // TODO: we still need to add the
+ // fee and the penalty to the
+ // outstanding
+ installment(313.0, 0, 0, 0, 313.0, false, "01 March
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 April
2023"), //
+ installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
+ );
+
+ // let's add a backdated repayment on 19th of January reverse
replaying the chargeback
+ addRepaymentForLoan(loanId, 200.0, "19 January 2023");
+
+ verifyTransactions(loanId, //
+ transaction(1250.0, "Disbursement", "01 January 2023",
1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+ transaction(200.0, "Repayment", "19 January 2023", 1120.0,
130.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
+ transaction(383.0, "Repayment", "20 January 2023", 737.0,
383.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+ transaction(100.0, "Chargeback", "21 January 2023", 937.0,
100.0, 0.0, 0.0, 0.0, 0.0, 0.0) //
+ );
+ });
+ }
+
+ @Test
+ public void
createLoanWithCreditAllocationAndChargebackPrincipalInterestFeePenalty() {
+ runAt("01 January 2023", () -> {
+ // Create Client
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ // Create Loan Product
+ Long loanProductId =
createLoanProduct(charbackAllocation("PRINCIPAL", "INTEREST", "FEE",
"PENALTY"));
+
+ // Apply and Approve Loan
+ Long loanId = applyAndApproveLoan(clientId, loanProductId);
+
+ // Disburse Loan
+ disburseLoan(loanId, BigDecimal.valueOf(1250.0), "01 January
2023");
+
+ // Add Charges
+ Long feeId = addCharge(loanId, false, 50, "15 January 2023");
+ Long penaltyId = addCharge(loanId, true, 20, "20 January 2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(313.0, 0, 50, 20, 383.0, false, "01 February
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 March
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 April
2023"), //
+ installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
+ );
+
+ // Update Business Date
+ updateBusinessDate("20 January 2023");
+
+ // Add Repayment
+ Long repaymentTransaction = addRepaymentForLoan(loanId, 383.0, "20
January 2023");
+
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(313.0, 0, 50, 20, 0.0, true, "01 February
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 March
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 April
2023"), //
+ installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
+ );
+
+ // Add Chargeback
+ addChargebackForLoan(loanId, repaymentTransaction, 100.0); // 100
principal, 0 interest, 0 fee 0 penalty
+
+ verifyTransactions(loanId, //
+ transaction(1250.0, "Disbursement", "01 January 2023",
1250.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0), //
+ transaction(383.0, "Repayment", "20 January 2023", 937.0,
313.0, 0.0, 50.0, 20.0, 0.0, 0.0), //
+ transaction(100.0, "Chargeback", "20 January 2023",
1037.0, 100.0, 0.0, 0.0, 0.0, 0.0, 0.0) //
+ );
+
+ // Verify Repayment Schedule
+ verifyRepaymentSchedule(loanId, //
+ installment(0, null, "01 January 2023"), //
+ installment(413.0, 0, 50, 20, 100.0, false, "01 February
2023"), // TODO: we still need to add the
+ // fee and the penalty to the
+ // outstanding
+ installment(313.0, 0, 0, 0, 313.0, false, "01 March
2023"), //
+ installment(313.0, 0, 0, 0, 313.0, false, "01 April
2023"), //
+ installment(311.0, 0, 0, 0, 311.0, false, "01 May 2023") //
+ );
+ });
+ }
+
+ @Nullable
+ private Long applyAndApproveLoan(Long clientId, Long loanProductId) {
+ PostLoansRequest applicationRequest = applyLoanRequest(clientId,
loanProductId, "01 January 2023", 1250.0, 4)//
+ .repaymentEvery(1)//
+ .loanTermFrequency(4)//
+ .repaymentFrequencyType(RepaymentFrequencyType.MONTHS)//
+ .loanTermFrequencyType(RepaymentFrequencyType.MONTHS)//
+
.transactionProcessingStrategyCode("advanced-payment-allocation-strategy");
+
+ PostLoansResponse postLoansResponse =
loanTransactionHelper.applyLoan(applicationRequest);
+
+ PostLoansLoanIdResponse approvedLoanResult =
loanTransactionHelper.approveLoan(postLoansResponse.getResourceId(),
+ approveLoanRequest(1250.0, "01 January 2023"));
+
+ Long loanId = approvedLoanResult.getLoanId();
+ return loanId;
+ }
+
+ public Long createLoanProduct(CreditAllocationData...
creditAllocationData) {
+ PostLoanProductsRequest postLoanProductsRequest =
loanProductWithAdvancedPaymentAllocationWith4Installments(creditAllocationData);
+ PostLoanProductsResponse loanProductResponse =
loanProductHelper.createLoanProduct(postLoanProductsRequest);
+ return loanProductResponse.getResourceId();
+ }
+
+ private PostLoanProductsRequest
loanProductWithAdvancedPaymentAllocationWith4Installments(
+ CreditAllocationData... creditAllocationData) {
+ return
createOnePeriod30DaysLongNoInterestPeriodicAccrualProduct().numberOfRepayments(4)//
+ .repaymentEvery(1)//
+
.repaymentFrequencyType(RepaymentFrequencyType.MONTHS.longValue())//
+ .loanScheduleType(LoanScheduleType.PROGRESSIVE.toString()) //
+
.loanScheduleProcessingType(LoanScheduleProcessingType.VERTICAL.toString()) //
+
.transactionProcessingStrategyCode("advanced-payment-allocation-strategy")
+ .paymentAllocation(List.of(createDefaultPaymentAllocation(),
createRepaymentPaymentAllocation()))
+ .creditAllocation(Arrays.asList(creditAllocationData));
+ }
+
+ private AdvancedPaymentData createDefaultPaymentAllocation() {
+ AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
+ advancedPaymentData.setTransactionType("DEFAULT");
+
advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
+
+ List<PaymentAllocationOrder> paymentAllocationOrders =
getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY,
+ PaymentAllocationType.PAST_DUE_FEE,
PaymentAllocationType.PAST_DUE_PRINCIPAL,
PaymentAllocationType.PAST_DUE_INTEREST,
+ PaymentAllocationType.DUE_PENALTY,
PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_PRINCIPAL,
+ PaymentAllocationType.DUE_INTEREST,
PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
+ PaymentAllocationType.IN_ADVANCE_PRINCIPAL,
PaymentAllocationType.IN_ADVANCE_INTEREST);
+
+ advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
+ return advancedPaymentData;
+ }
+
+ private AdvancedPaymentData createRepaymentPaymentAllocation() {
+ AdvancedPaymentData advancedPaymentData = new AdvancedPaymentData();
+ advancedPaymentData.setTransactionType("REPAYMENT");
+
advancedPaymentData.setFutureInstallmentAllocationRule("NEXT_INSTALLMENT");
+
+ List<PaymentAllocationOrder> paymentAllocationOrders =
getPaymentAllocationOrder(PaymentAllocationType.PAST_DUE_PENALTY,
+ PaymentAllocationType.PAST_DUE_FEE,
PaymentAllocationType.PAST_DUE_INTEREST,
PaymentAllocationType.PAST_DUE_PRINCIPAL,
+ PaymentAllocationType.DUE_PENALTY,
PaymentAllocationType.DUE_FEE, PaymentAllocationType.DUE_INTEREST,
+ PaymentAllocationType.DUE_PRINCIPAL,
PaymentAllocationType.IN_ADVANCE_PENALTY, PaymentAllocationType.IN_ADVANCE_FEE,
+ PaymentAllocationType.IN_ADVANCE_PRINCIPAL,
PaymentAllocationType.IN_ADVANCE_INTEREST);
+
+ advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
+ return advancedPaymentData;
+ }
+
+ private CreditAllocationData charbackAllocation(String... allocationRules)
{
+ CreditAllocationData creditAllocationData = new CreditAllocationData();
+ creditAllocationData.setTransactionType("CHARGEBACK");
+
creditAllocationData.setCreditAllocationOrder(createCreditAllocationOrders(allocationRules));
+ return creditAllocationData;
+ }
+
+ public List<CreditAllocationOrder> createCreditAllocationOrders(String...
allocationRule) {
+ AtomicInteger integer = new AtomicInteger(1);
+ return Arrays.stream(allocationRule).map(allocation -> {
+ CreditAllocationOrder creditAllocationOrder = new
CreditAllocationOrder();
+ creditAllocationOrder.setCreditAllocationRule(allocation);
+ creditAllocationOrder.setOrder(integer.getAndIncrement());
+ return creditAllocationOrder;
+ }).toList();
+ }
+
+ private List<PaymentAllocationOrder>
getPaymentAllocationOrder(PaymentAllocationType... paymentAllocationTypes) {
+ AtomicInteger integer = new AtomicInteger(1);
+ return Arrays.stream(paymentAllocationTypes).map(pat -> {
+ PaymentAllocationOrder paymentAllocationOrder = new
PaymentAllocationOrder();
+ paymentAllocationOrder.setPaymentAllocationRule(pat.name());
+ paymentAllocationOrder.setOrder(integer.getAndIncrement());
+ return paymentAllocationOrder;
+ }).toList();
+ }
+
+}
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
index 3d3de0628..f34191726 100644
---
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanTransactionHelper.java
@@ -744,6 +744,11 @@ public class LoanTransactionHelper extends IntegrationTest
{
return ok(fineract().loanTransactions.adjustLoanTransaction1(loanId,
transactionExternalId, request, "undo"));
}
+ public PostLoansLoanIdTransactionsResponse chargebackLoanTransaction(final
Long loanId, final Long transactionId,
+ final PostLoansLoanIdTransactionsTransactionIdRequest request) {
+ return ok(fineract().loanTransactions.adjustLoanTransaction(loanId,
transactionId, request, "chargeback"));
+ }
+
public PostLoansLoanIdTransactionsResponse chargebackLoanTransaction(final
String loanExternalId, final Long transactionId,
final PostLoansLoanIdTransactionsTransactionIdRequest request) {
return
ok(fineract().loanTransactions.adjustLoanTransaction2(loanExternalId,
transactionId, request, "chargeback"));