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")); + }); + } }
