This is an automated email from the ASF dual-hosted git repository.
adamsaghy 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 379983541 [FINERACT-1968] Refund for active loan transaction
379983541 is described below
commit 379983541469573dfcb8e0b9335f19961998e041
Author: taskain7 <[email protected]>
AuthorDate: Fri Nov 10 00:39:03 2023 +0100
[FINERACT-1968] Refund for active loan transaction
---
.../domain/PaymentAllocationTransactionType.java | 5 +-
...dvancedPaymentScheduleTransactionProcessor.java | 167 +++++++++-
...tiveLoansWithAdvancedPaymentAllocationTest.java | 340 +++++++++++++++++++++
3 files changed, 504 insertions(+), 8 deletions(-)
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java
index 85b3d865d..014b04214 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanproduct/domain/PaymentAllocationTransactionType.java
@@ -37,8 +37,9 @@ public enum PaymentAllocationTransactionType {
GOODWILL_CREDIT(LoanTransactionType.GOODWILL_CREDIT, "Goodwill credit"), //
CHARGE_REFUND(LoanTransactionType.CHARGE_REFUND, "Charge refund"), //
CHARGE_ADJUSTMENT(LoanTransactionType.CHARGE_ADJUSTMENT, "Charge
adjustment"), //
- WAIVE_INTEREST(LoanTransactionType.WAIVE_INTEREST, "Waive interest"),
CHARGE_PAYMENT(LoanTransactionType.CHARGE_PAYMENT,
- "Charge payment");//
+ WAIVE_INTEREST(LoanTransactionType.WAIVE_INTEREST, "Waive interest"), //
+ CHARGE_PAYMENT(LoanTransactionType.CHARGE_PAYMENT, "Charge payment"), //
+ REFUND_FOR_ACTIVE_LOAN(LoanTransactionType.REFUND_FOR_ACTIVE_LOAN, "Refund
for active loan");
private final LoanTransactionType loanTransactionType;
private final String humanReadableName;
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
index aeca0d77d..709e14f2b 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/transactionprocessor/impl/AdvancedPaymentScheduleTransactionProcessor.java
@@ -25,6 +25,7 @@ import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
+import java.util.Optional;
import java.util.Set;
import java.util.stream.Collectors;
import lombok.AllArgsConstructor;
@@ -97,6 +98,12 @@ public class AdvancedPaymentScheduleTransactionProcessor
extends AbstractLoanRep
throw new NotImplementedException();
}
+ @Override
+ public Money handleRepaymentSchedule(List<LoanTransaction>
transactionsPostDisbursement, MonetaryCurrency currency,
+ List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> loanCharges) {
+ throw new NotImplementedException();
+ }
+
private void processSingleTransaction(LoanTransaction loanTransaction,
MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges,
ChangedTransactionDetail changedTransactionDetail) {
@@ -240,12 +247,6 @@ public class AdvancedPaymentScheduleTransactionProcessor
extends AbstractLoanRep
.addToPrincipal(disbursementTransaction.getTransactionDate(),
remainingAmount);
}
- @Override
- public Money handleRepaymentSchedule(List<LoanTransaction>
transactionsPostDisbursement, MonetaryCurrency currency,
- List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> loanCharges) {
- throw new NotImplementedException();
- }
-
private void handleRepayment(LoanTransaction loanTransaction,
MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges) {
if (loanTransaction.isRepaymentLikeType() ||
loanTransaction.isInterestWaiver() || loanTransaction.isRecoveryRepayment()) {
@@ -303,6 +304,39 @@ public class AdvancedPaymentScheduleTransactionProcessor
extends AbstractLoanRep
};
}
+ private Money unpayAllocation(PaymentAllocationType paymentAllocationType,
LoanRepaymentScheduleInstallment currentInstallment,
+ LoanTransaction loanTransaction, Money
transactionAmountUnprocessed,
+ LoanTransactionToRepaymentScheduleMapping
loanTransactionToRepaymentScheduleMapping, Balances balances) {
+ LocalDate transactionDate = loanTransaction.getTransactionDate();
+ Money zero = transactionAmountUnprocessed.zero();
+ return switch (paymentAllocationType.getAllocationType()) {
+ case PENALTY -> {
+ Money unpaidPenaltyPortion =
currentInstallment.unpayPenaltyChargesComponent(transactionDate,
transactionAmountUnprocessed);
+
balances.setAggregatedPenaltyChargesPortion(balances.getAggregatedPenaltyChargesPortion().minus(unpaidPenaltyPortion));
+
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero,
zero, unpaidPenaltyPortion);
+ yield unpaidPenaltyPortion;
+ }
+ case FEE -> {
+ Money unpaidFeePortion =
currentInstallment.unpayFeeChargesComponent(transactionDate,
transactionAmountUnprocessed);
+
balances.setAggregatedFeeChargesPortion(balances.getAggregatedFeeChargesPortion().minus(unpaidFeePortion));
+
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero, zero,
unpaidFeePortion, zero);
+ yield unpaidFeePortion;
+ }
+ case INTEREST -> {
+ Money unpaidInterestPortion =
currentInstallment.unpayInterestComponent(transactionDate,
transactionAmountUnprocessed);
+
balances.setAggregatedInterestPortion(balances.getAggregatedInterestPortion().minus(unpaidInterestPortion));
+
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping, zero,
unpaidInterestPortion, zero, zero);
+ yield unpaidInterestPortion;
+ }
+ case PRINCIPAL -> {
+ Money unpaidPrincipalPortion =
currentInstallment.unpayPrincipalComponent(transactionDate,
transactionAmountUnprocessed);
+
balances.setAggregatedPrincipalPortion(balances.getAggregatedPrincipalPortion().minus(unpaidPrincipalPortion));
+
addToTransactionMapping(loanTransactionToRepaymentScheduleMapping,
unpaidPrincipalPortion, zero, zero, zero);
+ yield unpaidPrincipalPortion;
+ }
+ };
+ }
+
private void
addToTransactionMapping(LoanTransactionToRepaymentScheduleMapping
loanTransactionToRepaymentScheduleMapping,
Money principalPortion, Money interestPortion, Money feePortion,
Money penaltyPortion) {
BigDecimal aggregatedPenalty = ObjectUtils
@@ -397,6 +431,127 @@ public class AdvancedPaymentScheduleTransactionProcessor
extends AbstractLoanRep
}
}
+ @Override
+ protected void handleRefund(LoanTransaction loanTransaction,
MonetaryCurrency currency,
+ List<LoanRepaymentScheduleInstallment> installments,
Set<LoanCharge> charges) {
+ Money zero = Money.zero(currency);
+ List<LoanTransactionToRepaymentScheduleMapping> transactionMappings =
new ArrayList<>();
+ Money transactionAmountUnprocessed =
loanTransaction.getAmount(currency);
+
+ List<LoanPaymentAllocationRule> paymentAllocationRules =
loanTransaction.getLoan().getPaymentAllocationRules();
+ LoanPaymentAllocationRule defaultPaymentAllocationRule =
paymentAllocationRules.stream()
+ .filter(e ->
PaymentAllocationTransactionType.DEFAULT.equals(e.getTransactionType())).findFirst().orElseThrow();
+ Optional<LoanPaymentAllocationRule> paymentAllocationRuleOptional =
paymentAllocationRules.stream()
+ .filter(e ->
loanTransaction.getTypeOf().equals(e.getTransactionType().getLoanTransactionType())).findFirst();
+ Balances balances = new Balances(zero, zero, zero, zero);
+ if (paymentAllocationRuleOptional.isPresent()) {
+ LoanPaymentAllocationRule paymentAllocationRule =
paymentAllocationRuleOptional.get();
+ List<PaymentAllocationType> paymentAllocationTypes =
paymentAllocationRule.getAllocationTypes();
+ FutureInstallmentAllocationRule futureInstallmentAllocationRule =
paymentAllocationRule.getFutureInstallmentAllocationRule();
+ for (PaymentAllocationType paymentAllocationType :
paymentAllocationTypes) {
+ transactionAmountUnprocessed =
refundTransaction(loanTransaction, currency, installments, zero,
transactionMappings,
+ transactionAmountUnprocessed,
futureInstallmentAllocationRule, balances, paymentAllocationType);
+ if (!transactionAmountUnprocessed.isGreaterThanZero()) {
+ break;
+ }
+ }
+ } else {
+ // if the allocation rule is not defined then the reverse order of
the default allocation rule will be used
+ LoanPaymentAllocationRule paymentAllocationRule =
defaultPaymentAllocationRule;
+ List<PaymentAllocationType> paymentAllocationTypes =
paymentAllocationRule.getAllocationTypes();
+ FutureInstallmentAllocationRule futureInstallmentAllocationRule =
FutureInstallmentAllocationRule.LAST_INSTALLMENT;
+ for (int i = paymentAllocationTypes.size() - 1; i >= 0; i--) {
+ PaymentAllocationType paymentAllocationType =
paymentAllocationTypes.get(i);
+ transactionAmountUnprocessed =
refundTransaction(loanTransaction, currency, installments, zero,
transactionMappings,
+ transactionAmountUnprocessed,
futureInstallmentAllocationRule, balances, paymentAllocationType);
+ if (!transactionAmountUnprocessed.isGreaterThanZero()) {
+ break;
+ }
+ }
+ }
+
loanTransaction.updateComponents(balances.getAggregatedPrincipalPortion(),
balances.getAggregatedInterestPortion(),
+ balances.getAggregatedFeeChargesPortion(),
balances.getAggregatedPenaltyChargesPortion());
+
loanTransaction.updateLoanTransactionToRepaymentScheduleMappings(transactionMappings);
+ }
+
+ private Money refundTransaction(LoanTransaction loanTransaction,
MonetaryCurrency currency,
+ List<LoanRepaymentScheduleInstallment> installments, Money zero,
+ List<LoanTransactionToRepaymentScheduleMapping>
transactionMappings, Money transactionAmountUnprocessed,
+ FutureInstallmentAllocationRule futureInstallmentAllocationRule,
Balances balances,
+ PaymentAllocationType paymentAllocationType) {
+ LoanRepaymentScheduleInstallment currentInstallment = null;
+ Money refundedPortion = zero;
+ do {
+ switch (paymentAllocationType.getDueType()) {
+ case PAST_DUE -> {
+ currentInstallment =
installments.stream().filter(installment ->
installment.getTotalPaid(currency).isGreaterThan(zero))
+ .filter(e ->
loanTransaction.isAfter(e.getDueDate()))
+
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
+ if (currentInstallment != null) {
+ LoanTransactionToRepaymentScheduleMapping
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
+ transactionMappings, loanTransaction,
currentInstallment, currency);
+ refundedPortion =
unpayAllocation(paymentAllocationType, currentInstallment, loanTransaction,
+ transactionAmountUnprocessed,
loanTransactionToRepaymentScheduleMapping, balances);
+ transactionAmountUnprocessed =
transactionAmountUnprocessed.minus(refundedPortion);
+ }
+ }
+ case DUE -> {
+ currentInstallment =
installments.stream().filter(installment ->
installment.getTotalPaid(currency).isGreaterThan(zero))
+ .filter(installment ->
loanTransaction.isOn(installment.getDueDate()))
+
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).orElse(null);
+ if (currentInstallment != null) {
+ LoanTransactionToRepaymentScheduleMapping
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
+ transactionMappings, loanTransaction,
currentInstallment, currency);
+ refundedPortion =
unpayAllocation(paymentAllocationType, currentInstallment, loanTransaction,
+ transactionAmountUnprocessed,
loanTransactionToRepaymentScheduleMapping, balances);
+ transactionAmountUnprocessed =
transactionAmountUnprocessed.minus(refundedPortion);
+ }
+ }
+ case IN_ADVANCE -> {
+ List<LoanRepaymentScheduleInstallment> currentInstallments
= new ArrayList<>();
+ if
(FutureInstallmentAllocationRule.REAMORTIZATION.equals(futureInstallmentAllocationRule))
{
+ currentInstallments = installments.stream()
+ .filter(installment ->
installment.getTotalPaid(currency).isGreaterThan(zero))
+ .filter(e ->
loanTransaction.isBefore(e.getDueDate())).toList();
+ } else if
(FutureInstallmentAllocationRule.NEXT_INSTALLMENT.equals(futureInstallmentAllocationRule))
{
+ currentInstallments = installments.stream()
+ .filter(installment ->
installment.getTotalPaid(currency).isGreaterThan(zero))
+ .filter(e ->
loanTransaction.isBefore(e.getDueDate()))
+
.min(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
+ } else if
(FutureInstallmentAllocationRule.LAST_INSTALLMENT.equals(futureInstallmentAllocationRule))
{
+ currentInstallments = installments.stream()
+ .filter(installment ->
installment.getTotalPaid(currency).isGreaterThan(zero))
+ .filter(e ->
loanTransaction.isBefore(e.getDueDate()))
+
.max(Comparator.comparing(LoanRepaymentScheduleInstallment::getInstallmentNumber)).stream().toList();
+ }
+ int numberOfInstallments = currentInstallments.size();
+ refundedPortion = zero;
+ if (numberOfInstallments > 0) {
+ Money evenPortion =
transactionAmountUnprocessed.dividedBy(numberOfInstallments,
MoneyHelper.getRoundingMode());
+ Money balanceAdjustment =
transactionAmountUnprocessed.minus(evenPortion.multipliedBy(numberOfInstallments));
+ for (LoanRepaymentScheduleInstallment
internalCurrentInstallment : currentInstallments) {
+ currentInstallment = internalCurrentInstallment;
+ if
(internalCurrentInstallment.equals(currentInstallments.get(numberOfInstallments
- 1))) {
+ evenPortion =
evenPortion.add(balanceAdjustment);
+ }
+ LoanTransactionToRepaymentScheduleMapping
loanTransactionToRepaymentScheduleMapping = getTransactionMapping(
+ transactionMappings, loanTransaction,
currentInstallment, currency);
+ Money internalUnpaidPortion =
unpayAllocation(paymentAllocationType, currentInstallment, loanTransaction,
+ evenPortion,
loanTransactionToRepaymentScheduleMapping, balances);
+ if (internalUnpaidPortion.isGreaterThanZero()) {
+ refundedPortion = internalUnpaidPortion;
+ }
+ transactionAmountUnprocessed =
transactionAmountUnprocessed.minus(internalUnpaidPortion);
+ }
+ } else {
+ currentInstallment = null;
+ }
+ }
+ }
+ } while (currentInstallment != null &&
transactionAmountUnprocessed.isGreaterThanZero() &&
refundedPortion.isGreaterThanZero());
+ return transactionAmountUnprocessed;
+ }
+
private void processTransaction(LoanTransaction loanTransaction,
MonetaryCurrency currency,
List<LoanRepaymentScheduleInstallment> installments, Money
transactionAmountUnprocessed, Set<LoanCharge> charges) {
Money zero = Money.zero(currency);
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/RefundForActiveLoansWithAdvancedPaymentAllocationTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/RefundForActiveLoansWithAdvancedPaymentAllocationTest.java
new file mode 100644
index 000000000..cbe683a6e
--- /dev/null
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/RefundForActiveLoansWithAdvancedPaymentAllocationTest.java
@@ -0,0 +1,340 @@
+/**
+ * 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 static org.junit.jupiter.api.Assertions.assertEquals;
+
+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.time.LocalDate;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeFormatterBuilder;
+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.BusinessDateRequest;
+import org.apache.fineract.client.models.GetLoansLoanIdRepaymentPeriod;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.PaymentAllocationOrder;
+import org.apache.fineract.client.models.PostClientsResponse;
+import org.apache.fineract.client.models.PostLoansLoanIdRequest;
+import org.apache.fineract.client.models.PostLoansRequest;
+import org.apache.fineract.client.models.PostLoansResponse;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import org.apache.fineract.integrationtests.common.BusinessDateHelper;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.GlobalConfigurationHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.accounting.Account;
+import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
+import org.apache.fineract.integrationtests.common.charges.ChargesHelper;
+import
org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
+import
org.apache.fineract.integrationtests.common.loans.LoanTestLifecycleExtension;
+import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
+import
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.AdvancedPaymentScheduleTransactionProcessor;
+import org.apache.fineract.portfolio.loanproduct.domain.PaymentAllocationType;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+
+@ExtendWith(LoanTestLifecycleExtension.class)
+@Slf4j
+public class RefundForActiveLoansWithAdvancedPaymentAllocationTest {
+
+ private static final String DATETIME_PATTERN = "dd MMMM yyyy";
+ private static final DateTimeFormatter DATE_FORMATTER = new
DateTimeFormatterBuilder().appendPattern(DATETIME_PATTERN).toFormatter();
+ private static RequestSpecification requestSpec;
+ private static ResponseSpecification responseSpec;
+ private static LoanTransactionHelper loanTransactionHelper;
+ private static PostClientsResponse client;
+ private static BusinessDateHelper businessDateHelper;
+ private static AccountHelper accountHelper;
+
+ @BeforeAll
+ public static void setup() {
+ Utils.initializeRESTAssured();
+ ClientHelper clientHelper = new ClientHelper(requestSpec,
responseSpec);
+ 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);
+ client =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest());
+ businessDateHelper = new BusinessDateHelper();
+ accountHelper = new AccountHelper(requestSpec, responseSpec);
+ }
+
+ @Test
+ public void refundForActiveLoanWithDefaultPaymentAllocation() {
+ try {
+
+ GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec,
responseSpec, Boolean.TRUE);
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
+ .date("2023.02.15").dateFormat("yyyy.MM.dd").locale("en"));
+
+ final Account assetAccount = accountHelper.createAssetAccount();
+ final Account incomeAccount = accountHelper.createIncomeAccount();
+ final Account expenseAccount =
accountHelper.createExpenseAccount();
+ final Account overpaymentAccount =
accountHelper.createLiabilityAccount();
+
+ Integer loanProductId = createLoanProduct("1000", "30", "4",
assetAccount, incomeAccount, expenseAccount, overpaymentAccount);
+
+ final PostLoansResponse loanResponse =
applyForLoanApplication(client.getClientId(), loanProductId, 1000L, 90, 30, 3,
0,
+ "01 January 2023", "01 January 2023");
+
+ int loanId = loanResponse.getLoanId().intValue();
+
+ loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
+ new
PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000)).dateFormat(DATETIME_PATTERN)
+ .approvedOnDate("01 January 2023").locale("en"));
+
+ loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().actualDisbursementDate("01
January 2023").dateFormat(DATETIME_PATTERN)
+
.transactionAmount(BigDecimal.valueOf(1000.00)).locale("en"));
+
+ final float feePortion = 50.0f;
+ final float penaltyPortion = 100.0f;
+
+ Integer fee = ChargesHelper.createCharges(requestSpec,
responseSpec, ChargesHelper
+
.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
String.valueOf(feePortion), false));
+
+ Integer penalty = ChargesHelper.createCharges(requestSpec,
responseSpec, ChargesHelper
+
.getLoanSpecifiedDueDateJSON(ChargesHelper.CHARGE_CALCULATION_TYPE_FLAT,
String.valueOf(penaltyPortion), true));
+
+ final String firstInstallmentChargeAddedDate =
DATE_FORMATTER.format(LocalDate.of(2023, 1, 3));
+ loanTransactionHelper.addChargesForLoan(loanId,
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(
+ String.valueOf(fee), firstInstallmentChargeAddedDate,
String.valueOf(feePortion)));
+
+ loanTransactionHelper.addChargesForLoan(loanId,
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(
+ String.valueOf(penalty), firstInstallmentChargeAddedDate,
String.valueOf(penaltyPortion)));
+
+ final String secondInstallmentChargeAddedDate =
DATE_FORMATTER.format(LocalDate.of(2023, 2, 3));
+ loanTransactionHelper.addChargesForLoan(loanId,
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(
+ String.valueOf(fee), secondInstallmentChargeAddedDate,
String.valueOf(feePortion)));
+
+ loanTransactionHelper.addChargesForLoan(loanId,
LoanTransactionHelper.getSpecifiedDueDateChargesForLoanAsJSON(
+ String.valueOf(penalty), secondInstallmentChargeAddedDate,
String.valueOf(penaltyPortion)));
+
+ GetLoansLoanIdResponse loanDetails =
loanTransactionHelper.getLoanDetails((long) loanId);
+
+ GetLoansLoanIdRepaymentPeriod firstRepaymentInstallment =
loanDetails.getRepaymentSchedule().getPeriods().get(2);
+ GetLoansLoanIdRepaymentPeriod secondRepaymentInstallment =
loanDetails.getRepaymentSchedule().getPeriods().get(3);
+ GetLoansLoanIdRepaymentPeriod thirdRepaymentInstallment =
loanDetails.getRepaymentSchedule().getPeriods().get(4);
+
+ assertEquals(5,
loanDetails.getRepaymentSchedule().getPeriods().size());
+ assertEquals(feePortion,
firstRepaymentInstallment.getFeeChargesDue());
+ assertEquals(feePortion,
firstRepaymentInstallment.getFeeChargesOutstanding());
+ assertEquals(penaltyPortion,
firstRepaymentInstallment.getPenaltyChargesDue());
+ assertEquals(penaltyPortion,
firstRepaymentInstallment.getPenaltyChargesOutstanding());
+ assertEquals(250.0f, firstRepaymentInstallment.getPrincipalDue());
+ assertEquals(250.0f,
firstRepaymentInstallment.getPrincipalOutstanding());
+ assertEquals(400.0f,
firstRepaymentInstallment.getTotalDueForPeriod());
+ assertEquals(400.0f,
firstRepaymentInstallment.getTotalOutstandingForPeriod());
+ assertEquals(LocalDate.of(2023, 1, 31),
firstRepaymentInstallment.getDueDate());
+
+ assertEquals(feePortion,
secondRepaymentInstallment.getFeeChargesDue());
+ assertEquals(feePortion,
secondRepaymentInstallment.getFeeChargesOutstanding());
+ assertEquals(penaltyPortion,
secondRepaymentInstallment.getPenaltyChargesDue());
+ assertEquals(penaltyPortion,
secondRepaymentInstallment.getPenaltyChargesOutstanding());
+ assertEquals(400.0f,
secondRepaymentInstallment.getTotalDueForPeriod());
+ assertEquals(400.0f,
secondRepaymentInstallment.getTotalOutstandingForPeriod());
+ assertEquals(LocalDate.of(2023, 3, 2),
secondRepaymentInstallment.getDueDate());
+
+ assertEquals(0.0f, thirdRepaymentInstallment.getFeeChargesDue());
+ assertEquals(0.0f,
thirdRepaymentInstallment.getFeeChargesOutstanding());
+ assertEquals(0.0f,
thirdRepaymentInstallment.getPenaltyChargesDue());
+ assertEquals(0.0f,
thirdRepaymentInstallment.getPenaltyChargesOutstanding());
+ assertEquals(250.0f,
thirdRepaymentInstallment.getTotalDueForPeriod());
+ assertEquals(250.0f,
thirdRepaymentInstallment.getTotalOutstandingForPeriod());
+ assertEquals(LocalDate.of(2023, 4, 1),
thirdRepaymentInstallment.getDueDate());
+
+ businessDateHelper.updateBusinessDate(new
BusinessDateRequest().type(BusinessDateType.BUSINESS_DATE.getName())
+ .date("2023.03.01").dateFormat("yyyy.MM.dd").locale("en"));
+ loanTransactionHelper.makeRepayment("01 March 2023", 810.0f,
loanId);
+
+ loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
+
+ firstRepaymentInstallment =
loanDetails.getRepaymentSchedule().getPeriods().get(2);
+ secondRepaymentInstallment =
loanDetails.getRepaymentSchedule().getPeriods().get(3);
+ thirdRepaymentInstallment =
loanDetails.getRepaymentSchedule().getPeriods().get(4);
+
+ assertEquals(5,
loanDetails.getRepaymentSchedule().getPeriods().size());
+ assertEquals(feePortion,
firstRepaymentInstallment.getFeeChargesDue());
+ assertEquals(0.0f,
firstRepaymentInstallment.getFeeChargesOutstanding());
+ assertEquals(penaltyPortion,
firstRepaymentInstallment.getPenaltyChargesDue());
+ assertEquals(0.0f,
firstRepaymentInstallment.getPenaltyChargesOutstanding());
+ assertEquals(250.0f, firstRepaymentInstallment.getPrincipalDue());
+ assertEquals(0.0f,
firstRepaymentInstallment.getPrincipalOutstanding());
+ assertEquals(400.0f,
firstRepaymentInstallment.getTotalDueForPeriod());
+ assertEquals(0.0f,
firstRepaymentInstallment.getTotalOutstandingForPeriod());
+ assertEquals(LocalDate.of(2023, 1, 31),
firstRepaymentInstallment.getDueDate());
+
+ assertEquals(feePortion,
secondRepaymentInstallment.getFeeChargesDue());
+ assertEquals(0.0f,
secondRepaymentInstallment.getFeeChargesOutstanding());
+ assertEquals(penaltyPortion,
secondRepaymentInstallment.getPenaltyChargesDue());
+ assertEquals(0.0f,
secondRepaymentInstallment.getPenaltyChargesOutstanding());
+ assertEquals(400.0f,
secondRepaymentInstallment.getTotalDueForPeriod());
+ assertEquals(0.0f,
secondRepaymentInstallment.getTotalOutstandingForPeriod());
+ assertEquals(LocalDate.of(2023, 3, 2),
secondRepaymentInstallment.getDueDate());
+
+ assertEquals(0.0f, thirdRepaymentInstallment.getFeeChargesDue());
+ assertEquals(0.0f,
thirdRepaymentInstallment.getFeeChargesOutstanding());
+ assertEquals(0.0f,
thirdRepaymentInstallment.getPenaltyChargesDue());
+ assertEquals(0.0f,
thirdRepaymentInstallment.getPenaltyChargesOutstanding());
+ assertEquals(250.0f,
thirdRepaymentInstallment.getTotalDueForPeriod());
+ assertEquals(240.0f,
thirdRepaymentInstallment.getTotalOutstandingForPeriod());
+ assertEquals(LocalDate.of(2023, 4, 1),
thirdRepaymentInstallment.getDueDate());
+
+ loanTransactionHelper.makeRefundByCash("01 March 2023", 15.0f,
loanId);
+
+ loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
+
+ firstRepaymentInstallment =
loanDetails.getRepaymentSchedule().getPeriods().get(2);
+ secondRepaymentInstallment =
loanDetails.getRepaymentSchedule().getPeriods().get(3);
+ thirdRepaymentInstallment =
loanDetails.getRepaymentSchedule().getPeriods().get(4);
+
+ assertEquals(5,
loanDetails.getRepaymentSchedule().getPeriods().size());
+ assertEquals(feePortion,
firstRepaymentInstallment.getFeeChargesDue());
+ assertEquals(0.0f,
firstRepaymentInstallment.getFeeChargesOutstanding());
+ assertEquals(penaltyPortion,
firstRepaymentInstallment.getPenaltyChargesDue());
+ assertEquals(0.0f,
firstRepaymentInstallment.getPenaltyChargesOutstanding());
+ assertEquals(250.0f, firstRepaymentInstallment.getPrincipalDue());
+ assertEquals(0.0f,
firstRepaymentInstallment.getPrincipalOutstanding());
+ assertEquals(400.0f,
firstRepaymentInstallment.getTotalDueForPeriod());
+ assertEquals(0.0f,
firstRepaymentInstallment.getTotalOutstandingForPeriod());
+ assertEquals(LocalDate.of(2023, 1, 31),
firstRepaymentInstallment.getDueDate());
+
+ assertEquals(feePortion,
secondRepaymentInstallment.getFeeChargesDue());
+ assertEquals(0.0f,
secondRepaymentInstallment.getFeeChargesOutstanding());
+ assertEquals(penaltyPortion,
secondRepaymentInstallment.getPenaltyChargesDue());
+ assertEquals(0.0f,
secondRepaymentInstallment.getPenaltyChargesOutstanding());
+ assertEquals(250.0f, secondRepaymentInstallment.getPrincipalDue());
+ assertEquals(5.0f,
secondRepaymentInstallment.getPrincipalOutstanding());
+ assertEquals(400.0f,
secondRepaymentInstallment.getTotalDueForPeriod());
+ assertEquals(5.0f,
secondRepaymentInstallment.getTotalOutstandingForPeriod());
+ assertEquals(LocalDate.of(2023, 3, 2),
secondRepaymentInstallment.getDueDate());
+
+ assertEquals(0.0f, thirdRepaymentInstallment.getFeeChargesDue());
+ assertEquals(0.0f,
thirdRepaymentInstallment.getFeeChargesOutstanding());
+ assertEquals(0.0f,
thirdRepaymentInstallment.getPenaltyChargesDue());
+ assertEquals(0.0f,
thirdRepaymentInstallment.getPenaltyChargesOutstanding());
+ assertEquals(250.0f,
thirdRepaymentInstallment.getTotalDueForPeriod());
+ assertEquals(250.0f,
thirdRepaymentInstallment.getTotalOutstandingForPeriod());
+ assertEquals(LocalDate.of(2023, 4, 1),
thirdRepaymentInstallment.getDueDate());
+
+ loanTransactionHelper.makeRefundByCash("01 March 2023", 265.0f,
loanId);
+
+ loanDetails = loanTransactionHelper.getLoanDetails((long) loanId);
+
+ firstRepaymentInstallment =
loanDetails.getRepaymentSchedule().getPeriods().get(2);
+ secondRepaymentInstallment =
loanDetails.getRepaymentSchedule().getPeriods().get(3);
+ thirdRepaymentInstallment =
loanDetails.getRepaymentSchedule().getPeriods().get(4);
+
+ assertEquals(5,
loanDetails.getRepaymentSchedule().getPeriods().size());
+ assertEquals(feePortion,
firstRepaymentInstallment.getFeeChargesDue());
+ assertEquals(0.0f,
firstRepaymentInstallment.getFeeChargesOutstanding());
+ assertEquals(penaltyPortion,
firstRepaymentInstallment.getPenaltyChargesDue());
+ assertEquals(0.0f,
firstRepaymentInstallment.getPenaltyChargesOutstanding());
+ assertEquals(250.0f, firstRepaymentInstallment.getPrincipalDue());
+ assertEquals(0.0f,
firstRepaymentInstallment.getPrincipalOutstanding());
+ assertEquals(400.0f,
firstRepaymentInstallment.getTotalDueForPeriod());
+ assertEquals(0.0f,
firstRepaymentInstallment.getTotalOutstandingForPeriod());
+ assertEquals(LocalDate.of(2023, 1, 31),
firstRepaymentInstallment.getDueDate());
+
+ assertEquals(feePortion,
secondRepaymentInstallment.getFeeChargesDue());
+ assertEquals(20.0f,
secondRepaymentInstallment.getFeeChargesOutstanding());
+ assertEquals(penaltyPortion,
secondRepaymentInstallment.getPenaltyChargesDue());
+ assertEquals(0.0f,
secondRepaymentInstallment.getPenaltyChargesOutstanding());
+ assertEquals(250.0f, secondRepaymentInstallment.getPrincipalDue());
+ assertEquals(250.0f,
secondRepaymentInstallment.getPrincipalOutstanding());
+ assertEquals(400.0f,
secondRepaymentInstallment.getTotalDueForPeriod());
+ assertEquals(270.0f,
secondRepaymentInstallment.getTotalOutstandingForPeriod());
+ assertEquals(LocalDate.of(2023, 3, 2),
secondRepaymentInstallment.getDueDate());
+
+ assertEquals(0.0f, thirdRepaymentInstallment.getFeeChargesDue());
+ assertEquals(0.0f,
thirdRepaymentInstallment.getFeeChargesOutstanding());
+ assertEquals(0.0f,
thirdRepaymentInstallment.getPenaltyChargesDue());
+ assertEquals(0.0f,
thirdRepaymentInstallment.getPenaltyChargesOutstanding());
+ assertEquals(250.0f,
thirdRepaymentInstallment.getTotalDueForPeriod());
+ assertEquals(250.0f,
thirdRepaymentInstallment.getTotalOutstandingForPeriod());
+ assertEquals(LocalDate.of(2023, 4, 1),
thirdRepaymentInstallment.getDueDate());
+ } finally {
+ GlobalConfigurationHelper.updateIsBusinessDateEnabled(requestSpec,
responseSpec, Boolean.FALSE);
+ }
+ }
+
+ private Integer createLoanProduct(final String principal, final String
repaymentAfterEvery, final String numberOfRepayments,
+ final Account... accounts) {
+ AdvancedPaymentData defaultAllocation =
createDefaultPaymentAllocation();
+ log.info("------------------------------CREATING NEW LOAN PRODUCT
---------------------------------------");
+ final String loanProductJSON = new
LoanProductTestBuilder().withMinPrincipal(principal).withPrincipal(principal)
+
.withRepaymentTypeAsDays().withRepaymentAfterEvery(repaymentAfterEvery).withNumberOfRepayments(numberOfRepayments)
+ .withEnableDownPayment(true, "25",
true).withinterestRatePerPeriod("0").withInterestRateFrequencyTypeAsMonths()
+
.withRepaymentStrategy(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
+
.withAmortizationTypeAsEqualPrincipalPayment().withInterestTypeAsFlat().withAccountingRulePeriodicAccrual(accounts)
+
.addAdvancedPaymentAllocation(defaultAllocation).withDaysInMonth("30").withDaysInYear("365").withMoratorium("0",
"0")
+ .build(null);
+ return loanTransactionHelper.getLoanProductId(loanProductJSON);
+ }
+
+ private static 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_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_INTEREST,
PaymentAllocationType.IN_ADVANCE_PRINCIPAL);
+
+ advancedPaymentData.setPaymentAllocationOrder(paymentAllocationOrders);
+ return advancedPaymentData;
+ }
+
+ private static 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();
+ }
+
+ private static PostLoansResponse applyForLoanApplication(final Long
clientId, final Integer loanProductId, final Long principal,
+ final int loanTermFrequency, final int repaymentAfterEvery, final
int numberOfRepayments, final int interestRate,
+ final String expectedDisbursementDate, final String
submittedOnDate) {
+ log.info("--------------------------------APPLYING FOR LOAN
APPLICATION--------------------------------");
+ return loanTransactionHelper.applyLoan(new
PostLoansRequest().clientId(clientId).productId(loanProductId.longValue())
+
.expectedDisbursementDate(expectedDisbursementDate).dateFormat(DATETIME_PATTERN)
+
.transactionProcessingStrategyCode(AdvancedPaymentScheduleTransactionProcessor.ADVANCED_PAYMENT_ALLOCATION_STRATEGY)
+
.locale("en").submittedOnDate(submittedOnDate).amortizationType(1).interestRatePerPeriod(interestRate)
+
.interestCalculationPeriodType(1).interestType(0).repaymentFrequencyType(0).repaymentEvery(repaymentAfterEvery)
+
.repaymentFrequencyType(0).numberOfRepayments(numberOfRepayments).loanTermFrequency(loanTermFrequency)
+
.loanTermFrequencyType(0).principal(BigDecimal.valueOf(principal)).loanType("individual"));
+ }
+}