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 ba3aa80b7ca90159fa5ae091b044a09169f0d061
Author: budaidev <[email protected]>
AuthorDate: Mon Mar 31 19:42:20 2025 +0200

    FINERACT-2215: charge-off fix for reversed transactions
---
 .../portfolio/loanaccount/domain/LoanBuilder.java  | 435 +++++++++++++++++++++
 ...nWritePlatformServiceJpaRepositoryImplTest.java | 262 +++++++++++++
 2 files changed, 697 insertions(+)

diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanBuilder.java
 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanBuilder.java
new file mode 100644
index 0000000000..7cd31e6ab6
--- /dev/null
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/domain/LoanBuilder.java
@@ -0,0 +1,435 @@
+/**
+ * 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 static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import org.apache.fineract.infrastructure.codes.domain.CodeValue;
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
+import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.organisation.staff.domain.Staff;
+import org.apache.fineract.portfolio.accountdetails.domain.AccountType;
+import org.apache.fineract.portfolio.client.domain.Client;
+import org.apache.fineract.portfolio.fund.domain.Fund;
+import org.apache.fineract.portfolio.group.domain.Group;
+import 
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.LoanRepaymentScheduleTransactionProcessor;
+import 
org.apache.fineract.portfolio.loanaccount.domain.transactionprocessor.impl.InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor;
+import 
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanApplicationTerms;
+import 
org.apache.fineract.portfolio.loanaccount.loanschedule.domain.LoanScheduleModel;
+import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
+import 
org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
+import org.apache.fineract.portfolio.rate.domain.Rate;
+import org.apache.fineract.useradministration.domain.AppUser;
+
+/**
+ * Builder class for creating {@link Loan} objects for testing purposes.
+ */
+public class LoanBuilder {
+
+    private Long id;
+    private String accountNo;
+    private Client client = mock(Client.class);
+    private Group group;
+    private AccountType loanType = AccountType.INDIVIDUAL;
+    private LoanProduct loanProduct;
+    private Fund fund;
+    private Staff loanOfficer;
+    private CodeValue loanPurpose;
+    private LoanRepaymentScheduleTransactionProcessor transactionProcessor = 
new InterestPrincipalPenaltyFeesOrderLoanRepaymentScheduleTransactionProcessor(
+            mock(ExternalIdFactory.class));
+    private LoanProductRelatedDetail loanRepaymentScheduleDetail;
+    private LoanStatus loanStatus = LoanStatus.SUBMITTED_AND_PENDING_APPROVAL;
+    private LoanSubStatus loanSubStatus;
+    private Set<LoanCharge> charges = new HashSet<>();
+    private Set<LoanCollateralManagement> collateral = new HashSet<>();
+    private Boolean syncDisbursementWithMeeting = false;
+    private BigDecimal fixedEmiAmount;
+    private List<LoanDisbursementDetails> disbursementDetails = new 
ArrayList<>();
+    private BigDecimal maxOutstandingLoanBalance;
+    private Boolean createStandingInstructionAtDisbursement = false;
+    private Boolean isFloatingInterestRate = false;
+    private BigDecimal interestRateDifferential;
+    private List<Rate> rates = new ArrayList<>();
+    private BigDecimal fixedPrincipalPercentagePerInstallment;
+    private ExternalId externalId = ExternalId.empty();
+    private LoanApplicationTerms loanApplicationTerms = 
mock(LoanApplicationTerms.class);
+    private LoanScheduleModel loanScheduleModel = 
mock(LoanScheduleModel.class);
+    private Boolean enableInstallmentLevelDelinquency = false;
+    private LocalDate submittedOnDate = LocalDate.now(ZoneId.systemDefault());
+    private LocalDate approvedOnDate;
+    private LocalDate expectedDisbursementDate;
+    private LocalDate actualDisbursementDate;
+    private LocalDate closedOnDate;
+    private LocalDate writtenOffOnDate;
+    private AppUser closedBy;
+    private CodeValue writeOffReason;
+    private boolean chargedOff = false;
+    private CodeValue chargeOffReason;
+    private LocalDate chargedOffOnDate;
+    private AppUser chargedOffBy;
+    private BigDecimal proposedPrincipal;
+    private BigDecimal approvedPrincipal;
+    private BigDecimal netDisbursalAmount;
+    private List<LoanTransaction> loanTransactions = new ArrayList<>();
+    private LoanSummary summary;
+
+    public LoanBuilder(LoanProduct loanProduct) {
+        this.loanProduct = loanProduct;
+        this.loanRepaymentScheduleDetail = 
loanProduct.getLoanProductRelatedDetail();
+
+        Money principal = mock(Money.class);
+        when(principal.getAmount()).thenReturn(BigDecimal.ZERO);
+        when(loanRepaymentScheduleDetail.getPrincipal()).thenReturn(principal);
+
+    }
+
+    public Loan build() {
+        // Build a minimal valid loan using reflection to access the protected 
constructor
+        try {
+            Loan loan = Loan.newIndividualLoanApplication(accountNo, client, 
loanType, loanProduct, fund, loanOfficer, loanPurpose,
+                    transactionProcessor, loanRepaymentScheduleDetail, 
charges, collateral, fixedEmiAmount, disbursementDetails,
+                    maxOutstandingLoanBalance, 
createStandingInstructionAtDisbursement, isFloatingInterestRate, 
interestRateDifferential,
+                    rates, fixedPrincipalPercentagePerInstallment, externalId, 
loanApplicationTerms, loanScheduleModel,
+                    enableInstallmentLevelDelinquency, submittedOnDate);
+
+            if (id != null) {
+                loan.setId(id);
+            }
+            if (loanStatus != null) {
+                loan.setLoanStatus(loanStatus);
+            }
+
+            if (loanSubStatus != null) {
+                loan.setLoanSubStatus(loanSubStatus);
+            }
+
+            if (approvedOnDate != null) {
+                loan.setApprovedOnDate(approvedOnDate);
+            }
+
+            if (expectedDisbursementDate != null) {
+                loan.setExpectedDisbursementDate(expectedDisbursementDate);
+            }
+
+            if (actualDisbursementDate != null) {
+                loan.setActualDisbursementDate(actualDisbursementDate);
+            }
+
+            if (closedOnDate != null) {
+                loan.setClosedOnDate(closedOnDate);
+                loan.setClosedBy(closedBy);
+            }
+
+            if (writtenOffOnDate != null) {
+                loan.setWrittenOffOnDate(writtenOffOnDate);
+            }
+
+            if (chargedOff) {
+                // Use reflection to set chargedOff flag
+                java.lang.reflect.Field chargedOffField = 
Loan.class.getDeclaredField("chargedOff");
+                chargedOffField.setAccessible(true);
+                chargedOffField.set(loan, true);
+
+                // Use reflection to set chargeOffReason
+                java.lang.reflect.Field chargeOffReasonField = 
Loan.class.getDeclaredField("chargeOffReason");
+                chargeOffReasonField.setAccessible(true);
+                chargeOffReasonField.set(loan, chargeOffReason);
+
+                // Use reflection to set chargedOffOnDate
+                java.lang.reflect.Field chargedOffOnDateField = 
Loan.class.getDeclaredField("chargedOffOnDate");
+                chargedOffOnDateField.setAccessible(true);
+                chargedOffOnDateField.set(loan, chargedOffOnDate);
+
+                // Use reflection to set chargedOffBy
+                java.lang.reflect.Field chargedOffByField = 
Loan.class.getDeclaredField("chargedOffBy");
+                chargedOffByField.setAccessible(true);
+                chargedOffByField.set(loan, chargedOffBy);
+            }
+
+            if (proposedPrincipal != null) {
+                loan.setProposedPrincipal(proposedPrincipal);
+            }
+
+            if (approvedPrincipal != null) {
+                loan.setApprovedPrincipal(approvedPrincipal);
+            }
+
+            if (netDisbursalAmount != null) {
+                loan.setNetDisbursalAmount(netDisbursalAmount);
+            }
+
+            // Set loan transactions using reflection
+            if (!loanTransactions.isEmpty()) {
+                java.lang.reflect.Field loanTransactionsField = 
Loan.class.getDeclaredField("loanTransactions");
+                loanTransactionsField.setAccessible(true);
+                loanTransactionsField.set(loan, loanTransactions);
+            }
+
+            // Set summary using reflection if provided
+            if (summary != null) {
+                java.lang.reflect.Field summaryField = 
Loan.class.getDeclaredField("summary");
+                summaryField.setAccessible(true);
+                summaryField.set(loan, summary);
+            }
+
+            return loan;
+        } catch (Exception e) {
+            throw new RuntimeException("Error building Loan object", e);
+        }
+    }
+
+    public LoanBuilder withId(Long id) {
+        this.id = id;
+        return this;
+    }
+
+    public LoanBuilder withAccountNo(String accountNo) {
+        this.accountNo = accountNo;
+        return this;
+    }
+
+    public LoanBuilder withClient(Client client) {
+        this.client = client;
+        return this;
+    }
+
+    public LoanBuilder withGroup(Group group) {
+        this.group = group;
+        return this;
+    }
+
+    public LoanBuilder withLoanType(AccountType loanType) {
+        this.loanType = loanType;
+        return this;
+    }
+
+    public LoanBuilder withLoanProduct(LoanProduct loanProduct) {
+        this.loanProduct = loanProduct;
+        return this;
+    }
+
+    public LoanBuilder withFund(Fund fund) {
+        this.fund = fund;
+        return this;
+    }
+
+    public LoanBuilder withLoanOfficer(Staff loanOfficer) {
+        this.loanOfficer = loanOfficer;
+        return this;
+    }
+
+    public LoanBuilder withLoanPurpose(CodeValue loanPurpose) {
+        this.loanPurpose = loanPurpose;
+        return this;
+    }
+
+    public LoanBuilder 
withTransactionProcessor(LoanRepaymentScheduleTransactionProcessor 
transactionProcessor) {
+        this.transactionProcessor = transactionProcessor;
+        return this;
+    }
+
+    public LoanBuilder 
withLoanRepaymentScheduleDetail(LoanProductRelatedDetail 
loanRepaymentScheduleDetail) {
+        this.loanRepaymentScheduleDetail = loanRepaymentScheduleDetail;
+        return this;
+    }
+
+    public LoanBuilder withLoanStatus(LoanStatus loanStatus) {
+        this.loanStatus = loanStatus;
+        return this;
+    }
+
+    public LoanBuilder withLoanSubStatus(LoanSubStatus loanSubStatus) {
+        this.loanSubStatus = loanSubStatus;
+        return this;
+    }
+
+    public LoanBuilder withCharges(Set<LoanCharge> charges) {
+        this.charges = charges;
+        return this;
+    }
+
+    public LoanBuilder withCollateral(Set<LoanCollateralManagement> 
collateral) {
+        this.collateral = collateral;
+        return this;
+    }
+
+    public LoanBuilder withSyncDisbursementWithMeeting(Boolean 
syncDisbursementWithMeeting) {
+        this.syncDisbursementWithMeeting = syncDisbursementWithMeeting;
+        return this;
+    }
+
+    public LoanBuilder withFixedEmiAmount(BigDecimal fixedEmiAmount) {
+        this.fixedEmiAmount = fixedEmiAmount;
+        return this;
+    }
+
+    public LoanBuilder withDisbursementDetails(List<LoanDisbursementDetails> 
disbursementDetails) {
+        this.disbursementDetails = disbursementDetails;
+        return this;
+    }
+
+    public LoanBuilder withMaxOutstandingLoanBalance(BigDecimal 
maxOutstandingLoanBalance) {
+        this.maxOutstandingLoanBalance = maxOutstandingLoanBalance;
+        return this;
+    }
+
+    public LoanBuilder withCreateStandingInstructionAtDisbursement(Boolean 
createStandingInstructionAtDisbursement) {
+        this.createStandingInstructionAtDisbursement = 
createStandingInstructionAtDisbursement;
+        return this;
+    }
+
+    public LoanBuilder withFloatingInterestRate(Boolean 
isFloatingInterestRate) {
+        this.isFloatingInterestRate = isFloatingInterestRate;
+        return this;
+    }
+
+    public LoanBuilder withInterestRateDifferential(BigDecimal 
interestRateDifferential) {
+        this.interestRateDifferential = interestRateDifferential;
+        return this;
+    }
+
+    public LoanBuilder withRates(List<Rate> rates) {
+        this.rates = rates;
+        return this;
+    }
+
+    public LoanBuilder withFixedPrincipalPercentagePerInstallment(BigDecimal 
fixedPrincipalPercentagePerInstallment) {
+        this.fixedPrincipalPercentagePerInstallment = 
fixedPrincipalPercentagePerInstallment;
+        return this;
+    }
+
+    public LoanBuilder withExternalId(ExternalId externalId) {
+        this.externalId = externalId;
+        return this;
+    }
+
+    public LoanBuilder withLoanApplicationTerms(LoanApplicationTerms 
loanApplicationTerms) {
+        this.loanApplicationTerms = loanApplicationTerms;
+        return this;
+    }
+
+    public LoanBuilder withLoanScheduleModel(LoanScheduleModel 
loanScheduleModel) {
+        this.loanScheduleModel = loanScheduleModel;
+        return this;
+    }
+
+    public LoanBuilder withEnableInstallmentLevelDelinquency(Boolean 
enableInstallmentLevelDelinquency) {
+        this.enableInstallmentLevelDelinquency = 
enableInstallmentLevelDelinquency;
+        return this;
+    }
+
+    public LoanBuilder withSubmittedOnDate(LocalDate submittedOnDate) {
+        this.submittedOnDate = submittedOnDate;
+        return this;
+    }
+
+    public LoanBuilder withApprovedOnDate(LocalDate approvedOnDate) {
+        this.approvedOnDate = approvedOnDate;
+        return this;
+    }
+
+    public LoanBuilder withExpectedDisbursementDate(LocalDate 
expectedDisbursementDate) {
+        this.expectedDisbursementDate = expectedDisbursementDate;
+        return this;
+    }
+
+    public LoanBuilder withActualDisbursementDate(LocalDate 
actualDisbursementDate) {
+        this.actualDisbursementDate = actualDisbursementDate;
+        return this;
+    }
+
+    public LoanBuilder withClosedOnDate(LocalDate closedOnDate) {
+        this.closedOnDate = closedOnDate;
+        return this;
+    }
+
+    public LoanBuilder withClosedBy(AppUser closedBy) {
+        this.closedBy = closedBy;
+        return this;
+    }
+
+    public LoanBuilder withWrittenOffOnDate(LocalDate writtenOffOnDate) {
+        this.writtenOffOnDate = writtenOffOnDate;
+        return this;
+    }
+
+    public LoanBuilder withWriteOffReason(CodeValue writeOffReason) {
+        this.writeOffReason = writeOffReason;
+        return this;
+    }
+
+    public LoanBuilder withChargedOff(boolean chargedOff) {
+        this.chargedOff = chargedOff;
+        return this;
+    }
+
+    public LoanBuilder withChargeOffReason(CodeValue chargeOffReason) {
+        this.chargeOffReason = chargeOffReason;
+        return this;
+    }
+
+    public LoanBuilder withChargedOffOnDate(LocalDate chargedOffOnDate) {
+        this.chargedOffOnDate = chargedOffOnDate;
+        return this;
+    }
+
+    public LoanBuilder withChargedOffBy(AppUser chargedOffBy) {
+        this.chargedOffBy = chargedOffBy;
+        return this;
+    }
+
+    public LoanBuilder withProposedPrincipal(BigDecimal proposedPrincipal) {
+        this.proposedPrincipal = proposedPrincipal;
+        return this;
+    }
+
+    public LoanBuilder withApprovedPrincipal(BigDecimal approvedPrincipal) {
+        this.approvedPrincipal = approvedPrincipal;
+        return this;
+    }
+
+    public LoanBuilder withNetDisbursalAmount(BigDecimal netDisbursalAmount) {
+        this.netDisbursalAmount = netDisbursalAmount;
+        return this;
+    }
+
+    public LoanBuilder withLoanTransactions(List<LoanTransaction> 
loanTransactions) {
+        this.loanTransactions = loanTransactions;
+        return this;
+    }
+
+    public LoanBuilder withLoanTransaction(LoanTransaction loanTransaction) {
+        this.loanTransactions.add(loanTransaction);
+        return this;
+    }
+
+    public LoanBuilder withSummary(LoanSummary summary) {
+        this.summary = summary;
+        return this;
+    }
+}
diff --git 
a/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImplTest.java
 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImplTest.java
