diff --git a/fineract-avro-schemas/src/main/avro/loan/v1/LoanSummaryDataV1.avsc b/fineract-avro-schemas/src/main/avro/loan/v1/LoanSummaryDataV1.avsc index 1dfffa539..5e74118cf 100644 --- a/fineract-avro-schemas/src/main/avro/loan/v1/LoanSummaryDataV1.avsc +++ b/fineract-avro-schemas/src/main/avro/loan/v1/LoanSummaryDataV1.avsc @@ -450,6 +450,14 @@ "null", "bigdecimal" ] + }, + { + "default": null, + "name": "totalInterestRefund", + "type": [ + "null", + "bigdecimal" + ] } ] } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java index f2cb9a7cd..22f3935ba 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoanApiConstants.java @@ -18,6 +18,9 @@ */ package org.apache.fineract.portfolio.loanaccount.api; +import java.util.List; +import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; + public interface LoanApiConstants { String fixedEmiAmountParameterName = "fixedEmiAmount"; @@ -173,4 +176,18 @@ public interface LoanApiConstants { // Data Validator names String LOAN_FRAUD_DATAVALIDATOR_PREFIX = "loans.fraud"; + + // Loan Summary Transaction Types + List LOAN_SUMMARY_TRANSACTION_TYPES = List.of(LoanTransactionType.CHARGE_ADJUSTMENT.getValue(), // + LoanTransactionType.CHARGEBACK.getValue(), // + LoanTransactionType.CREDIT_BALANCE_REFUND.getValue(), // + LoanTransactionType.DOWN_PAYMENT.getValue(), // + LoanTransactionType.GOODWILL_CREDIT.getValue(), // + LoanTransactionType.INTEREST_PAYMENT_WAIVER.getValue(), // + LoanTransactionType.INTEREST_REFUND.getValue(), // + LoanTransactionType.MERCHANT_ISSUED_REFUND.getValue(), // + LoanTransactionType.PAYOUT_REFUND.getValue(), // + LoanTransactionType.REPAYMENT.getValue() // + ); + } diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java index a2dccd031..af9e1b9f1 100644 --- a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanSummaryData.java @@ -21,6 +21,7 @@ package org.apache.fineract.portfolio.loanaccount.data; import java.math.BigDecimal; import java.time.LocalDate; import java.util.Collection; +import java.util.Optional; import lombok.Builder; import lombok.Data; import lombok.experimental.Accessors; @@ -30,7 +31,6 @@ import org.apache.fineract.organisation.monetary.data.CurrencyData; import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionType; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanScheduleData; import org.apache.fineract.portfolio.loanaccount.loanschedule.data.LoanSchedulePeriodData; -import org.springframework.util.CollectionUtils; /** * Immutable data object representing loan summary information. @@ -96,6 +96,7 @@ public class LoanSummaryData { private BigDecimal totalRepaymentTransaction; private BigDecimal totalRepaymentTransactionReversed; private BigDecimal totalInterestPaymentWaiver; + private BigDecimal totalInterestRefund; private final Long chargeOffReasonId; private final String chargeOffReason; @@ -103,7 +104,7 @@ public class LoanSummaryData { private BigDecimal totalUnpaidAccruedNotDueInterest; public static LoanSummaryData withTransactionAmountsSummary(final LoanSummaryData defaultSummaryData, - final Collection loanTransactions, final LoanScheduleData repaymentSchedule) { + final LoanScheduleData repaymentSchedule, final Collection loanTransactionBalances) { BigDecimal totalMerchantRefund = BigDecimal.ZERO; BigDecimal totalMerchantRefundReversed = BigDecimal.ZERO; @@ -119,32 +120,44 @@ public class LoanSummaryData { BigDecimal totalRepaymentTransaction = BigDecimal.ZERO; BigDecimal totalRepaymentTransactionReversed = BigDecimal.ZERO; BigDecimal totalInterestPaymentWaiver = BigDecimal.ZERO; + BigDecimal totalInterestRefund = BigDecimal.ZERO; BigDecimal totalUnpaidAccruedDueInterest = BigDecimal.ZERO; BigDecimal totalUnpaidAccruedNotDueInterest = BigDecimal.ZERO; - if (!CollectionUtils.isEmpty(loanTransactions)) { + totalChargeAdjustment = fetchLoanTransactionBalanceByType(loanTransactionBalances, + LoanTransactionType.CHARGE_ADJUSTMENT.getValue()); + totalChargeAdjustmentReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, + LoanTransactionType.CHARGE_ADJUSTMENT.getValue()); - totalMerchantRefund = computeTotalAmountForNonReversedTransactions(LoanTransactionType.MERCHANT_ISSUED_REFUND, - loanTransactions); - totalMerchantRefundReversed = computeTotalAmountForReversedTransactions(LoanTransactionType.MERCHANT_ISSUED_REFUND, - loanTransactions); - totalPayoutRefund = computeTotalAmountForNonReversedTransactions(LoanTransactionType.PAYOUT_REFUND, loanTransactions); - totalPayoutRefundReversed = computeTotalAmountForReversedTransactions(LoanTransactionType.PAYOUT_REFUND, loanTransactions); - totalGoodwillCredit = computeTotalAmountForNonReversedTransactions(LoanTransactionType.GOODWILL_CREDIT, loanTransactions); - totalGoodwillCreditReversed = computeTotalAmountForReversedTransactions(LoanTransactionType.GOODWILL_CREDIT, loanTransactions); - totalChargeAdjustment = computeTotalAmountForNonReversedTransactions(LoanTransactionType.CHARGE_ADJUSTMENT, loanTransactions); - totalChargeAdjustmentReversed = computeTotalAmountForReversedTransactions(LoanTransactionType.CHARGE_ADJUSTMENT, - loanTransactions); - totalChargeback = computeTotalAmountForNonReversedTransactions(LoanTransactionType.CHARGEBACK, loanTransactions); - totalCreditBalanceRefund = computeTotalAmountForNonReversedTransactions(LoanTransactionType.CREDIT_BALANCE_REFUND, - loanTransactions); - totalCreditBalanceRefundReversed = computeTotalAmountForReversedTransactions(LoanTransactionType.CREDIT_BALANCE_REFUND, - loanTransactions); - totalRepaymentTransaction = computeTotalRepaymentTransactionAmount(loanTransactions); - totalRepaymentTransactionReversed = computeTotalAmountForReversedTransactions(LoanTransactionType.REPAYMENT, loanTransactions); - totalInterestPaymentWaiver = computeTotalAmountForNonReversedTransactions(LoanTransactionType.INTEREST_PAYMENT_WAIVER, - loanTransactions); - } + totalChargeback = fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.CHARGEBACK.getValue()); + + totalCreditBalanceRefund = fetchLoanTransactionBalanceByType(loanTransactionBalances, + LoanTransactionType.CREDIT_BALANCE_REFUND.getValue()); + totalCreditBalanceRefundReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, + LoanTransactionType.CREDIT_BALANCE_REFUND.getValue()); + + totalGoodwillCredit = fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.GOODWILL_CREDIT.getValue()); + totalGoodwillCreditReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, + LoanTransactionType.GOODWILL_CREDIT.getValue()); + + totalInterestRefund = fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.INTEREST_REFUND.getValue()); + + totalInterestPaymentWaiver = fetchLoanTransactionBalanceByType(loanTransactionBalances, + LoanTransactionType.INTEREST_PAYMENT_WAIVER.getValue()); + + totalMerchantRefund = fetchLoanTransactionBalanceByType(loanTransactionBalances, + LoanTransactionType.MERCHANT_ISSUED_REFUND.getValue()); + totalMerchantRefundReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, + LoanTransactionType.MERCHANT_ISSUED_REFUND.getValue()); + + totalPayoutRefund = fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.PAYOUT_REFUND.getValue()); + totalPayoutRefundReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, + LoanTransactionType.PAYOUT_REFUND.getValue()); + + totalRepaymentTransaction = fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.REPAYMENT.getValue()) + .add(fetchLoanTransactionBalanceByType(loanTransactionBalances, LoanTransactionType.DOWN_PAYMENT.getValue())); + totalRepaymentTransactionReversed = fetchLoanTransactionBalanceReversedByType(loanTransactionBalances, + LoanTransactionType.REPAYMENT.getValue()); if (repaymentSchedule != null) { // Accrued Due Interest on Past due installments @@ -200,35 +213,29 @@ public class LoanSummaryData { .totalCreditBalanceRefundReversed(totalCreditBalanceRefundReversed).totalRepaymentTransaction(totalRepaymentTransaction) .totalRepaymentTransactionReversed(totalRepaymentTransactionReversed).totalInterestPaymentWaiver(totalInterestPaymentWaiver) .totalUnpaidAccruedDueInterest(totalUnpaidAccruedDueInterest) - .totalUnpaidAccruedNotDueInterest(totalUnpaidAccruedNotDueInterest).build(); + .totalUnpaidAccruedNotDueInterest(totalUnpaidAccruedNotDueInterest).totalInterestRefund(totalInterestRefund).build(); + } + + private static BigDecimal fetchLoanTransactionBalanceByType(final Collection loanTransactionBalances, + final Integer transactionType) { + final Optional optLoanTransactionBalance = loanTransactionBalances.stream() + .filter(balance -> balance.getTransactionType().equals(transactionType) && !balance.isReversed()).findFirst(); + return optLoanTransactionBalance.isPresent() ? optLoanTransactionBalance.get().getAmount() : BigDecimal.ZERO; + } + + private static BigDecimal fetchLoanTransactionBalanceReversedByType(final Collection loanTransactionBalances, + final Integer transactionType) { + final Optional optLoanTransactionBalance = loanTransactionBalances.stream() + .filter(balance -> balance.getTransactionType().equals(transactionType) && balance.isReversed() + && balance.isManuallyAdjustedOrReversed()) + .findFirst(); + return optLoanTransactionBalance.isPresent() ? optLoanTransactionBalance.get().getAmount() : BigDecimal.ZERO; } public static LoanSummaryData withOnlyCurrencyData(CurrencyData currencyData) { return LoanSummaryData.builder().currency(currencyData).build(); } - private static BigDecimal computeTotalAmountForReversedTransactions(LoanTransactionType transactionType, - Collection loanTransactions) { - return loanTransactions.stream().filter( - transaction -> transaction.getType().getCode().equals(transactionType.getCode()) && transaction.getReversedOnDate() != null) - .map(txn -> txn.getAmount()).reduce(BigDecimal.ZERO, BigDecimal::add); - } - - private static BigDecimal computeTotalAmountForNonReversedTransactions(LoanTransactionType transactionType, - Collection loanTransactions) { - return loanTransactions.stream().filter( - transaction -> transaction.getType().getCode().equals(transactionType.getCode()) && transaction.getReversedOnDate() == null) - .map(txn -> txn.getAmount()).reduce(BigDecimal.ZERO, BigDecimal::add); - } - - private static BigDecimal computeTotalRepaymentTransactionAmount(Collection loanTransactions) { - BigDecimal totalRepaymentTransaction = computeTotalAmountForNonReversedTransactions(LoanTransactionType.REPAYMENT, - loanTransactions); - BigDecimal totalDownPaymentTransaction = computeTotalAmountForNonReversedTransactions(LoanTransactionType.DOWN_PAYMENT, - loanTransactions); - return totalRepaymentTransaction.add(totalDownPaymentTransaction); - } - private static BigDecimal computeTotalAccruedDueInterestAmount(Collection periods) { final LocalDate businessDate = DateUtils.getBusinessLocalDate(); return periods.stream().filter(period -> !period.getDownPaymentPeriod() && businessDate.isAfter(period.getDueDate())) diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionBalance.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionBalance.java new file mode 100644 index 000000000..bd9b2016f --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/data/LoanTransactionBalance.java @@ -0,0 +1,32 @@ +/** + * 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.Data; + +@Data +public class LoanTransactionBalance { + + private final Integer transactionType; + private final boolean reversed; + private final boolean manuallyAdjustedOrReversed; + private final BigDecimal amount; + +} diff --git a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryBalancesRepository.java b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryBalancesRepository.java new file mode 100644 index 000000000..6890aa989 --- /dev/null +++ b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanSummaryBalancesRepository.java @@ -0,0 +1,71 @@ +/** + * 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.domain; + +import jakarta.persistence.EntityManager; +import jakarta.persistence.TypedQuery; +import jakarta.persistence.criteria.CriteriaBuilder; +import jakarta.persistence.criteria.CriteriaQuery; +import jakarta.persistence.criteria.Path; +import jakarta.persistence.criteria.Predicate; +import jakarta.persistence.criteria.Root; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.jpa.CriteriaQueryFactory; +import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionBalance; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class LoanSummaryBalancesRepository { + + private final EntityManager entityManager; + private final CriteriaQueryFactory criteriaQueryFactory; + + public Collection retrieveLoanSummaryBalancesByTransactionType(final Long loanId, + final List transactionTypes) { + CriteriaBuilder cb = entityManager.getCriteriaBuilder(); + CriteriaQuery query = cb.createQuery(LoanTransactionBalance.class); + + Root root = query.from(LoanTransaction.class); + + Specification spec = (r, q, builder) -> { + Path la = r.get("loan"); + + List predicates = new ArrayList<>(); + predicates.add(cb.equal(la.get("id"), loanId)); // Loan + predicates.add(r.get("typeOf").in(transactionTypes)); // Transaction Type + + return cb.and(predicates.toArray(new Predicate[0])); + }; + + criteriaQueryFactory.applySpecificationToCriteria(root, spec, query); + + query.groupBy(root.get("typeOf"), root.get("reversed"), root.get("manuallyAdjustedOrReversed")); + query.select(cb.construct(LoanTransactionBalance.class, root.get("typeOf"), root.get("reversed"), + root.get("manuallyAdjustedOrReversed"), cb.sum(root.get("amount")))); + + TypedQuery queryToExecute = entityManager.createQuery(query); + return queryToExecute.getResultList(); + } + +} diff --git a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java index e74cbf3c8..72aa76396 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java +++ b/fineract-provider/src/main/java/org/apache/fineract/infrastructure/event/external/service/serialization/serializer/loan/LoanBusinessEventSerializer.java @@ -31,11 +31,12 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.LoanBusines import org.apache.fineract.infrastructure.event.external.service.serialization.mapper.loan.LoanAccountDataMapper; import org.apache.fineract.infrastructure.event.external.service.serialization.serializer.BusinessEventSerializer; import org.apache.fineract.portfolio.delinquency.service.DelinquencyReadPlatformService; +import org.apache.fineract.portfolio.loanaccount.api.LoanApiConstants; import org.apache.fineract.portfolio.loanaccount.data.CollectionData; import org.apache.fineract.portfolio.loanaccount.data.LoanAccountData; import org.apache.fineract.portfolio.loanaccount.data.LoanChargeData; import org.apache.fineract.portfolio.loanaccount.data.LoanSummaryData; -import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; +import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryBalancesRepository; import org.apache.fineract.portfolio.loanaccount.service.LoanChargeReadPlatformService; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; import org.springframework.stereotype.Component; @@ -49,6 +50,7 @@ public class LoanBusinessEventSerializer implements BusinessEventSerializer { private final LoanChargeReadPlatformService loanChargeReadPlatformService; private final DelinquencyReadPlatformService delinquencyReadPlatformService; private final LoanInstallmentLevelDelinquencyEventProducer installmentLevelDelinquencyEventProducer; + private final LoanSummaryBalancesRepository loanSummaryBalancesRepository; @Override public boolean canSerialize(BusinessEvent event) { @@ -72,9 +74,9 @@ public class LoanBusinessEventSerializer implements BusinessEventSerializer { data.setDelinquent(delinquentData); if (data.getSummary() != null) { - final Collection currentLoanTransactions = service.retrieveLoanTransactions(loanId); - data.setSummary( - LoanSummaryData.withTransactionAmountsSummary(data.getSummary(), currentLoanTransactions, data.getRepaymentSchedule())); + data.setSummary(LoanSummaryData.withTransactionAmountsSummary(data.getSummary(), data.getRepaymentSchedule(), + loanSummaryBalancesRepository.retrieveLoanSummaryBalancesByTransactionType(loanId, + LoanApiConstants.LOAN_SUMMARY_TRANSACTION_TYPES))); } else { data.setSummary(LoanSummaryData.withOnlyCurrencyData(data.getCurrency())); } diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java index 468086871..d6b284a42 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResource.java @@ -130,6 +130,7 @@ import org.apache.fineract.portfolio.loanaccount.data.LoanTransactionData; import org.apache.fineract.portfolio.loanaccount.data.PaidInAdvanceData; import org.apache.fineract.portfolio.loanaccount.data.RepaymentScheduleRelatedLoanData; import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus; +import org.apache.fineract.portfolio.loanaccount.domain.LoanSummaryBalancesRepository; import org.apache.fineract.portfolio.loanaccount.domain.LoanTermVariationType; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; import org.apache.fineract.portfolio.loanaccount.exception.LoanTemplateTypeRequiredException; @@ -281,6 +282,7 @@ public class LoansApiResource { private final DefaultToApiJsonSerializer jsonSerializerTagHistory; private final DelinquencyReadPlatformService delinquencyReadPlatformService; private final SqlValidator sqlValidator; + private final LoanSummaryBalancesRepository loanSummaryBalancesRepository; /* * This template API is used for loan approval, ideally this should be invoked on loan that are pending for @@ -1114,8 +1116,9 @@ public class LoansApiResource { // updating summary with transaction amounts summary if (loanBasicDetails.getSummary() != null) { - loanBasicDetails.setSummary( - LoanSummaryData.withTransactionAmountsSummary(loanBasicDetails.getSummary(), currentLoanRepayments, repaymentSchedule)); + loanBasicDetails.setSummary(LoanSummaryData.withTransactionAmountsSummary(loanBasicDetails.getSummary(), repaymentSchedule, + loanSummaryBalancesRepository.retrieveLoanSummaryBalancesByTransactionType(loanBasicDetails.getId(), + LoanApiConstants.LOAN_SUMMARY_TRANSACTION_TYPES))); } final LoanAccountData loanAccount = LoanAccountData.associationsAndTemplate(loanBasicDetails, repaymentSchedule, loanRepayments, diff --git a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java index f8a26679d..44dbe6621 100644 --- a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java +++ b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/api/LoansApiResourceSwagger.java @@ -643,6 +643,8 @@ final class LoansApiResourceSwagger { public BigDecimal totalUnpaidAccruedDueInterest; @Schema(example = "0.000000") public BigDecimal totalUnpaidAccruedNotDueInterest; + @Schema(example = "0.000000") + public BigDecimal totalInterestRefund; public Set overdueCharges; @Schema(example = "1") public Long chargeOffReasonId; diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java index e0224ef3d..f94bca231 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/AdvancedPaymentAllocationLoanRepaymentScheduleTest.java @@ -4836,6 +4836,67 @@ public class AdvancedPaymentAllocationLoanRepaymentScheduleTest extends BaseLoan }); } + // UC146: Validate totalInterestRefund using Advanced payment allocation with Interest Refund options + // 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 tranasaction to validate totalInterestRefund different than Zero + @Test + public void uc146() { + AtomicLong createdLoanId = new AtomicLong(); + runAt("1 January 2024", () -> { + Long clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId(); + final ArrayList interestRefundTypes = new ArrayList(); + interestRefundTypes.add("PAYOUT_REFUND"); + interestRefundTypes.add("MERCHANT_ISSUED_REFUND"); + PostLoanProductsRequest product = createOnePeriod30DaysLongNoInterestPeriodicAccrualProductWithAdvancedPaymentAllocation() + .interestRatePerPeriod(12.0).interestCalculationPeriodType(RepaymentFrequencyType.DAYS).interestRateFrequencyType(YEARS) + .daysInMonthType(DaysInMonthType.ACTUAL.getValue()).daysInYearType(DaysInYearType.DAYS_365.getValue()) + .numberOfRepayments(4)// + .repaymentEvery(5)// + .repaymentFrequencyType(0L)// + .allowPartialPeriodInterestCalcualtion(false)// + .multiDisburseLoan(false)// + .disallowExpectedDisbursements(null)// + .allowApprovedDisbursedAmountsOverApplied(null)// + .overAppliedCalculationType(null)// + .overAppliedNumber(null)// + .installmentAmountInMultiplesOf(null)// + .supportedInterestRefundTypes(interestRefundTypes) // + ;// + PostLoanProductsResponse loanProductResponse = loanProductHelper.createLoanProduct(product); + PostLoansRequest applicationRequest = applyLoanRequest(clientId, loanProductResponse.getResourceId(), "1 January 2024", 1000.0, + 4); + + 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 January 2024", () -> { + loanTransactionHelper.makeMerchantIssuedRefund(createdLoanId.get(), new PostLoansLoanIdTransactionsRequest() + .dateFormat(DATETIME_PATTERN).transactionDate("10 January 2024").locale("en").transactionAmount(300.0)); + + // 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()); + }); + } + private Long applyAndApproveLoanProgressiveAdvancedPaymentAllocationStrategyMonthlyRepayments(Long clientId, Long loanProductId, Integer numberOfRepayments, String loanDisbursementDate, double amount) { LOG.info("------------------------------APPLY AND APPROVE LOAN ---------------------------------------"); diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionSummaryTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionSummaryTest.java index 5ce326a9b..d15f5e4a3 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionSummaryTest.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanTransactionSummaryTest.java @@ -31,6 +31,7 @@ import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.util.HashMap; import java.util.UUID; +import lombok.extern.slf4j.Slf4j; import org.apache.fineract.client.models.GetDelinquencyBucketsResponse; import org.apache.fineract.client.models.GetLoanProductsProductIdResponse; import org.apache.fineract.client.models.GetLoansLoanIdResponse; @@ -52,12 +53,11 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; +@Slf4j @ExtendWith(LoanTestLifecycleExtension.class) public class LoanTransactionSummaryTest { private ResponseSpecification responseSpec; - private ResponseSpecification responseSpecErr400; - private ResponseSpecification responseSpecErr503; private RequestSpecification requestSpec; private ClientHelper clientHelper; private LoanTransactionHelper loanTransactionHelper; @@ -69,8 +69,6 @@ public class LoanTransactionSummaryTest { this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); - this.responseSpecErr400 = new ResponseSpecBuilder().expectStatusCode(400).build(); - this.responseSpecErr503 = new ResponseSpecBuilder().expectStatusCode(503).build(); this.loanTransactionHelper = new LoanTransactionHelper(this.requestSpec, this.responseSpec); this.clientHelper = new ClientHelper(this.requestSpec, this.responseSpec); } @@ -86,13 +84,14 @@ public class LoanTransactionSummaryTest { delinquencyBucketId); // Client and Loan account creation - final Integer clientId = clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId().intValue(); + log.info("Client Id {}", clientId); final GetLoanProductsProductIdResponse getLoanProductsProductResponse = createLoanProduct(loanTransactionHelper, delinquencyBucketId); assertNotNull(getLoanProductsProductResponse); final Integer loanId = createLoanAccount(clientId, getLoanProductsProductResponse.getId(), loanExternalIdStr); + log.info("Loan Id {}", loanId); // make Repayments final PostLoansLoanIdTransactionsResponse repaymentTransaction_1 = loanTransactionHelper.makeLoanRepayment(loanExternalIdStr,