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

commit 8c34fe21225949db388331085ae83611d9faba2e
Author: mark.vituska <[email protected]>
AuthorDate: Wed Apr 30 15:14:53 2025 +0200

    FINERACT-2181: fix undo transaction on charges waiver not reversing changes 
on loan charge
---
 .../portfolio/loanaccount/domain/LoanCharge.java   | 24 +++++++++-
 .../loanaccount/domain/LoanInstallmentCharge.java  | 16 ++++++-
 .../adjustment/LoanAdjustmentServiceImpl.java      | 12 +++++
 .../integrationtests/LoanWaiveChargeTest.java      | 53 ++++++++++++++++++++++
 4 files changed, 102 insertions(+), 3 deletions(-)

diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java
index 8715b859b6..bacf7f02ff 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanCharge.java
@@ -117,9 +117,11 @@ public class LoanCharge extends 
AbstractAuditableWithUTCDateTimeCustom<Long> {
     @Column(name = "is_penalty", nullable = false)
     private boolean penaltyCharge = false;
 
+    @Setter
     @Column(name = "is_paid_derived", nullable = false)
     private boolean paid = false;
 
+    @Setter
     @Column(name = "waived", nullable = false)
     private boolean waived = false;
 
@@ -290,7 +292,8 @@ public class LoanCharge extends 
AbstractAuditableWithUTCDateTimeCustom<Long> {
     public Money waive(final MonetaryCurrency currency, final Integer 
loanInstallmentNumber) {
         if (isInstalmentFee()) {
             final LoanInstallmentCharge chargePerInstallment = 
getInstallmentLoanCharge(loanInstallmentNumber);
-            final Money amountWaived = chargePerInstallment.waive(currency);
+            chargePerInstallment.waive();
+            final Money amountWaived = 
chargePerInstallment.getAmountWaived(currency);
             if (this.amountWaived == null) {
                 this.amountWaived = BigDecimal.ZERO;
             }
@@ -310,6 +313,25 @@ public class LoanCharge extends 
AbstractAuditableWithUTCDateTimeCustom<Long> {
 
     }
 
+    public void undoWaive(final MonetaryCurrency currency, final Integer 
loanInstallmentNumber) {
+        if (isInstalmentFee()) {
+            final LoanInstallmentCharge chargePerInstallment = 
getInstallmentLoanCharge(loanInstallmentNumber);
+            chargePerInstallment.undoWaive();
+            Money amountReversed = 
chargePerInstallment.getAmountOutstanding(currency);
+            this.amountWaived = 
this.amountWaived.subtract(amountReversed.getAmount());
+            this.amountOutstanding = 
this.amountOutstanding.add(amountReversed.getAmount());
+            if (!determineIfFullyPaid()) {
+                this.paid = false;
+                this.waived = false;
+            }
+            return;
+        }
+        this.amountOutstanding = this.amountWaived;
+        this.amountWaived = BigDecimal.ZERO;
+        this.paid = false;
+        this.waived = false;
+    }
+
     private BigDecimal calculateAmountOutstanding(final MonetaryCurrency 
currency) {
         return 
getAmount(currency).minus(getAmountWaived(currency)).minus(getAmountPaid(currency)).getAmount();
     }
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java
index 396c069791..aa085b4557 100644
--- 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/domain/LoanInstallmentCharge.java
@@ -65,9 +65,11 @@ public class LoanInstallmentCharge extends 
AbstractPersistableCustom<Long> imple
     @Column(name = "amount_through_charge_payment", scale = 6, precision = 19, 
nullable = true)
     private BigDecimal amountThroughChargePayment;
 
+    @Setter
     @Column(name = "is_paid_derived", nullable = false)
     private boolean paid = false;
 
+    @Setter
     @Column(name = "waived", nullable = false)
     private boolean waived = false;
 
@@ -97,18 +99,28 @@ public class LoanInstallmentCharge extends 
AbstractPersistableCustom<Long> imple
         this.paid = determineIfFullyPaid();
     }
 
-    public Money waive(final MonetaryCurrency currency) {
+    public void waive() {
         this.amountWaived = this.amountOutstanding;
         this.amountOutstanding = BigDecimal.ZERO;
         this.paid = false;
         this.waived = true;
-        return getAmountWaived(currency);
+    }
+
+    public void undoWaive() {
+        this.amountOutstanding = this.amountWaived;
+        this.amountWaived = BigDecimal.ZERO;
+        this.paid = false;
+        this.waived = false;
     }
 
     public Money getAmountWaived(final MonetaryCurrency currency) {
         return Money.of(currency, this.amountWaived);
     }
 
+    public Money getAmountOutstanding(final MonetaryCurrency currency) {
+        return Money.of(currency, this.amountOutstanding);
+    }
+
     private boolean determineIfFullyPaid() {
         if (this.amount == null) {
             return true;
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java
index 9888ab9647..339d4f3580 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/adjustment/LoanAdjustmentServiceImpl.java
@@ -36,6 +36,7 @@ import 
org.apache.fineract.infrastructure.core.service.DateUtils;
 import 
org.apache.fineract.infrastructure.event.business.domain.loan.LoanAdjustTransactionBusinessEvent;
 import 
org.apache.fineract.infrastructure.event.business.domain.loan.LoanBalanceChangedBusinessEvent;
 import 
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import org.apache.fineract.organisation.monetary.domain.MonetaryCurrency;
 import org.apache.fineract.organisation.monetary.domain.Money;
 import org.apache.fineract.portfolio.account.PortfolioAccountType;
 import 
org.apache.fineract.portfolio.account.service.AccountTransfersWritePlatformService;
@@ -44,6 +45,7 @@ import 
org.apache.fineract.portfolio.loanaccount.data.HolidayDetailDTO;
 import org.apache.fineract.portfolio.loanaccount.data.ScheduleGeneratorDTO;
 import org.apache.fineract.portfolio.loanaccount.domain.Loan;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
 import org.apache.fineract.portfolio.loanaccount.domain.LoanEvent;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanLifecycleStateMachine;
 import 
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
@@ -119,6 +121,16 @@ public class LoanAdjustmentServiceImpl implements 
LoanAdjustmentService {
             newTransactionDetail = LoanTransaction.waiver(loan.getOffice(), 
loan, transactionAmountAsMoney, transactionDate,
                     interestComponent, unrecognizedIncome, txnExternalId);
         }
+        if (transactionToAdjust.isChargesWaiver()) {
+            transactionToAdjust.getLoanChargesPaid().forEach(loanChargePaidBy 
-> {
+                LoanCharge loanCharge = loanChargePaidBy.getLoanCharge();
+                MonetaryCurrency currency = loanCharge.getLoan().getCurrency();
+
+                Integer installmentNumber = 
loanChargePaidBy.getInstallmentNumber();
+
+                loanCharge.undoWaive(currency, installmentNumber);
+            });
+        }
 
         LocalDate recalculateFrom = null;
 
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanWaiveChargeTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanWaiveChargeTest.java
index 742ca90f49..651e357eca 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanWaiveChargeTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanWaiveChargeTest.java
@@ -19,14 +19,19 @@
 package org.apache.fineract.integrationtests;
 
 import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 
 import com.google.common.collect.Streams;
 import java.math.BigDecimal;
 import java.time.LocalDate;
+import java.util.Objects;
+import java.util.Optional;
 import java.util.concurrent.atomic.AtomicLong;
 import java.util.stream.Stream;
+import org.apache.fineract.client.models.GetLoansLoanIdLoanChargePaidByData;
 import org.apache.fineract.client.models.GetLoansLoanIdResponse;
+import org.apache.fineract.client.models.GetLoansLoanIdTransactionsResponse;
 import org.apache.fineract.client.models.PostChargesResponse;
 import org.apache.fineract.client.models.PostLoanProductsRequest;
 import org.apache.fineract.client.models.PostLoanProductsResponse;
@@ -35,9 +40,11 @@ import 
org.apache.fineract.client.models.PostLoansLoanIdResponse;
 import org.apache.fineract.client.models.PostLoansLoanIdTransactionsRequest;
 import org.apache.fineract.client.models.PostLoansRequest;
 import org.apache.fineract.client.models.PostLoansResponse;
+import org.apache.fineract.client.util.CallFailedRuntimeException;
 import org.apache.fineract.integrationtests.common.ClientHelper;
 import 
org.apache.fineract.integrationtests.common.loans.LoanProductTestBuilder;
 import org.junit.jupiter.api.Named;
+import org.junit.jupiter.api.Test;
 import org.junit.jupiter.params.ParameterizedTest;
 import org.junit.jupiter.params.provider.Arguments;
 import org.junit.jupiter.params.provider.MethodSource;
@@ -266,4 +273,50 @@ public class LoanWaiveChargeTest extends 
BaseLoanIntegrationTest {
         });
 
     }
+
+    @Test
+    public void testLoanCannotBeChargedOffWhenUndoingFeeWaiver() {
+        double amount = 1000.0;
+        AtomicLong appliedLoanId = new AtomicLong();
+
+        runAt("01 January 2023", () -> {
+            // Create Client
+            Long clientId = 
clientHelper.createClient(ClientHelper.defaultClientCreationRequest()).getClientId();
+
+            // Create Loan Product
+            PostLoanProductsRequest product = create4IProgressive();
+            PostLoanProductsResponse loanProductResponse = 
loanProductHelper.createLoanProduct(product);
+
+            Long loanProductId = loanProductResponse.getResourceId();
+
+            // Apply and Approve Loan
+            Long loanId = applyAndApproveProgressiveLoan(clientId, 
loanProductId, "01 January 2023", amount, 9.9, 4, null);
+            appliedLoanId.set(loanId);
+
+            // disburse Loan
+            disburseLoan(loanId, BigDecimal.valueOf(amount), "01 January 
2023");
+        });
+        runAt("23 January 2023", () -> {
+            // create charge
+            double chargeAmount = 5.0;
+            PostChargesResponse chargeResult = createCharge(chargeAmount, 
"EUR");
+            Long chargeId = chargeResult.getResourceId();
+
+            PostLoansLoanIdChargesResponse loanChargeResult = 
addLoanCharge(appliedLoanId.get(), chargeId, "23 January 2023", chargeAmount);
+            long loanChargeId = loanChargeResult.getResourceId();
+
+            // waive charge
+            waiveLoanCharge(appliedLoanId.get(), loanChargeId, 1);
+
+            GetLoansLoanIdTransactionsResponse loanTransactions = 
loanTransactionHelper.getLoanTransactions(appliedLoanId.get());
+            Optional<GetLoansLoanIdLoanChargePaidByData> chargeData = 
loanTransactions.getContent().stream()
+                    .flatMap(t -> 
t.getLoanChargePaidByList().stream()).filter(t -> Objects.equals(loanChargeId, 
t.getChargeId()))
+                    .findAny();
+
+            loanTransactionHelper.reverseLoanTransaction(appliedLoanId.get(), 
chargeData.get().getTransactionId(), "23 January 2023");
+            CallFailedRuntimeException callFailedRuntimeException = 
assertThrows(CallFailedRuntimeException.class,
+                    () -> chargeOffLoan(appliedLoanId.get(), "05 January 
2023"));
+            
assertTrue(callFailedRuntimeException.getMessage().contains("error.msg.loan.monetary.transactions.after.charge.off"));
+        });
+    }
 }

Reply via email to