new file mode 100644
index 0000000000..3eb52ad7a4
--- /dev/null
+++ 
b/fineract-provider/src/test/java/org/apache/fineract/portfolio/loanaccount/service/LoanWritePlatformServiceJpaRepositoryImplTest.java
@@ -0,0 +1,262 @@
+/**
+ * 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.service;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.mockito.ArgumentMatchers.anyLong;
+import static org.mockito.Mockito.lenient;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.when;
+
+import java.math.BigDecimal;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import org.apache.fineract.commands.service.CommandProcessingService;
+import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
+import 
org.apache.fineract.infrastructure.configuration.domain.ConfigurationDomainService;
+import org.apache.fineract.infrastructure.core.api.JsonCommand;
+import org.apache.fineract.infrastructure.core.data.CommandProcessingResult;
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
+import 
org.apache.fineract.infrastructure.core.exception.GeneralPlatformDomainRuleException;
+import org.apache.fineract.infrastructure.core.service.DateUtils;
+import org.apache.fineract.infrastructure.core.service.ExternalIdFactory;
+import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
+import 
org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
+import 
org.apache.fineract.infrastructure.security.service.PlatformSecurityContext;
+import org.apache.fineract.organisation.monetary.data.CurrencyData;
+import org.apache.fineract.organisation.monetary.domain.Money;
+import org.apache.fineract.organisation.monetary.domain.MoneyHelper;
+import org.apache.fineract.portfolio.client.domain.Client;
+import org.apache.fineract.portfolio.loanaccount.domain.Loan;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanBuilder;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanCharge;
+import 
org.apache.fineract.portfolio.loanaccount.domain.LoanRepaymentScheduleInstallmentRepository;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
+import org.apache.fineract.portfolio.loanaccount.domain.LoanTransaction;
+import 
org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
+import 
org.apache.fineract.portfolio.loanaccount.serialization.LoanTransactionValidator;
+import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
+import 
org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
+import org.apache.fineract.useradministration.domain.AppUser;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.jupiter.MockitoExtension;
+import org.springframework.context.ApplicationContext;
+import org.springframework.test.util.ReflectionTestUtils;
+
+@ExtendWith(MockitoExtension.class)
+public class LoanWritePlatformServiceJpaRepositoryImplTest {
+
+    @Mock
+    private LoanAssembler loanAssembler;
+
+    @Mock
+    private LoanRepository loanRepository;
+
+    @Mock
+    private LoanTransactionRepository loanTransactionRepository;
+
+    @Mock
+    private ApplicationContext applicationContext;
+
+    @Mock
+    private CommandProcessingService commandProcessingService;
+
+    @Mock
+    private ExternalIdFactory externalIdFactory;
+
+    @Mock
+    private PlatformSecurityContext context;
+
+    @Mock
+    private LoanTransactionValidator loanTransactionValidator;
+
+    @Mock
+    private BusinessEventNotifierService businessEventNotifierService;
+
+    @Mock
+    private ReprocessLoanTransactionsService reprocessLoanTransactionsService;
+
+    @Mock
+    private LoanRepaymentScheduleInstallmentRepository 
loanRepaymentScheduleInstallmentRepository;
+
+    @Mock
+    private LoanRepositoryWrapper loanRepositoryWrapper;
+
+    @Mock
+    private LoanJournalEntryPoster journalEntryPoster;
+
+    @InjectMocks
+    private LoanWritePlatformServiceJpaRepositoryImpl loanWritePlatformService;
+
+    private Loan loan;
+    private AppUser appUser;
+    private JsonCommand command;
+    private static final Long LOAN_ID = 1L;
+
+    @BeforeEach
+    public void setUp() {
+        appUser = mock(AppUser.class);
+
+        ThreadLocalContextUtil
+                .setBusinessDates(new 
HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, 
DateUtils.parseLocalDate("2025-05-20"))));
+
+        when(context.getAuthenticatedUserIfPresent()).thenReturn(appUser);
+    }
+
+    private void setupMoneyHelper() {
+        ConfigurationDomainService cds = 
Mockito.mock(ConfigurationDomainService.class);
+        lenient().when(cds.getRoundingMode()).thenReturn(6);
+
+        MoneyHelper moneyHelper = new MoneyHelper();
+        ReflectionTestUtils.setField(moneyHelper, 
"configurationDomainService", cds);
+        moneyHelper.initialize();
+    }
+
+    @Test
+    public void chargeOff_withInactiveLoan_expectException() {
+        LoanProductRelatedDetail loanProductDetail = 
mock(LoanProductRelatedDetail.class);
+
+        LoanProduct loanProduct = mock(LoanProduct.class);
+        
when(loanProduct.getLoanProductRelatedDetail()).thenReturn(loanProductDetail);
+        loan = new LoanBuilder(loanProduct).withId(LOAN_ID).build();
+
+        when(loanAssembler.assembleFrom(anyLong())).thenReturn(loan);
+
+        command = mock(JsonCommand.class);
+
+        GeneralPlatformDomainRuleException exception = 
assertThrows(GeneralPlatformDomainRuleException.class,
+                () -> loanWritePlatformService.chargeOff(command));
+
+        assertEquals("Loan: 1 Charge-off is not allowed. Loan Account is not 
Active", exception.getMessage());
+    }
+
+    @Test
+    public void chargeOff_withChargedOffLoan_expectException() {
+        LoanProductRelatedDetail loanProductDetail = 
mock(LoanProductRelatedDetail.class);
+
+        LoanProduct loanProduct = mock(LoanProduct.class);
+        
when(loanProduct.getLoanProductRelatedDetail()).thenReturn(loanProductDetail);
+        loan = new 
LoanBuilder(loanProduct).withId(LOAN_ID).withLoanStatus(LoanStatus.ACTIVE).withChargedOff(true).build();
+
+        when(loanAssembler.assembleFrom(anyLong())).thenReturn(loan);
+
+        command = mock(JsonCommand.class);
+
+        GeneralPlatformDomainRuleException exception = 
assertThrows(GeneralPlatformDomainRuleException.class,
+                () -> loanWritePlatformService.chargeOff(command));
+
+        assertEquals("Loan: 1 is already charged-off", exception.getMessage());
+    }
+
+    @Test
+    public void chargeOff_transactionBeforeLast_expectException() {
+        setupMoneyHelper();
+        LoanProductRelatedDetail loanProductDetail = 
mock(LoanProductRelatedDetail.class);
+
+        LoanProduct loanProduct = mock(LoanProduct.class);
+        
when(loanProduct.getLoanProductRelatedDetail()).thenReturn(loanProductDetail);
+
+        LoanTransaction t1 = LoanTransaction.repayment(null, 
Money.of(CurrencyData.blank(), BigDecimal.valueOf(100)), null,
+                DateUtils.parseLocalDate("2025-05-15"), null);
+
+        loan = new 
LoanBuilder(loanProduct).withId(LOAN_ID).withLoanStatus(LoanStatus.ACTIVE).withLoanTransactions(List.of(t1)).build();
+
+        when(loanAssembler.assembleFrom(anyLong())).thenReturn(loan);
+
+        command = mock(JsonCommand.class);
+        
when(command.localDateValueOfParameterNamed("transactionDate")).thenReturn(DateUtils.parseLocalDate("2025-05-14"));
+
+        GeneralPlatformDomainRuleException exception = 
assertThrows(GeneralPlatformDomainRuleException.class,
+                () -> loanWritePlatformService.chargeOff(command));
+
+        assertEquals("Loan: 1 charge-off cannot be executed. User transaction 
was found after the charge-off transaction date!",
+                exception.getMessage());
+    }
+
+    @Test
+    public void chargeOff_cannotBeInFuture_expectException() {
+        setupMoneyHelper();
+        LoanProductRelatedDetail loanProductDetail = 
mock(LoanProductRelatedDetail.class);
+
+        LoanProduct loanProduct = mock(LoanProduct.class);
+        
when(loanProduct.getLoanProductRelatedDetail()).thenReturn(loanProductDetail);
+
+        LoanTransaction t1 = LoanTransaction.repayment(null, 
Money.of(CurrencyData.blank(), BigDecimal.valueOf(100)), null,
+                DateUtils.parseLocalDate("2025-05-13"), null);
+
+        loan = new 
LoanBuilder(loanProduct).withId(LOAN_ID).withLoanStatus(LoanStatus.ACTIVE).withLoanTransactions(List.of(t1)).build();
+
+        when(loanAssembler.assembleFrom(anyLong())).thenReturn(loan);
+
+        command = mock(JsonCommand.class);
+        
when(command.localDateValueOfParameterNamed("transactionDate")).thenReturn(DateUtils.parseLocalDate("2025-05-24"));
+
+        GeneralPlatformDomainRuleException exception = 
assertThrows(GeneralPlatformDomainRuleException.class,
+                () -> loanWritePlatformService.chargeOff(command));
+
+        assertEquals("The transaction date cannot be in the future.", 
exception.getMessage());
+    }
+
+    @Test
+    public void chargeOff_forReversedTransaction_shouldRun() {
+        LoanProductRelatedDetail loanProductDetail = 
mock(LoanProductRelatedDetail.class);
+        
when(loanProductDetail.getAnnualNominalInterestRate()).thenReturn(BigDecimal.valueOf(10));
+
+        LoanProduct loanProduct = mock(LoanProduct.class);
+        
when(loanProduct.getLoanProductRelatedDetail()).thenReturn(loanProductDetail);
+
+        LoanCharge charge = mock(LoanCharge.class);
+        
when(charge.getSubmittedOnDate()).thenReturn(DateUtils.parseLocalDate("2025-05-10"));
+
+        Client client = mock(Client.class);
+        when(client.getId()).thenReturn(1L);
+
+        LoanSummary summary = LoanSummary.create(BigDecimal.TEN);
+        summary.zeroFields();
+        loan = new 
LoanBuilder(loanProduct).withId(LOAN_ID).withLoanStatus(LoanStatus.ACTIVE).withCharges(Set.of(charge))
+                .withSummary(summary).withClient(client).build();
+
+        LoanTransaction t1 = LoanTransaction.chargeOff(loan, 
DateUtils.parseLocalDate("2025-05-13"), ExternalId.empty());
+        t1.reverse();
+        LoanTransaction t2 = LoanTransaction.chargeOff(loan, 
DateUtils.parseLocalDate("2025-05-10"), ExternalId.empty());
+
+        loan.addLoanTransaction(t1);
+        loan.addLoanTransaction(t2);
+
+        when(loanAssembler.assembleFrom(anyLong())).thenReturn(loan);
+
+        command = mock(JsonCommand.class);
+        
when(command.localDateValueOfParameterNamed("transactionDate")).thenReturn(DateUtils.parseLocalDate("2025-05-12"));
+
+        CommandProcessingResult result = 
loanWritePlatformService.chargeOff(command);
+
+        assertEquals(1L, result.getClientId());
+    }
+}

Reply via email to