FINERACT-2215: charge-off fix for reversed transactions

This commit is contained in:
budaidev
2025-04-02 13:24:19 +02:00
committed by Adam Saghy
parent a0a6d3aa14
commit ba3aa80b7c
2 changed files with 697 additions and 0 deletions
@@ -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;
}
}
@@ -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());
}
}