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