FINERACT-2107: Loan transaction Interest Refund GL Entries
This commit is contained in:
committed by
Adam Saghy
parent
7304af57bc
commit
5ebd3429c6
+39
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+2
@@ -60,6 +60,7 @@ public class LoanTransactionEnumData {
|
|||||||
private final boolean reAge;
|
private final boolean reAge;
|
||||||
private final boolean reAmortize;
|
private final boolean reAmortize;
|
||||||
private final boolean accrualActivity;
|
private final boolean accrualActivity;
|
||||||
|
private final boolean interestRefund;
|
||||||
|
|
||||||
public LoanTransactionEnumData(final Long id, final String code, final String value) {
|
public LoanTransactionEnumData(final Long id, final String code, final String value) {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
@@ -94,6 +95,7 @@ public class LoanTransactionEnumData {
|
|||||||
this.accrualActivity = Long.valueOf(32).equals(this.id);
|
this.accrualActivity = Long.valueOf(32).equals(this.id);
|
||||||
this.reAge = Long.valueOf(LoanTransactionType.REAGE.getValue()).equals(this.id);
|
this.reAge = Long.valueOf(LoanTransactionType.REAGE.getValue()).equals(this.id);
|
||||||
this.reAmortize = Long.valueOf(LoanTransactionType.REAMORTIZE.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() {
|
public boolean isRepaymentType() {
|
||||||
|
|||||||
+3
@@ -27,6 +27,7 @@ import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuild
|
|||||||
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
import org.apache.fineract.infrastructure.core.domain.ExternalId;
|
||||||
import org.apache.fineract.portfolio.delinquency.validator.LoanDelinquencyActionData;
|
import org.apache.fineract.portfolio.delinquency.validator.LoanDelinquencyActionData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.data.LoanRefundRequestData;
|
||||||
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
import org.apache.fineract.portfolio.paymentdetail.domain.PaymentDetail;
|
||||||
|
|
||||||
public interface LoanAccountDomainService {
|
public interface LoanAccountDomainService {
|
||||||
@@ -93,4 +94,6 @@ public interface LoanAccountDomainService {
|
|||||||
LoanTransaction creditBalanceRefund(Loan loan, LocalDate transactionDate, BigDecimal transactionAmount, String noteText,
|
LoanTransaction creditBalanceRefund(Loan loan, LocalDate transactionDate, BigDecimal transactionAmount, String noteText,
|
||||||
ExternalId externalId, PaymentDetail paymentDetail);
|
ExternalId externalId, PaymentDetail paymentDetail);
|
||||||
|
|
||||||
|
LoanTransaction applyInterestRefund(Loan loan, LoanRefundRequestData loanRefundRequest);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+7
@@ -323,6 +323,13 @@ public class LoanTransaction extends AbstractAuditableWithUTCDateTimeCustom<Long
|
|||||||
externalId);
|
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,
|
public static boolean transactionAmountsMatch(final MonetaryCurrency currency, final LoanTransaction loanTransaction,
|
||||||
final LoanTransaction newLoanTransaction) {
|
final LoanTransaction newLoanTransaction) {
|
||||||
return loanTransaction.getAmount(currency).isEqualTo(newLoanTransaction.getAmount(currency))
|
return loanTransaction.getAmount(currency).isEqualTo(newLoanTransaction.getAmount(currency))
|
||||||
|
|||||||
+5
-3
@@ -111,13 +111,15 @@ public class AccrualBasedAccountingProcessorForLoan implements AccountingProcess
|
|||||||
createJournalEntriesForChargeOff(loanDTO, loanTransactionDTO, office);
|
createJournalEntriesForChargeOff(loanDTO, loanTransactionDTO, office);
|
||||||
}
|
}
|
||||||
// Logic for Interest Payment Waiver
|
// Logic for Interest Payment Waiver
|
||||||
else if (loanTransactionDTO.getTransactionType().isInterestPaymentWaiver()) {
|
else if (loanTransactionDTO.getTransactionType().isInterestPaymentWaiver()
|
||||||
createJournalEntriesForInterestPaymentWaiver(loanDTO, loanTransactionDTO, office);
|
|| 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 loanProductId = loanDTO.getLoanProductId();
|
||||||
final Long loanId = loanDTO.getLoanId();
|
final Long loanId = loanDTO.getLoanId();
|
||||||
final String currencyCode = loanDTO.getCurrencyCode();
|
final String currencyCode = loanDTO.getCurrencyCode();
|
||||||
|
|||||||
+25
@@ -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_BY;
|
||||||
import static org.apache.fineract.infrastructure.core.domain.AuditableFieldsConstants.LAST_MODIFIED_DATE;
|
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 edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
|
||||||
import jakarta.ws.rs.Consumes;
|
import jakarta.ws.rs.Consumes;
|
||||||
import jakarta.ws.rs.GET;
|
import jakarta.ws.rs.GET;
|
||||||
|
import jakarta.ws.rs.POST;
|
||||||
import jakarta.ws.rs.Path;
|
import jakarta.ws.rs.Path;
|
||||||
import jakarta.ws.rs.PathParam;
|
import jakarta.ws.rs.PathParam;
|
||||||
import jakarta.ws.rs.Produces;
|
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.boot.FineractProfiles;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
import org.apache.fineract.infrastructure.core.serialization.ApiRequestJsonSerializationSettings;
|
||||||
import org.apache.fineract.infrastructure.core.serialization.ToApiJsonSerializer;
|
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.Loan;
|
||||||
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
|
||||||
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
|
||||||
@@ -64,6 +68,8 @@ public class InternalLoanInformationApiResource implements InitializingBean {
|
|||||||
private final ToApiJsonSerializer<List> toApiJsonSerializerForList;
|
private final ToApiJsonSerializer<List> toApiJsonSerializerForList;
|
||||||
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
private final ApiRequestParameterHelper apiRequestParameterHelper;
|
||||||
private final AdvancedPaymentDataMapper advancedPaymentDataMapper;
|
private final AdvancedPaymentDataMapper advancedPaymentDataMapper;
|
||||||
|
private final LoanAccountDomainService loanAccountDomainService;
|
||||||
|
private final Gson gson = new Gson();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
|
@SuppressFBWarnings("SLF4J_SIGN_ONLY_FORMAT")
|
||||||
@@ -152,4 +158,23 @@ public class InternalLoanInformationApiResource implements InitializingBean {
|
|||||||
final Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId);
|
final Loan loan = loanRepositoryWrapper.findOneWithNotFoundDetection(loanId);
|
||||||
return advancedPaymentDataMapper.mapLoanPaymentAllocationRule(loan.getPaymentAllocationRules());
|
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();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-2
@@ -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.Holiday;
|
||||||
import org.apache.fineract.organisation.holiday.domain.HolidayRepository;
|
import org.apache.fineract.organisation.holiday.domain.HolidayRepository;
|
||||||
import org.apache.fineract.organisation.holiday.domain.HolidayStatusType;
|
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.MonetaryCurrency;
|
||||||
import org.apache.fineract.organisation.monetary.domain.Money;
|
import org.apache.fineract.organisation.monetary.domain.Money;
|
||||||
import org.apache.fineract.organisation.workingdays.domain.WorkingDays;
|
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.domain.Group;
|
||||||
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
|
import org.apache.fineract.portfolio.group.exception.GroupNotActiveException;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
|
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.LoanScheduleDelinquencyData;
|
||||||
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
|
||||||
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
|
import org.apache.fineract.portfolio.loanaccount.service.LoanAccrualTransactionBusinessEventService;
|
||||||
@@ -120,7 +120,6 @@ public class LoanAccountDomainServiceJpa implements LoanAccountDomainService {
|
|||||||
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
|
private final JournalEntryWritePlatformService journalEntryWritePlatformService;
|
||||||
private final NoteRepository noteRepository;
|
private final NoteRepository noteRepository;
|
||||||
private final AccountTransferRepository accountTransferRepository;
|
private final AccountTransferRepository accountTransferRepository;
|
||||||
private final ApplicationCurrencyRepositoryWrapper applicationCurrencyRepository;
|
|
||||||
private final BusinessEventNotifierService businessEventNotifierService;
|
private final BusinessEventNotifierService businessEventNotifierService;
|
||||||
private final LoanUtilService loanUtilService;
|
private final LoanUtilService loanUtilService;
|
||||||
private final StandingInstructionRepository standingInstructionRepository;
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
@@ -49,6 +49,7 @@ import java.util.function.Consumer;
|
|||||||
import lombok.AllArgsConstructor;
|
import lombok.AllArgsConstructor;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import lombok.ToString;
|
import lombok.ToString;
|
||||||
|
import lombok.extern.slf4j.Slf4j;
|
||||||
import org.apache.fineract.batch.domain.BatchRequest;
|
import org.apache.fineract.batch.domain.BatchRequest;
|
||||||
import org.apache.fineract.batch.domain.BatchResponse;
|
import org.apache.fineract.batch.domain.BatchResponse;
|
||||||
import org.apache.fineract.client.models.AdvancedPaymentData;
|
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.Assertions;
|
||||||
import org.junit.jupiter.api.extension.ExtendWith;
|
import org.junit.jupiter.api.extension.ExtendWith;
|
||||||
|
|
||||||
|
@Slf4j
|
||||||
@ExtendWith(LoanTestLifecycleExtension.class)
|
@ExtendWith(LoanTestLifecycleExtension.class)
|
||||||
public abstract class BaseLoanIntegrationTest {
|
public abstract class BaseLoanIntegrationTest {
|
||||||
|
|
||||||
|
|||||||
+245
@@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+8
@@ -1700,6 +1700,14 @@ public class LoanTransactionHelper extends IntegrationTest {
|
|||||||
return Utils.performServerGet(requestSpec, responseSpec, GET_LOAN_TRANSACTION_URL, jsonReturn);
|
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) {
|
public void printRepaymentSchedule(GetLoansLoanIdResponse getLoansLoanIdResponse) {
|
||||||
GetLoansLoanIdRepaymentSchedule getLoanRepaymentSchedule = getLoansLoanIdResponse.getRepaymentSchedule();
|
GetLoansLoanIdRepaymentSchedule getLoanRepaymentSchedule = getLoansLoanIdResponse.getRepaymentSchedule();
|
||||||
if (getLoanRepaymentSchedule != null) {
|
if (getLoanRepaymentSchedule != null) {
|
||||||
|
|||||||
Reference in New Issue
Block a user