FINERACT-2327: Forbid accrual and accrual activity creation for closed, overpaid and cancelled loans

This commit is contained in:
mariiaKraievska
2025-02-18 17:56:37 +01:00
committed by Adam Saghy
parent 0f1be3c913
commit 6bb68051ce
4 changed files with 216 additions and 1 deletions
@@ -65,7 +65,7 @@ public class LoanAccrualActivityProcessingServiceImpl implements LoanAccrualActi
@Override
public void makeAccrualActivityTransaction(@NotNull Loan loan, @NotNull LocalDate currentDate) {
if (!loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting()) {
if (!loan.getLoanProductRelatedDetail().isEnableAccrualActivityPosting() || loan.isClosed() || loan.getStatus().isOverpaid()) {
return;
}
// check if loan has installment in the past or due on current date
@@ -129,6 +129,9 @@ public class LoanAccrualsProcessingServiceImpl implements LoanAccrualsProcessing
@Override
@Transactional
public void addPeriodicAccruals(@NotNull LocalDate tillDate, @NotNull Loan loan) {
if (loan.isClosed() || loan.getStatus().isOverpaid()) {
return;
}
addAccruals(loan, tillDate, true, false, true);
}
@@ -0,0 +1,110 @@
/**
* 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.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.stream.Stream;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanAccountDomainService;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProductRelatedDetail;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class LoanAccrualActivityProcessingServiceImplTest {
@InjectMocks
private LoanAccrualActivityProcessingServiceImpl accrualActivityProcessingService;
@Mock
private Loan loan;
@Mock
private LoanStatus loanStatus;
@Mock
private LoanProductRelatedDetail loanProductRelatedDetail;
@Mock
private BusinessEventNotifierService businessEventNotifierService;
@Mock
private LoanAccountDomainService loanAccountDomainService;
@Mock
private LoanTransactionAssembler loanTransactionAssembler;
@BeforeEach
void setUp() {
when(loan.isClosed()).thenReturn(false);
when(loan.getStatus()).thenReturn(loanStatus);
when(loanStatus.isOverpaid()).thenReturn(false);
}
@ParameterizedTest
@MethodSource("loanStatusTestCases")
void addPeriodicAccruals_ShouldNotProceed_WhenLoanIsClosedOrOverpaid(final boolean isClosed, final boolean isOverpaid) {
// Given
final LocalDate currentDate = LocalDate.now(ZoneId.systemDefault());
when(loan.isClosed()).thenReturn(isClosed);
when(loan.getStatus()).thenReturn(loanStatus);
when(loanStatus.isOverpaid()).thenReturn(isOverpaid);
when(loan.getLoanProductRelatedDetail()).thenReturn(loanProductRelatedDetail);
when(loanProductRelatedDetail.isEnableAccrualActivityPosting()).thenReturn(true);
// When
accrualActivityProcessingService.makeAccrualActivityTransaction(loan, currentDate);
// Then
verify(loan, times(1)).isClosed();
verify(loan, never()).getRepaymentScheduleInstallments(any());
verify(loan, never()).addLoanTransaction(any());
verify(loanAccountDomainService, never()).saveLoanTransactionWithDataIntegrityViolationChecks(any());
verify(loanTransactionAssembler, never()).assembleAccrualActivityTransaction(any(), any(), any());
verify(businessEventNotifierService, never()).notifyPreBusinessEvent(any());
verify(businessEventNotifierService, never()).notifyPostBusinessEvent(any());
}
private static Stream<Arguments> loanStatusTestCases() {
return Stream.of(Arguments.of(true, false), // Loan is closed
Arguments.of(false, true) // Loan is overpaid
);
}
}
@@ -0,0 +1,102 @@
/**
* 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.mockito.Mockito.any;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.stream.Stream;
import org.apache.fineract.accounting.journalentry.service.JournalEntryWritePlatformService;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanTransactionRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.Arguments;
import org.junit.jupiter.params.provider.MethodSource;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import org.mockito.junit.jupiter.MockitoSettings;
import org.mockito.quality.Strictness;
@ExtendWith(MockitoExtension.class)
@MockitoSettings(strictness = Strictness.LENIENT)
public class LoanAccrualsProcessingServiceImplTest {
@InjectMocks
private LoanAccrualsProcessingServiceImpl accrualsProcessingService;
@Mock
private Loan loan;
@Mock
private LoanStatus loanStatus;
@Mock
private BusinessEventNotifierService businessEventNotifierService;
@Mock
private LoanTransactionRepository loanTransactionRepository;
@Mock
private JournalEntryWritePlatformService journalEntryWritePlatformService;
@BeforeEach
void setUp() {
when(loan.isClosed()).thenReturn(false);
when(loan.getStatus()).thenReturn(loanStatus);
when(loanStatus.isOverpaid()).thenReturn(false);
}
@ParameterizedTest
@MethodSource("loanStatusTestCases")
void addPeriodicAccruals_ShouldNotProceed_WhenLoanIsClosedOrOverpaid(final boolean isClosed, final boolean isOverpaid) {
// Given
final LocalDate tillDate = LocalDate.now(ZoneId.systemDefault());
when(loan.isClosed()).thenReturn(isClosed);
when(loan.getStatus()).thenReturn(loanStatus);
when(loanStatus.isOverpaid()).thenReturn(isOverpaid);
// When
accrualsProcessingService.addPeriodicAccruals(tillDate, loan);
// Then
verify(loan, times(1)).isClosed();
verify(loanTransactionRepository, never()).saveAndFlush(any());
verify(journalEntryWritePlatformService, never()).createJournalEntriesForLoan(any());
verify(businessEventNotifierService, never()).notifyPostBusinessEvent(any());
verify(loan, never()).addLoanTransaction(any());
}
private static Stream<Arguments> loanStatusTestCases() {
return Stream.of(Arguments.of(true, false), // Loan is closed
Arguments.of(false, true) // Loan is overpaid
);
}
}