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 5ebd3429c FINERACT-2107: Loan transaction Interest Refund GL Entries
5ebd3429c is described below
commit 5ebd3429c6ca781f2af8d082b950022730e93ef7
Author: Jose Alberto Hernandez <[email protected]>
AuthorDate: Tue Aug 13 03:54:08 2024 -0600
FINERACT-2107: Loan transaction Interest Refund GL Entries
---
.../loanaccount/data/LoanRefundRequestData.java | 39 ++++
.../loanaccount/data/LoanTransactionEnumData.java | 2 +
.../domain/LoanAccountDomainService.java | 3 +
.../loanaccount/domain/LoanTransaction.java | 7 +
.../AccrualBasedAccountingProcessorForLoan.java | 8 +-
.../api/InternalLoanInformationApiResource.java | 25 +++
.../domain/LoanAccountDomainServiceJpa.java | 25 ++-
.../integrationtests/BaseLoanIntegrationTest.java | 2 +
.../LoanRefundTransactionTest.java | 245 +++++++++++++++++++++
.../common/loans/LoanTransactionHelper.java | 8 +
10 files changed, 359 insertions(+), 5 deletions(-)
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanRefundRequestData.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanRefundRequestData.java
new file mode 100644
index 000000000..22daadc5c
--- /dev/null
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanRefundRequestData.java
@@ -0,0 +1,39 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanaccount.data;
+
+import java.math.BigDecimal;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class LoanRefundRequestData {
+
+ private final BigDecimal principal;
+ private final BigDecimal interest;
+ private final BigDecimal feeCharges;
+ private final BigDecimal penaltyCharges;
+ private final BigDecimal overpayment;
+
+ public BigDecimal getTotalAmount() {
+ return principal.add(interest).add(feeCharges).add(penaltyCharges);
+ }
+
+}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
index 78497b620..b3e34c744 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionEnumData.java
@@ -60,6 +60,7 @@ public class LoanTransactionEnumData {
private final boolean reAge;
private final boolean reAmortize;
private final boolean accrualActivity;
+ private final boolean interestRefund;
public LoanTransactionEnumData(final Long id, final String code, final
String value) {
this.id = id;
@@ -94,6 +95,7 @@ public class LoanTransactionEnumData {
this.accrualActivity = Long.valueOf(32).equals(this.id);
this.reAge =
Long.valueOf(LoanTransactionType.REAGE.getValue()).equals(this.id);
this.reAmortize =
Long.valueOf(LoanTransactionType.REAMORTIZE.getValue()).equals(this.id);
+ this.interestRefund =
Long.valueOf(LoanTransactionType.INTEREST_REFUND.getValue()).equals(this.id);
}
public boolean isRepaymentType() {
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java
index 51d13848b..965ccf225 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainService.java
@@ -27,6 +27,7 @@ import
org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import
org.apache.fineract.portfolio.delinquency.validator.LoanDelinquencyActionData;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
+import org.apache.fineract.portfolio.loanaccount.data.LoanRefundRequestData;
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
public interface LoanAccountDomainService {
@@ -93,4 +94,6 @@ public interface LoanAccountDomainService {
LoanTransaction creditBalanceRefund(Loan loan, LocalDate transactionDate,
BigDecimal transactionAmount, String noteText,
ExternalId externalId, PaymentDetail paymentDetail);
+ LoanTransaction applyInterestRefund(Loan loan, LoanRefundRequestData
loanRefundRequest);
+
}
diff --git
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
index 26e7dfddd..f0a8fde00 100644
---
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
+++
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanTransaction.java
@@ -323,6 +323,13 @@ public class LoanTransaction extends
AbstractAuditableWithUTCDateTimeCustom<Long
externalId);
}
+ public static LoanTransaction interestRefund(final Loan loan, final Office
office, final BigDecimal amount, final BigDecimal principal,
+ final BigDecimal interest, final BigDecimal feeCharges, final
BigDecimal penaltyCharges, final PaymentDetail paymentDetail,
+ final LocalDate refundDate, final ExternalId externalId) {
+ return new LoanTransaction(loan, office,
LoanTransactionType.INTEREST_REFUND.getValue(), refundDate, amount, principal,
interest,
+ feeCharges, penaltyCharges, amount, false, paymentDetail,
externalId);
+ }
+
public static boolean transactionAmountsMatch(final MonetaryCurrency
currency, final LoanTransaction loanTransaction,
final LoanTransaction newLoanTransaction) {
return
loanTransaction.getAmount(currency).isEqualTo(newLoanTransaction.getAmount(currency))
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 82686cf78..0d7e4902a 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
@@ -111,13 +111,15 @@ public class AccrualBasedAccountingProcessorForLoan
implements AccountingProcess
createJournalEntriesForChargeOff(loanDTO, loanTransactionDTO,
office);
}
// Logic for Interest Payment Waiver
- else if
(loanTransactionDTO.getTransactionType().isInterestPaymentWaiver()) {
- createJournalEntriesForInterestPaymentWaiver(loanDTO,
loanTransactionDTO, office);
+ else if
(loanTransactionDTO.getTransactionType().isInterestPaymentWaiver()
+ ||
loanTransactionDTO.getTransactionType().isInterestRefund()) {
+
createJournalEntriesForInterestPaymentWaiverOrInterestRefund(loanDTO,
loanTransactionDTO, office);
}
}
}
- private void createJournalEntriesForInterestPaymentWaiver(LoanDTO loanDTO,
LoanTransactionDTO loanTransactionDTO, Office office) {
+ private void
createJournalEntriesForInterestPaymentWaiverOrInterestRefund(LoanDTO loanDTO,
LoanTransactionDTO loanTransactionDTO,
+ Office office) {
final Long loanProductId = loanDTO.getLoanProductId();
final Long loanId = loanDTO.getLoanId();
final String currencyCode = loanDTO.getCurrencyCode();
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java
index a15d7f4a1..bef3ac73b 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/InternalLoanInformationApiResource.java
@@ -23,9 +23,11 @@ import static
org.apache.fineract.infrastructure.core.domain.AuditableFieldsCons
import static
org.apache.fineract.infrastructure.core.domain.AuditableFieldsConstants.LAST_MODIFIED_BY;
import static
org.apache.fineract.infrastructure.core.domain.AuditableFieldsConstants.LAST_MODIFIED_DATE;
+import com.google.gson.Gson;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import jakarta.ws.rs.Consumes;
import jakarta.ws.rs.GET;
+import jakarta.ws.rs.POST;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
@@ -41,7 +43,9 @@ import
org.apache.fineract.infrastructure.core.api.ApiRequestParameterHelper;
import org.apache.fineract.infrastructure.core.boot.FineractProfiles;
import
org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
import
org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
+import org.apache.fineract.portfolio.loanaccount.data.LoanRefundRequestData;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import
org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
import
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
@@ -64,6 +68,8 @@ public class InternalLoanInformationApiResource implements
InitializingBean {
private final ToApiJsonSerializer<List> toApiJsonSerializerForList;
private final ApiRequestParameterHelper apiRequestParameterHelper;
private final AdvancedPaymentDataMapper advancedPaymentDataMapper;
+ private final LoanAccountDomainService loanAccountDomainService;
+ private final Gson gson = new Gson();
@Override
@SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
@@ -152,4 +158,23 @@ public class InternalLoanInformationApiResource implements
InitializingBean {
final Loan loan =
loanRepositoryWrapper.findOneWithNotFoundDetection(loanId);
return
advancedPaymentDataMapper.mapLoanPaymentAllocationRule(loan.getPaymentAllocationRules());
}
+
+ @POST
+ @Path("{loanId}/apply-interest-refund")
+ @Consumes({ MediaType.APPLICATION_JSON })
+ @Produces({ MediaType.APPLICATION_JSON })
+ @SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
+ public Long applyInterestRefundToLoan(@Context final UriInfo uriInfo,
@PathParam("loanId") Long loanId,
+ final String apiRequestBodyAsJson) {
+
log.warn("------------------------------------------------------------");
+ log.warn("
");
+ log.warn(" Apply Loan Transaction to Interest Refund loanId {} ",
loanId);
+ log.warn("
");
+
log.warn("------------------------------------------------------------");
+ LoanRefundRequestData loanRefundRequestData =
gson.fromJson(apiRequestBodyAsJson, LoanRefundRequestData.class);
+ final Loan loan =
loanRepositoryWrapper.findOneWithNotFoundDetection(loanId);
+ final LoanTransaction loanTransaction =
loanAccountDomainService.applyInterestRefund(loan, loanRefundRequestData);
+ return loanTransaction.getId();
+ }
+
}
diff --git
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
index 5a0f488cc..a83c30591 100644
---
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
+++
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanAccountDomainServiceJpa.java
@@ -67,7 +67,6 @@ import
org.apache.fineract.infrastructure.event.business.service.BusinessEventNo
import org.apache.fineract.organisation.holiday.domain.Holiday;
import org.apache.fineract.organisation.holiday.domain.HolidayRepository;
import org.apache.fineract.organisation.holiday.domain.HolidayStatusType;
-import
org.apache.fineract.organisation.monetary.domain.ApplicationCurrencyRepositoryWrapper;
import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
import org.apache.fineract.organisation.monetary.domain.Money;
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
@@ -87,6 +86,7 @@ import
org.apache.fineract.portfolio.delinquency.validator.LoanDelinquencyAction
import org.apache.fineract.portfolio.group.domain.Group;
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
+import org.apache.fineract.portfolio.loanaccount.data.LoanRefundRequestData;
import
org.apache.fineract.portfolio.loanaccount.data.LoanScheduleDelinquencyData;
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
import
org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
@@ -120,7 +120,6 @@ public class LoanAccountDomainServiceJpa implements
LoanAccountDomainService {
private final JournalEntryWritePlatformService
journalEntryWritePlatformService;
private final NoteRepository noteRepository;
private final AccountTransferRepository accountTransferRepository;
- private final ApplicationCurrencyRepositoryWrapper
applicationCurrencyRepository;
private final BusinessEventNotifierService businessEventNotifierService;
private final LoanUtilService loanUtilService;
private final StandingInstructionRepository standingInstructionRepository;
@@ -801,4 +800,26 @@ public class LoanAccountDomainServiceJpa implements
LoanAccountDomainService {
}
}
+ @Override
+ public LoanTransaction applyInterestRefund(final Loan loan, final
LoanRefundRequestData loanRefundRequest) {
+ final PaymentDetail paymentDetail = null;
+ final LocalDate transactionDate = DateUtils.getBusinessLocalDate();
+ final BigDecimal refundAmount = loanRefundRequest.getTotalAmount();
+
+ final List<Long> existingTransactionIds = new ArrayList<>();
+ final List<Long> existingReversedTransactionIds = new ArrayList<>();
+ existingTransactionIds.addAll(loan.findExistingTransactionIds());
+
existingReversedTransactionIds.addAll(loan.findExistingReversedTransactionIds());
+
+ final LoanTransaction interestRefundTransaction =
LoanTransaction.interestRefund(loan, loan.getOffice(), refundAmount,
+ loanRefundRequest.getPrincipal(),
loanRefundRequest.getInterest(), loanRefundRequest.getFeeCharges(),
+ loanRefundRequest.getPenaltyCharges(), paymentDetail,
transactionDate, externalIdFactory.create());
+ interestRefundTransaction.updateLoan(loan);
+
saveLoanTransactionWithDataIntegrityViolationChecks(interestRefundTransaction);
+ loan.addLoanTransaction(interestRefundTransaction);
+ postJournalEntries(loan, existingTransactionIds,
existingReversedTransactionIds, false);
+
+ return interestRefundTransaction;
+ }
+
}
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 fc0e6f6e6..15d557e88 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
@@ -49,6 +49,7 @@ import java.util.function.Consumer;
import lombok.AllArgsConstructor;
import lombok.RequiredArgsConstructor;
import lombok.ToString;
+import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.batch.domain.BatchRequest;
import org.apache.fineract.batch.domain.BatchResponse;
import org.apache.fineract.client.models.AdvancedPaymentData;
@@ -100,6 +101,7 @@ import org.hamcrest.Matchers;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.extension.ExtendWith;
+@Slf4j
@ExtendWith(LoanTestLifecycleExtension.class)
public abstract class BaseLoanIntegrationTest {
diff --git
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRefundTransactionTest.java
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRefundTransactionTest.java
new file mode 100644
index 000000000..2d2e5bbc2
--- /dev/null
+++
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanRefundTransactionTest.java
@@ -0,0 +1,245 @@
+/**
+ * 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.apache.fineract.integrationtests.BaseLoanIntegrationTest.InterestRateFrequencyType.YEARS;
+import static
org.apache.fineract.integrationtests.BaseLoanIntegrationTest.RepaymentFrequencyType.DAYS;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+
+import com.google.gson.Gson;
+import io.restassured.builder.RequestSpecBuilder;
+import io.restassured.builder.ResponseSpecBuilder;
+import io.restassured.http.ContentType;
+import io.restassured.specification.RequestSpecification;
+import io.restassured.specification.ResponseSpecification;
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.concurrent.atomic.AtomicLong;
+import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.PostLoanProductsRequest;
+import org.apache.fineract.client.models.PostLoanProductsResponse;
+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.integrationtests.common.BusinessDateHelper;
+import org.apache.fineract.integrationtests.common.ClientHelper;
+import org.apache.fineract.integrationtests.common.LoanRescheduleRequestHelper;
+import org.apache.fineract.integrationtests.common.Utils;
+import org.apache.fineract.integrationtests.common.accounting.AccountHelper;
+import
org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
+import org.apache.fineract.integrationtests.common.loans.LoanTransactionHelper;
+import org.apache.fineract.portfolio.common.domain.DaysInMonthType;
+import org.apache.fineract.portfolio.common.domain.DaysInYearType;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class LoanRefundTransactionTest extends BaseLoanIntegrationTest {
+
+ private static final Logger LOG =
LoggerFactory.getLogger(LoanRefundTransactionTest.class);
+ private static final String DATETIME_PATTERN = "dd MMMM yyyy";
+ private static ResponseSpecification responseSpec;
+ private static RequestSpecification requestSpec;
+ private static BusinessDateHelper businessDateHelper;
+ private static LoanTransactionHelper loanTransactionHelper;
+ private static AccountHelper accountHelper;
+ private static LoanRescheduleRequestHelper loanRescheduleRequestHelper;
+
+ @BeforeAll
+ public static void setup() {
+ Utils.initializeRESTAssured();
+ requestSpec = new
RequestSpecBuilder().setContentType(ContentType.JSON).build();
+ requestSpec.header("Authorization", "Basic " +
Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey());
+ requestSpec.header("Fineract-Platform-TenantId", "default");
+ responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build();
+ loanTransactionHelper = new LoanTransactionHelper(requestSpec,
responseSpec);
+ businessDateHelper = new BusinessDateHelper();
+ accountHelper = new AccountHelper(requestSpec, responseSpec);
+ ClientHelper clientHelper = new ClientHelper(requestSpec,
responseSpec);
+ loanRescheduleRequestHelper = new
LoanRescheduleRequestHelper(requestSpec, responseSpec);
+ }
+
+ // UC1: (Internal case) Generate a totalInterestRefund using Advanced
payment allocation with Interest Refund
+ // options and Apply
+ // Interest Refund transaction
+ // 1. Create a Loan product with Adv. Pment. Alloc. and with Accrual and
Interest Refund
+ // 2. Submit, Approve and Disburse a Loan account
+ // 3. Apply the INTEREST_REFUND transaction to validate the Journal
Entries generated
+ @Test
+ public void uc1() {
+ final String operationDate = "1 January 2024";
+ AtomicLong createdLoanId = new AtomicLong();
+ runAt(operationDate, () -> {
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ PostLoanProductsRequest product =
createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
+
.interestRatePerPeriod(108.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
+
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.ACTUAL.getValue())
+
.daysInYearType(DaysInYearType.DAYS_360.getValue()).numberOfRepayments(4)//
+ .maxInterestRatePerPeriod((double) 110)//
+ .repaymentEvery(1)//
+ .repaymentFrequencyType(1L)//
+ .allowPartialPeriodInterestCalcualtion(false)//
+ .multiDisburseLoan(false)//
+ .disallowExpectedDisbursements(null)//
+ .allowApprovedDisbursedAmountsOverApplied(null)//
+ .overAppliedCalculationType(null)//
+ .overAppliedNumber(null)//
+ .installmentAmountInMultiplesOf(null)//
+ ;//
+ PostLoanProductsResponse loanProductResponse =
loanProductHelper.createLoanProduct(product);
+ PostLoansRequest applicationRequest = applyLoanRequest(clientId,
loanProductResponse.getResourceId(), operationDate, 1000.0, 4)
+ .interestRatePerPeriod(BigDecimal.valueOf(108.0));
+
+ applicationRequest =
applicationRequest.interestCalculationPeriodType(DAYS)
+
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
+
+ PostLoansResponse loanResponse =
loanTransactionHelper.applyLoan(applicationRequest);
+ createdLoanId.set(loanResponse.getLoanId());
+
+ loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
+ new
PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000.0)).dateFormat(DATETIME_PATTERN)
+ .approvedOnDate("1 January 2024").locale("en"));
+
+ loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().actualDisbursementDate("1
January 2024").dateFormat(DATETIME_PATTERN)
+
.transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
+
+ // After Disbursement we are expecting zero interest refund
+ GetLoansLoanIdResponse loanDetails =
loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ assertEquals(BigDecimal.ZERO,
loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
+ });
+
+ runAt("10 February 2024", () -> {
+ // After Interest refund transaction we are expecting non zero
interest refund
+ GetLoansLoanIdResponse loanDetails =
loanTransactionHelper.getLoanDetails(createdLoanId.get());
+ LOG.info("value {}",
loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
+ assertEquals(BigDecimal.ZERO,
loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
+
+ final BigDecimal inteterestRefund = BigDecimal.valueOf(11);
+ final Long loanTransactionId =
loanTransactionHelper.applyInterestRefundLoanTransaction(requestSpec,
responseSpec,
+ createdLoanId.get(),
buildJsonBody(BigDecimal.valueOf(5.0), BigDecimal.valueOf(3.0),
BigDecimal.valueOf(2.0),
+ BigDecimal.valueOf(1.0), null));
+
+ loanDetails =
loanTransactionHelper.getLoanDetails(createdLoanId.get());
+ assertEquals(inteterestRefund,
loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
+
+ verifyTRJournalEntries(loanTransactionId, journalEntry(5.0,
loansReceivableAccount, "CREDIT"),
+ journalEntry(3.0, interestReceivableAccount, "CREDIT"),
journalEntry(2.0, feeReceivableAccount, "CREDIT"),
+ journalEntry(1.0, penaltyReceivableAccount, "CREDIT"),
journalEntry(11.0, overpaymentAccount, "CREDIT"),
+ journalEntry(22.0, interestIncomeAccount, "DEBIT"));
+ });
+ }
+
+ // UC2: (Internal case) Generate a totalInterestRefund using Advanced
payment allocation with Interest Refund
+ // options and Apply
+ // Interest Refund transaction when the Loan Account is ChargeBack
+ // 1. Create a Loan product with Adv. Pment. Alloc. and with Accrual and
Interest Refund
+ // 2. Submit, Approve and Disburse a Loan account
+ // 3. Apply the MERCHANT_ISSUED_REFUND transaction to validate
totalInterestRefund different than Zero
+ // 4- Apply the INTEREST_REFUND transaction to validate the Journal
Entries generated
+ @Test
+ public void uc2() {
+ final String operationDate = "1 January 2024";
+ AtomicLong createdLoanId = new AtomicLong();
+ runAt(operationDate, () -> {
+ Long clientId =
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+ PostLoanProductsRequest product =
createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation()
+
.interestRatePerPeriod(108.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS)
+
.interestRateFrequencyType(YEARS).daysInMonthType(DaysInMonthType.ACTUAL.getValue())
+
.daysInYearType(DaysInYearType.DAYS_360.getValue()).numberOfRepayments(4)//
+ .maxInterestRatePerPeriod((double) 110)//
+ .repaymentEvery(1)//
+ .repaymentFrequencyType(1L)//
+ .allowPartialPeriodInterestCalcualtion(false)//
+ .multiDisburseLoan(false)//
+ .disallowExpectedDisbursements(null)//
+ .allowApprovedDisbursedAmountsOverApplied(null)//
+ .overAppliedCalculationType(null)//
+ .overAppliedNumber(null)//
+ .installmentAmountInMultiplesOf(null)//
+ ;//
+ PostLoanProductsResponse loanProductResponse =
loanProductHelper.createLoanProduct(product);
+ PostLoansRequest applicationRequest = applyLoanRequest(clientId,
loanProductResponse.getResourceId(), operationDate, 1000.0, 4)
+ .interestRatePerPeriod(BigDecimal.valueOf(108.0));
+
+ applicationRequest =
applicationRequest.interestCalculationPeriodType(DAYS)
+
.transactionProcessingStrategyCode(LoanProductTestBuilder.ADVANCED_PAYMENT_ALLOCATION_STRATEGY);
+
+ PostLoansResponse loanResponse =
loanTransactionHelper.applyLoan(applicationRequest);
+ createdLoanId.set(loanResponse.getLoanId());
+
+ loanTransactionHelper.approveLoan(loanResponse.getLoanId(),
+ new
PostLoansLoanIdRequest().approvedLoanAmount(BigDecimal.valueOf(1000.0)).dateFormat(DATETIME_PATTERN)
+ .approvedOnDate("1 January 2024").locale("en"));
+
+ loanTransactionHelper.disburseLoan(loanResponse.getLoanId(),
+ new PostLoansLoanIdRequest().actualDisbursementDate("1
January 2024").dateFormat(DATETIME_PATTERN)
+
.transactionAmount(BigDecimal.valueOf(1000.0)).locale("en"));
+
+ // After Disbursement we are expecting zero interest refund
+ GetLoansLoanIdResponse loanDetails =
loanTransactionHelper.getLoanDetails(loanResponse.getLoanId());
+ assertEquals(BigDecimal.ZERO,
loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
+ });
+
+ runAt("1 February 2024", () -> {
+ // After Interest refund transaction we are expecting non zero
interest refund
+ GetLoansLoanIdResponse loanDetails =
loanTransactionHelper.getLoanDetails(createdLoanId.get());
+ LOG.info("value {}",
loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
+ assertEquals(BigDecimal.ZERO,
loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
+
+ Long repayment1TransactionId =
addRepaymentForLoan(createdLoanId.get(), 250.0, "1 February 2024");
+ chargeOffLoan(createdLoanId.get(), "1 February 2024");
+ });
+
+ runAt("10 February 2024", () -> {
+ // After Interest refund transaction we are expecting non zero
interest refund
+ GetLoansLoanIdResponse loanDetails =
loanTransactionHelper.getLoanDetails(createdLoanId.get());
+ LOG.info("value {}",
loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
+ LOG.info("chargedOffOnDate {}",
loanDetails.getTimeline().getChargedOffOnDate());
+ assertEquals(BigDecimal.ZERO,
loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
+
+ final BigDecimal inteterestRefund = BigDecimal.valueOf(11);
+ final Long loanTransactionId =
loanTransactionHelper.applyInterestRefundLoanTransaction(requestSpec,
responseSpec,
+ createdLoanId.get(),
buildJsonBody(BigDecimal.valueOf(5.0), BigDecimal.valueOf(3.0),
BigDecimal.valueOf(2.0),
+ BigDecimal.valueOf(1.0), null));
+
+ loanDetails =
loanTransactionHelper.getLoanDetails(createdLoanId.get());
+ assertEquals(inteterestRefund,
loanDetails.getSummary().getTotalInterestRefund().stripTrailingZeros());
+
+ verifyTRJournalEntries(loanTransactionId, journalEntry(11.0,
interestIncomeChargeOffAccount, "CREDIT"),
+ journalEntry(11.0, overpaymentAccount, "CREDIT"),
journalEntry(22.0, interestIncomeAccount, "DEBIT"));
+ });
+ }
+
+ private String buildJsonBody(final BigDecimal principal, final BigDecimal
interest, final BigDecimal feeCharges,
+ final BigDecimal penaltyCharges, final BigDecimal overpayment) {
+ final HashMap<String, BigDecimal> map = new HashMap<>();
+ map.put("principal", principal);
+ map.put("interest", interest);
+ map.put("feeCharges", feeCharges);
+ map.put("penaltyCharges", penaltyCharges);
+ if (overpayment != null) {
+ map.put("overpayment", overpayment);
+ }
+
+ return new Gson().toJson(map);
+ }
+
+}
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 534049b7b..58f04ad23 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
@@ -1700,6 +1700,14 @@ public class LoanTransactionHelper extends
IntegrationTest {
return Utils.performServerGet(requestSpec, responseSpec,
GET_LOAN_TRANSACTION_URL, jsonReturn);
}
+ public Long applyInterestRefundLoanTransaction(final RequestSpecification
requestSpec, final ResponseSpecification responseSpec,
+ final Long loanId, final String jsonBody) {
+ final String POST_LOAN_TRANSACTION_URL =
"/fineract-provider/api/v1/internal/loan/" + loanId + "/apply-interest-refund/"
+ "?"
+ + Utils.TENANT_IDENTIFIER;
+ final String reponse = Utils.performServerPost(requestSpec,
responseSpec, POST_LOAN_TRANSACTION_URL, jsonBody);
+ return Long.valueOf(reponse);
+ }
+
public void printRepaymentSchedule(GetLoansLoanIdResponse
getLoansLoanIdResponse) {
GetLoansLoanIdRepaymentSchedule getLoanRepaymentSchedule =
getLoansLoanIdResponse.getRepaymentSchedule();
if (getLoanRepaymentSchedule != null) {