FINERACT-2107: Loan transaction Interest Refund GL Entries

This commit is contained in:
Jose Alberto Hernandez
2024-08-22 11:04:26 +02:00
committed by Adam Saghy
parent 7304af57bc
commit 5ebd3429c6
10 changed files with 359 additions and 5 deletions
@@ -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);
}
}
@@ -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() {
@@ -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);
}
@@ -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))
@@ -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();
@@ -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();
}
}
@@ -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;
}
}
@@ -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 {
@@ -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);
}
}
@@ -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) {