FINERACT-2141: Delayed settlement implementation for fineract loan investor module

This commit is contained in:
Arnold Galovics
2025-03-07 17:46:24 +01:00
committed by Arnold Gálovics
parent 23887bdf1c
commit 33e9f7f3db
45 changed files with 4161 additions and 339 deletions
@@ -24,18 +24,20 @@ import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType;
import org.apache.fineract.infrastructure.core.domain.ActionContext;
import org.apache.fineract.infrastructure.core.domain.FineractPlatformTenant;
@@ -45,19 +47,26 @@ import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccount
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.investor.data.ExternalTransferStatus;
import org.apache.fineract.investor.data.ExternalTransferSubStatus;
import org.apache.fineract.investor.domain.ExternalAssetOwner;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
import org.apache.fineract.investor.domain.LoanOwnershipTransferBusinessEvent;
import org.apache.fineract.investor.service.AccountingService;
import org.apache.fineract.investor.service.DelayedSettlementAttributeService;
import org.apache.fineract.investor.service.LoanTransferabilityService;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
@@ -69,17 +78,27 @@ import org.springframework.data.jpa.domain.Specification;
public class LoanAccountOwnerTransferBusinessStepTest {
public static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31);
private static final Long LOAN_PRODUCT_ID = 2L;
private final LocalDate actualDate = LocalDate.now(ZoneId.systemDefault());
@Mock
private ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
@Mock
private ExternalAssetOwnerTransferLoanMappingRepository externalAssetOwnerTransferLoanMappingRepository;
@Mock
private AccountingService accountingService;
@Mock
private BusinessEventNotifierService businessEventNotifierService;
@Mock
private AccountingService accountingService;
private LoanTransferabilityService loanTransferabilityService;
@Mock
private DelayedSettlementAttributeService delayedSettlementAttributeService;
private LoanAccountOwnerTransferBusinessStep underTest;
@BeforeEach
@@ -88,7 +107,8 @@ public class LoanAccountOwnerTransferBusinessStepTest {
ThreadLocalContextUtil.setActionContext(ActionContext.DEFAULT);
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BusinessDateType.BUSINESS_DATE, actualDate)));
underTest = new LoanAccountOwnerTransferBusinessStep(externalAssetOwnerTransferRepository,
externalAssetOwnerTransferLoanMappingRepository, accountingService, businessEventNotifierService);
externalAssetOwnerTransferLoanMappingRepository, accountingService, businessEventNotifierService,
loanTransferabilityService, delayedSettlementAttributeService);
}
@AfterEach
@@ -106,15 +126,21 @@ public class LoanAccountOwnerTransferBusinessStepTest {
final Loan processedLoan = underTest.execute(loanForProcessing);
// then
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
verifyNoInteractions(businessEventNotifierService);
verifyNoInteractions(businessEventNotifierService, loanTransferabilityService, accountingService);
assertEquals(processedLoan, loanForProcessing);
}
@Test
public void givenLoanTwoTransferButInvalidTransfers() {
// given
final LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L);
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(false);
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
@@ -127,14 +153,46 @@ public class LoanAccountOwnerTransferBusinessStepTest {
// then
assertEquals("Illegal transfer found. Expected PENDING and BUYBACK, found: PENDING and ACTIVE", exception.getMessage());
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
verifyNoInteractions(businessEventNotifierService);
verifyNoInteractions(businessEventNotifierService, loanTransferabilityService, accountingService);
}
@Test
public void givenSameDaySaleAndBuybackWithDelayedSettlement() {
// given
final LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L);
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(true);
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
when(secondResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK);
List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem, secondResponseItem);
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
.thenReturn(response);
// when
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> underTest.execute(loanForProcessing));
// then
assertEquals("Delayed Settlement enabled, but found 2 transfers of statuses: PENDING and BUYBACK", exception.getMessage());
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
verifyNoInteractions(businessEventNotifierService, loanTransferabilityService, accountingService);
}
@Test
public void givenLoanTwoTransferSameDay() {
// given
final LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L);
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(false);
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
@@ -192,13 +250,20 @@ public class LoanAccountOwnerTransferBusinessStepTest {
assertEquals(processedLoan, loanForProcessing);
verifyNoInteractions(loanTransferabilityService, accountingService);
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, secondSaveResult);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing, fourthSaveResult);
}
@Test
public void givenLoanBuyback() {
private static Stream<Arguments> buybackStatusDataProvider() {
return Stream.of(Arguments.of(ExternalTransferStatus.BUYBACK_INTERMEDIATE), Arguments.of(ExternalTransferStatus.BUYBACK));
}
@ParameterizedTest
@MethodSource("buybackStatusDataProvider")
public void givenLoanBuyback(final ExternalTransferStatus buybackStatus) {
// given
final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L);
@@ -206,7 +271,7 @@ public class LoanAccountOwnerTransferBusinessStepTest {
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.BUYBACK);
when(firstResponseItem.getStatus()).thenReturn(buybackStatus);
List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem);
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
.thenReturn(response);
@@ -218,6 +283,8 @@ public class LoanAccountOwnerTransferBusinessStepTest {
// when
final Loan processedLoan = underTest.execute(loanForProcessing);
// then
verifyNoInteractions(loanTransferabilityService);
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
verify(firstResponseItem).setEffectiveDateTo(actualDate);
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
@@ -226,147 +293,248 @@ public class LoanAccountOwnerTransferBusinessStepTest {
assertEquals(processedLoan, loanForProcessing);
verify(accountingService).createJournalEntriesForBuybackAssetTransfer(loanForProcessing, firstResponseItem);
verifyNoMoreInteractions(accountingService);
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, firstResponseItem);
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
}
@Test
public void givenLoanSale() {
private static Stream<Arguments> loanSaleTransferableDataProvider() {
return Stream.of(Arguments.of(false, ExternalTransferStatus.PENDING, ExternalTransferStatus.ACTIVE),
Arguments.of(true, ExternalTransferStatus.PENDING_INTERMEDIATE, ExternalTransferStatus.ACTIVE_INTERMEDIATE));
}
@ParameterizedTest
@MethodSource("loanSaleTransferableDataProvider")
public void givenLoanSaleTransferable(final boolean isDelayedSettlementEnabled, final ExternalTransferStatus pendingStatus,
final ExternalTransferStatus expectedActiveStatus) {
// given
final LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L);
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem);
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
.thenReturn(response);
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransfer.class);
ArgumentCaptor<ExternalAssetOwnerTransferLoanMapping> externalAssetOwnerTransferLoanMappingArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransferLoanMapping.class);
ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer);
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(isDelayedSettlementEnabled);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ONE);
when(newTransfer.getStatus()).thenReturn(ExternalTransferStatus.ACTIVE);
ExternalAssetOwnerTransfer pendingTransfer = new ExternalAssetOwnerTransfer();
pendingTransfer.setStatus(pendingStatus);
List<ExternalAssetOwnerTransfer> response = List.of(pendingTransfer);
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
.thenReturn(response);
when(loanTransferabilityService.isTransferable(loanForProcessing, pendingTransfer)).thenReturn(true);
ExternalAssetOwnerTransfer savedNewTransfer = new ExternalAssetOwnerTransfer();
savedNewTransfer.setStatus(expectedActiveStatus);
when(externalAssetOwnerTransferRepository.save(any())).thenReturn(pendingTransfer).thenReturn(savedNewTransfer);
// when
final Loan processedLoan = underTest.execute(loanForProcessing);
// then
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
verify(firstResponseItem).setEffectiveDateTo(actualDate);
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
verify(loanTransferabilityService).isTransferable(loanForProcessing, pendingTransfer);
verifyNoMoreInteractions(loanTransferabilityService);
verify(externalAssetOwnerTransferRepository).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getOwner(),
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getOwner());
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getExternalId(),
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getExternalId());
assertEquals(ExternalTransferStatus.ACTIVE, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus());
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSettlementDate());
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getLoanId(),
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getLoanId());
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getPurchasePriceRatio(),
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getPurchasePriceRatio());
assertEquals(actualDate.plusDays(1), externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom());
assertEquals(FUTURE_DATE_9999_12_31, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo());
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransfer.class);
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
ExternalAssetOwnerTransfer capturedPendingTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0);
ExternalAssetOwnerTransfer capturedActiveTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1);
assertEquals(actualDate, capturedPendingTransfer.getEffectiveDateTo());
assertCommonFieldsOfPendingAndActiveTransfers(capturedPendingTransfer, capturedActiveTransfer);
assertEquals(expectedActiveStatus, capturedActiveTransfer.getStatus());
assertEquals(actualDate, capturedActiveTransfer.getSettlementDate());
assertEquals(actualDate.plusDays(1), capturedActiveTransfer.getEffectiveDateFrom());
assertEquals(FUTURE_DATE_9999_12_31, capturedActiveTransfer.getEffectiveDateTo());
ArgumentCaptor<ExternalAssetOwnerTransferLoanMapping> externalAssetOwnerTransferLoanMappingArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransferLoanMapping.class);
verify(externalAssetOwnerTransferLoanMappingRepository).save(externalAssetOwnerTransferLoanMappingArgumentCaptor.capture());
assertEquals(1L, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getLoanId());
assertEquals(savedNewTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer());
assertEquals(processedLoan, loanForProcessing);
verify(externalAssetOwnerTransferLoanMappingRepository).save(externalAssetOwnerTransferLoanMappingArgumentCaptor.capture());
verify(accountingService).createJournalEntriesForSaleAssetTransfer(loanForProcessing, savedNewTransfer, null);
verifyNoMoreInteractions(accountingService);
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, savedNewTransfer);
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
}
private static Stream<Arguments> loanSaleNotTransferableDataProvider() {
return Stream.of(Arguments.of(ExternalTransferStatus.PENDING, ExternalTransferSubStatus.BALANCE_ZERO),
Arguments.of(ExternalTransferStatus.PENDING, ExternalTransferSubStatus.BALANCE_NEGATIVE),
Arguments.of(ExternalTransferStatus.PENDING_INTERMEDIATE, ExternalTransferSubStatus.BALANCE_ZERO),
Arguments.of(ExternalTransferStatus.PENDING_INTERMEDIATE, ExternalTransferSubStatus.BALANCE_NEGATIVE));
}
@ParameterizedTest
@MethodSource("loanSaleNotTransferableDataProvider")
public void givenLoanSaleNotTransferable(final ExternalTransferStatus pendingStatus,
final ExternalTransferSubStatus expectedSubStatus) {
// given
final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L);
ExternalAssetOwnerTransfer pendingTransfer = new ExternalAssetOwnerTransfer();
pendingTransfer.setStatus(pendingStatus);
List<ExternalAssetOwnerTransfer> response = List.of(pendingTransfer);
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
.thenReturn(response);
when(loanTransferabilityService.isTransferable(loanForProcessing, pendingTransfer)).thenReturn(false);
when(loanTransferabilityService.getDeclinedSubStatus(loanForProcessing)).thenReturn(expectedSubStatus);
ExternalAssetOwnerTransfer savedNewTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(savedNewTransfer.getStatus()).thenReturn(ExternalTransferStatus.DECLINED);
when(externalAssetOwnerTransferRepository.save(any())).thenReturn(pendingTransfer).thenReturn(savedNewTransfer);
// when
final Loan processedLoan = underTest.execute(loanForProcessing);
// then
verify(loanTransferabilityService).isTransferable(loanForProcessing, pendingTransfer);
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransfer.class);
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
ExternalAssetOwnerTransfer capturedPendingTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0);
ExternalAssetOwnerTransfer capturedActiveTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1);
assertEquals(actualDate, capturedPendingTransfer.getEffectiveDateTo());
assertCommonFieldsOfPendingAndActiveTransfers(capturedPendingTransfer, capturedActiveTransfer);
assertEquals(ExternalTransferStatus.DECLINED, capturedActiveTransfer.getStatus());
assertEquals(expectedSubStatus, capturedActiveTransfer.getSubStatus());
assertEquals(actualDate, capturedActiveTransfer.getSettlementDate());
assertEquals(actualDate, capturedActiveTransfer.getEffectiveDateFrom());
assertEquals(actualDate, capturedActiveTransfer.getEffectiveDateTo());
assertEquals(processedLoan, loanForProcessing);
verifyNoInteractions(accountingService);
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(1);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, savedNewTransfer);
}
@Test
public void testSaleLoanWithDelayedSettlementFromIntermediateToInvestor() {
// given
final LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L);
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(true);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
ExternalAssetOwner previousOwner = new ExternalAssetOwner();
ExternalAssetOwnerTransfer activeIntermediateTransfer = new ExternalAssetOwnerTransfer();
activeIntermediateTransfer.setOwner(previousOwner);
activeIntermediateTransfer.setStatus(ExternalTransferStatus.ACTIVE_INTERMEDIATE);
when(externalAssetOwnerTransferRepository.findOne(any(Specification.class))).thenReturn(Optional.of(activeIntermediateTransfer));
ExternalAssetOwnerTransfer pendingTransfer = new ExternalAssetOwnerTransfer();
pendingTransfer.setStatus(ExternalTransferStatus.PENDING);
List<ExternalAssetOwnerTransfer> response = List.of(pendingTransfer);
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
.thenReturn(response);
when(loanTransferabilityService.isTransferable(loanForProcessing, pendingTransfer)).thenReturn(true);
ExternalAssetOwnerTransfer savedNewTransfer = new ExternalAssetOwnerTransfer();
savedNewTransfer.setStatus(ExternalTransferStatus.ACTIVE);
when(externalAssetOwnerTransferRepository.save(any())).thenReturn(pendingTransfer).thenReturn(savedNewTransfer);
// when
final Loan processedLoan = underTest.execute(loanForProcessing);
// then
verify(loanTransferabilityService).isTransferable(loanForProcessing, pendingTransfer);
verifyNoMoreInteractions(loanTransferabilityService);
verify(externalAssetOwnerTransferRepository).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
verify(externalAssetOwnerTransferRepository).findOne(any(Specification.class));
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransfer.class);
verify(externalAssetOwnerTransferRepository, times(3)).save(externalAssetOwnerTransferArgumentCaptor.capture());
ExternalAssetOwnerTransfer capturedActiveIntermediateTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0);
ExternalAssetOwnerTransfer capturedPendingTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1);
ExternalAssetOwnerTransfer capturedActiveTransfer = externalAssetOwnerTransferArgumentCaptor.getAllValues().get(2);
assertEquals(actualDate, capturedActiveIntermediateTransfer.getEffectiveDateTo());
assertEquals(actualDate, capturedPendingTransfer.getEffectiveDateTo());
assertCommonFieldsOfPendingAndActiveTransfers(capturedPendingTransfer, capturedActiveTransfer);
assertEquals(ExternalTransferStatus.ACTIVE, capturedActiveTransfer.getStatus());
assertEquals(actualDate, capturedActiveTransfer.getSettlementDate());
assertEquals(actualDate.plusDays(1), capturedActiveTransfer.getEffectiveDateFrom());
assertEquals(FUTURE_DATE_9999_12_31, capturedActiveTransfer.getEffectiveDateTo());
ArgumentCaptor<ExternalAssetOwnerTransferLoanMapping> externalAssetOwnerTransferLoanMappingArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransferLoanMapping.class);
verify(externalAssetOwnerTransferLoanMappingRepository, times(1))
.save(externalAssetOwnerTransferLoanMappingArgumentCaptor.capture());
assertEquals(1L, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getLoanId());
assertEquals(newTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer());
assertEquals(savedNewTransfer, externalAssetOwnerTransferLoanMappingArgumentCaptor.getValue().getOwnerTransfer());
assertEquals(processedLoan, loanForProcessing);
verify(accountingService).createJournalEntriesForSaleAssetTransfer(loanForProcessing, savedNewTransfer, previousOwner);
verifyNoMoreInteractions(accountingService);
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, savedNewTransfer);
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
}
@Test
public void givenLoanSaleButBalanceIsZero() {
public void testSaleLoanWithDelayedSettlementFromIntermediateToInvestorActiveIntermediateTransferNotFound() {
// given
final LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L);
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem);
when(loanForProcessing.getLoanProduct()).thenReturn(loanProduct);
when(delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(true);
when(externalAssetOwnerTransferRepository.findOne(any(Specification.class))).thenReturn(Optional.empty());
ExternalAssetOwnerTransfer pendingTransfer = new ExternalAssetOwnerTransfer();
pendingTransfer.setStatus(ExternalTransferStatus.PENDING);
List<ExternalAssetOwnerTransfer> response = List.of(pendingTransfer);
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
.thenReturn(response);
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ZERO);
when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ZERO);
when(newTransfer.getStatus()).thenReturn(ExternalTransferStatus.DECLINED);
when(loanTransferabilityService.isTransferable(loanForProcessing, pendingTransfer)).thenReturn(true);
// when
final Loan processedLoan = underTest.execute(loanForProcessing);
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> underTest.execute(loanForProcessing));
// then
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
verify(firstResponseItem).setEffectiveDateTo(actualDate);
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
assertEquals("Expected a effective transfer of ACTIVE_INTERMEDIATE status to be present.", exception.getMessage());
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getOwner(),
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getOwner());
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getExternalId(),
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getExternalId());
assertEquals(ExternalTransferStatus.DECLINED, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus());
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSettlementDate());
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getLoanId(),
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getLoanId());
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getPurchasePriceRatio(),
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getPurchasePriceRatio());
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom());
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo());
assertEquals(processedLoan, loanForProcessing);
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(1);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer);
}
@Test
public void givenLoanSaleButBalanceIsNegative() {
// given
final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L);
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem);
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
.thenReturn(response);
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer newTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(externalAssetOwnerTransferRepository.save(any())).thenReturn(firstResponseItem).thenReturn(newTransfer);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ONE.negate());
when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ONE.negate());
when(newTransfer.getStatus()).thenReturn(ExternalTransferStatus.DECLINED);
// when
final Loan processedLoan = underTest.execute(loanForProcessing);
// then
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
verify(firstResponseItem).setEffectiveDateTo(actualDate);
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getOwner(),
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getOwner());
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getExternalId(),
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getExternalId());
assertEquals(ExternalTransferStatus.DECLINED, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus());
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSettlementDate());
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getLoanId(),
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getLoanId());
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getPurchasePriceRatio(),
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getPurchasePriceRatio());
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateFrom());
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getEffectiveDateTo());
assertEquals(processedLoan, loanForProcessing);
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(1);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, newTransfer);
verify(loanTransferabilityService).isTransferable(loanForProcessing, pendingTransfer);
verifyNoMoreInteractions(loanTransferabilityService);
verifyNoInteractions(accountingService);
verify(externalAssetOwnerTransferRepository).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
verify(externalAssetOwnerTransferRepository).findOne(any(Specification.class));
verify(externalAssetOwnerTransferRepository, never()).save(any(ExternalAssetOwnerTransfer.class));
verifyNoInteractions(externalAssetOwnerTransferLoanMappingRepository);
verifyBusinessEvents(0);
}
@Test
@@ -383,55 +551,6 @@ public class LoanAccountOwnerTransferBusinessStepTest {
assertEquals("Execute external asset owner transfer", actualEnumName);
}
@Test
public void givenLoanSaleAnsBuyBackButBalanceIsNegative() {
// given
final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
when(loanSummary.getTotalOutstanding()).thenReturn(BigDecimal.ZERO);
when(loanForProcessing.getTotalOverpaid()).thenReturn(BigDecimal.ONE);
ExternalAssetOwnerTransfer firstResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer secondResponseItem = Mockito.mock(ExternalAssetOwnerTransfer.class);
secondResponseItem.setSettlementDate(actualDate.plusDays(2));
ExternalAssetOwnerTransfer firstSaveResult = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer secondSaveResult = Mockito.mock(ExternalAssetOwnerTransfer.class);
secondSaveResult.setSettlementDate(actualDate.plusDays(2));
ExternalAssetOwnerTransfer thirdSaveResult = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer fourthSaveResult = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(firstResponseItem.getStatus()).thenReturn(ExternalTransferStatus.PENDING);
when(externalAssetOwnerTransferRepository.save(any(ExternalAssetOwnerTransfer.class))).thenReturn(firstSaveResult)
.thenReturn(secondSaveResult).thenReturn(thirdSaveResult).thenReturn(fourthSaveResult);
List<ExternalAssetOwnerTransfer> response = List.of(firstResponseItem);
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id"))))
.thenReturn(response);
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransfer.class);
// when
Loan processedLoan = underTest.execute(loanForProcessing);
// then
verify(externalAssetOwnerTransferRepository, times(1)).findAll(any(Specification.class), eq(Sort.by(Sort.Direction.ASC, "id")));
verify(firstResponseItem).setEffectiveDateTo(actualDate);
verify(externalAssetOwnerTransferRepository, times(2)).save(externalAssetOwnerTransferArgumentCaptor.capture());
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getOwner(),
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getOwner());
assertEquals(externalAssetOwnerTransferArgumentCaptor.getAllValues().get(0).getExternalId(),
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getExternalId());
assertEquals(ExternalTransferStatus.DECLINED, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getStatus());
assertEquals(ExternalTransferSubStatus.BALANCE_NEGATIVE,
externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSubStatus());
assertEquals(actualDate, externalAssetOwnerTransferArgumentCaptor.getAllValues().get(1).getSettlementDate());
assertEquals(processedLoan, loanForProcessing);
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, secondSaveResult);
}
@NotNull
private ArgumentCaptor<BusinessEvent<?>> verifyBusinessEvents(int expectedBusinessEvents) {
@SuppressWarnings("unchecked")
@@ -453,4 +572,12 @@ public class LoanAccountOwnerTransferBusinessStepTest {
assertTrue(businessEventArgumentCaptor.getAllValues().get(index) instanceof LoanAccountSnapshotBusinessEvent);
assertEquals(expectedLoan, ((LoanAccountSnapshotBusinessEvent) businessEventArgumentCaptor.getAllValues().get(index)).get());
}
private void assertCommonFieldsOfPendingAndActiveTransfers(final ExternalAssetOwnerTransfer pendingTransfer,
final ExternalAssetOwnerTransfer activeTransfer) {
assertEquals(pendingTransfer.getOwner(), activeTransfer.getOwner());
assertEquals(pendingTransfer.getExternalId(), activeTransfer.getExternalId());
assertEquals(pendingTransfer.getLoanId(), activeTransfer.getLoanId());
assertEquals(pendingTransfer.getPurchasePriceRatio(), activeTransfer.getPurchasePriceRatio());
}
}
@@ -0,0 +1,594 @@
/**
* 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.investor.service;
import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertSame;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.apache.fineract.accounting.common.AccountingConstants;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccount;
import org.apache.fineract.accounting.financialactivityaccount.domain.FinancialActivityAccountRepositoryWrapper;
import org.apache.fineract.accounting.glaccount.domain.GLAccount;
import org.apache.fineract.accounting.journalentry.domain.JournalEntry;
import org.apache.fineract.accounting.journalentry.domain.JournalEntryType;
import org.apache.fineract.infrastructure.core.domain.ExternalId;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.investor.accounting.journalentry.service.InvestorAccountingHelper;
import org.apache.fineract.investor.domain.ExternalAssetOwner;
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerJournalEntryMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMapping;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferJournalEntryMappingRepository;
import org.apache.fineract.organisation.office.domain.Office;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
class AccountingServiceImplTest {
@BeforeEach
public void setUp() {
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BUSINESS_DATE, LocalDate.of(2024, 9, 27))));
}
@Test
void createJournalEntriesForSaleAssetTransfer() {
// given
TestContext testContext = new TestContext();
Loan loan = testContext.createMockedLoan();
ExternalAssetOwner previousOwner = new ExternalAssetOwner();
ExternalAssetOwner newOwner = new ExternalAssetOwner();
ExternalAssetOwnerTransfer transfer = new ExternalAssetOwnerTransfer();
transfer.setOwner(newOwner);
JournalEntry principleAndInterestDebitJournalEntry = createJournalEntry(11, testContext.principleAndInterestAccount,
JournalEntryType.DEBIT);
JournalEntry principleAndInterestCreditJournalEntry = createJournalEntry(12, testContext.principleAndInterestAccount,
JournalEntryType.CREDIT);
JournalEntry feeAndPenaltyDebitJournalEntry = createJournalEntry(13, testContext.feeAndPenaltyAccount, JournalEntryType.DEBIT);
JournalEntry feeAndPenaltyCreditJournalEntry = createJournalEntry(14, testContext.feeAndPenaltyAccount, JournalEntryType.CREDIT);
JournalEntry transferDebitJournalEntry = createJournalEntry(15, testContext.transferAccount, JournalEntryType.DEBIT);
JournalEntry transferCreditJournalEntry = createJournalEntry(16, testContext.transferAccount, JournalEntryType.CREDIT);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(false), eq(testContext.principleAndInterestAccount))).thenReturn(principleAndInterestCreditJournalEntry);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(false), eq(testContext.feeAndPenaltyAccount))).thenReturn(feeAndPenaltyCreditJournalEntry);
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
any(), any(), eq(false))).thenReturn(transferDebitJournalEntry);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(true), eq(testContext.principleAndInterestAccount))).thenReturn(principleAndInterestDebitJournalEntry);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(true), eq(testContext.feeAndPenaltyAccount))).thenReturn(feeAndPenaltyDebitJournalEntry);
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
any(), any(), eq(true))).thenReturn(transferCreditJournalEntry);
// when
testContext.testSubject.createJournalEntriesForSaleAssetTransfer(loan, transfer, previousOwner);
// then
ArgumentCaptor<ExternalAssetOwnerJournalEntryMapping> ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerJournalEntryMapping.class);
verify(testContext.externalAssetOwnerJournalEntryMappingRepository, times(4))
.saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture());
List<ExternalAssetOwnerJournalEntryMapping> capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor
.getAllValues();
List<ExternalAssetOwnerJournalEntryMapping> expectedOwnerJournalEntryMappings = List.of(
ownerJournalEntryMapping(principleAndInterestCreditJournalEntry, previousOwner),
ownerJournalEntryMapping(feeAndPenaltyCreditJournalEntry, previousOwner),
ownerJournalEntryMapping(principleAndInterestDebitJournalEntry, newOwner),
ownerJournalEntryMapping(feeAndPenaltyDebitJournalEntry, newOwner));
assertNotNull(capturedOwnerJournalEntryMappings);
assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings);
ArgumentCaptor<ExternalAssetOwnerTransferJournalEntryMapping> transferJournalEntryMappingArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransferJournalEntryMapping.class);
verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(6))
.saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture());
List<ExternalAssetOwnerTransferJournalEntryMapping> capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor
.getAllValues();
List<ExternalAssetOwnerTransferJournalEntryMapping> expectedTransferJournalEntryMappings = List.of(
transferJournalEntryMapping(principleAndInterestCreditJournalEntry, transfer),
transferJournalEntryMapping(feeAndPenaltyCreditJournalEntry, transfer),
transferJournalEntryMapping(transferDebitJournalEntry, transfer),
transferJournalEntryMapping(principleAndInterestDebitJournalEntry, transfer),
transferJournalEntryMapping(feeAndPenaltyDebitJournalEntry, transfer),
transferJournalEntryMapping(transferCreditJournalEntry, transfer));
assertNotNull(capturedTransferJournalEntryMappings);
assertTransferJournalEntryMappings(expectedTransferJournalEntryMappings, capturedTransferJournalEntryMappings);
}
@Test
void createJournalEntriesForSaleAssetTransferOfOverpaidLoan() {
// given
TestContext testContext = new TestContext();
Loan loan = testContext.createMockedOverpaidLoan();
ExternalAssetOwner previousOwner = new ExternalAssetOwner();
ExternalAssetOwner newOwner = new ExternalAssetOwner();
ExternalAssetOwnerTransfer transfer = new ExternalAssetOwnerTransfer();
transfer.setOwner(newOwner);
JournalEntry overpaidDebitJournalEntry = createJournalEntry(11, testContext.overpaymentAccount, JournalEntryType.DEBIT);
JournalEntry overpaidCreditJournalEntry = createJournalEntry(12, testContext.overpaymentAccount, JournalEntryType.CREDIT);
JournalEntry transferDebitJournalEntry = createJournalEntry(15, testContext.transferAccount, JournalEntryType.DEBIT);
JournalEntry transferCreditJournalEntry = createJournalEntry(16, testContext.transferAccount, JournalEntryType.CREDIT);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(false), eq(testContext.overpaymentAccount))).thenReturn(overpaidCreditJournalEntry);
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
any(), any(), eq(false))).thenReturn(transferDebitJournalEntry);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(true), eq(testContext.overpaymentAccount))).thenReturn(overpaidDebitJournalEntry);
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
any(), any(), eq(true))).thenReturn(transferCreditJournalEntry);
// when
testContext.testSubject.createJournalEntriesForSaleAssetTransfer(loan, transfer, previousOwner);
// then
ArgumentCaptor<ExternalAssetOwnerJournalEntryMapping> ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerJournalEntryMapping.class);
verify(testContext.externalAssetOwnerJournalEntryMappingRepository, times(2))
.saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture());
List<ExternalAssetOwnerJournalEntryMapping> capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor
.getAllValues();
List<ExternalAssetOwnerJournalEntryMapping> expectedOwnerJournalEntryMappings = List.of(
ownerJournalEntryMapping(overpaidCreditJournalEntry, newOwner),
ownerJournalEntryMapping(overpaidDebitJournalEntry, previousOwner));
assertNotNull(capturedOwnerJournalEntryMappings);
assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings);
ArgumentCaptor<ExternalAssetOwnerTransferJournalEntryMapping> transferJournalEntryMappingArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransferJournalEntryMapping.class);
verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(4))
.saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture());
List<ExternalAssetOwnerTransferJournalEntryMapping> capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor
.getAllValues();
List<ExternalAssetOwnerTransferJournalEntryMapping> expectedTransferJournalEntryMappings = List.of(
transferJournalEntryMapping(overpaidCreditJournalEntry, transfer),
transferJournalEntryMapping(transferDebitJournalEntry, transfer),
transferJournalEntryMapping(overpaidDebitJournalEntry, transfer),
transferJournalEntryMapping(transferCreditJournalEntry, transfer));
assertNotNull(capturedTransferJournalEntryMappings);
assertTransferJournalEntryMappings(expectedTransferJournalEntryMappings, capturedTransferJournalEntryMappings);
}
@Test
void createJournalEntriesForSaleAssetTransferWithNullPreviousOwner() {
// given
TestContext testContext = new TestContext();
Loan loan = testContext.createMockedLoan();
ExternalAssetOwner newOwner = new ExternalAssetOwner();
ExternalAssetOwnerTransfer transfer = new ExternalAssetOwnerTransfer();
transfer.setOwner(newOwner);
JournalEntry principleAndInterestDebitJournalEntry = createJournalEntry(11, testContext.principleAndInterestAccount,
JournalEntryType.DEBIT);
JournalEntry principleAndInterestCreditJournalEntry = createJournalEntry(12, testContext.principleAndInterestAccount,
JournalEntryType.CREDIT);
JournalEntry feeAndPenaltyDebitJournalEntry = createJournalEntry(13, testContext.feeAndPenaltyAccount, JournalEntryType.DEBIT);
JournalEntry feeAndPenaltyCreditJournalEntry = createJournalEntry(14, testContext.feeAndPenaltyAccount, JournalEntryType.CREDIT);
JournalEntry transferDebitJournalEntry = createJournalEntry(15, testContext.transferAccount, JournalEntryType.DEBIT);
JournalEntry transferCreditJournalEntry = createJournalEntry(16, testContext.transferAccount, JournalEntryType.CREDIT);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(false), eq(testContext.principleAndInterestAccount))).thenReturn(principleAndInterestCreditJournalEntry);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(false), eq(testContext.feeAndPenaltyAccount))).thenReturn(feeAndPenaltyCreditJournalEntry);
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
any(), any(), eq(false))).thenReturn(transferDebitJournalEntry);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(true), eq(testContext.principleAndInterestAccount))).thenReturn(principleAndInterestDebitJournalEntry);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(true), eq(testContext.feeAndPenaltyAccount))).thenReturn(feeAndPenaltyDebitJournalEntry);
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
any(), any(), eq(true))).thenReturn(transferCreditJournalEntry);
// when
testContext.testSubject.createJournalEntriesForSaleAssetTransfer(loan, transfer, null);
// then
ArgumentCaptor<ExternalAssetOwnerJournalEntryMapping> ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerJournalEntryMapping.class);
verify(testContext.externalAssetOwnerJournalEntryMappingRepository, times(2))
.saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture());
List<ExternalAssetOwnerJournalEntryMapping> capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor
.getAllValues();
List<ExternalAssetOwnerJournalEntryMapping> expectedOwnerJournalEntryMappings = List.of(
ownerJournalEntryMapping(principleAndInterestDebitJournalEntry, newOwner),
ownerJournalEntryMapping(feeAndPenaltyDebitJournalEntry, newOwner));
assertNotNull(capturedOwnerJournalEntryMappings);
assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings);
ArgumentCaptor<ExternalAssetOwnerTransferJournalEntryMapping> transferJournalEntryMappingArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransferJournalEntryMapping.class);
verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(6))
.saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture());
List<ExternalAssetOwnerTransferJournalEntryMapping> capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor
.getAllValues();
List<ExternalAssetOwnerTransferJournalEntryMapping> expectedTransferJournalEntryMappings = List.of(
transferJournalEntryMapping(principleAndInterestCreditJournalEntry, transfer),
transferJournalEntryMapping(feeAndPenaltyCreditJournalEntry, transfer),
transferJournalEntryMapping(transferDebitJournalEntry, transfer),
transferJournalEntryMapping(principleAndInterestDebitJournalEntry, transfer),
transferJournalEntryMapping(feeAndPenaltyDebitJournalEntry, transfer),
transferJournalEntryMapping(transferCreditJournalEntry, transfer));
assertNotNull(capturedTransferJournalEntryMappings);
assertTransferJournalEntryMappings(expectedTransferJournalEntryMappings, capturedTransferJournalEntryMappings);
}
@Test
void createJournalEntriesForSaleAssetTransferOfOverpaidLoanWithNullPreviousOwner() {
// given
TestContext testContext = new TestContext();
Loan loan = testContext.createMockedOverpaidLoan();
ExternalAssetOwner newOwner = new ExternalAssetOwner();
ExternalAssetOwnerTransfer transfer = new ExternalAssetOwnerTransfer();
transfer.setOwner(newOwner);
JournalEntry overpaidDebitJournalEntry = createJournalEntry(11, testContext.overpaymentAccount, JournalEntryType.DEBIT);
JournalEntry overpaidCreditJournalEntry = createJournalEntry(12, testContext.overpaymentAccount, JournalEntryType.CREDIT);
JournalEntry transferDebitJournalEntry = createJournalEntry(15, testContext.transferAccount, JournalEntryType.DEBIT);
JournalEntry transferCreditJournalEntry = createJournalEntry(16, testContext.transferAccount, JournalEntryType.CREDIT);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(false), eq(testContext.overpaymentAccount))).thenReturn(overpaidCreditJournalEntry);
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
any(), any(), eq(false))).thenReturn(transferDebitJournalEntry);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(true), eq(testContext.overpaymentAccount))).thenReturn(overpaidDebitJournalEntry);
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
any(), any(), eq(true))).thenReturn(transferCreditJournalEntry);
// when
testContext.testSubject.createJournalEntriesForSaleAssetTransfer(loan, transfer, null);
// then
ArgumentCaptor<ExternalAssetOwnerJournalEntryMapping> ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerJournalEntryMapping.class);
verify(testContext.externalAssetOwnerJournalEntryMappingRepository).saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture());
List<ExternalAssetOwnerJournalEntryMapping> capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor
.getAllValues();
List<ExternalAssetOwnerJournalEntryMapping> expectedOwnerJournalEntryMappings = List
.of(ownerJournalEntryMapping(overpaidCreditJournalEntry, newOwner));
assertNotNull(capturedOwnerJournalEntryMappings);
assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings);
ArgumentCaptor<ExternalAssetOwnerTransferJournalEntryMapping> transferJournalEntryMappingArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransferJournalEntryMapping.class);
verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(4))
.saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture());
List<ExternalAssetOwnerTransferJournalEntryMapping> capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor
.getAllValues();
List<ExternalAssetOwnerTransferJournalEntryMapping> expectedTransferJournalEntryMappings = List.of(
transferJournalEntryMapping(overpaidCreditJournalEntry, transfer),
transferJournalEntryMapping(transferDebitJournalEntry, transfer),
transferJournalEntryMapping(overpaidDebitJournalEntry, transfer),
transferJournalEntryMapping(transferCreditJournalEntry, transfer));
assertNotNull(capturedTransferJournalEntryMappings);
assertTransferJournalEntryMappings(expectedTransferJournalEntryMappings, capturedTransferJournalEntryMappings);
}
@Test
void createJournalEntriesForBuybackAssetTransfer() {
// given
TestContext testContext = new TestContext();
Loan loan = testContext.createMockedLoan();
ExternalAssetOwner previousOwner = new ExternalAssetOwner();
ExternalAssetOwnerTransfer transfer = new ExternalAssetOwnerTransfer();
transfer.setOwner(previousOwner);
JournalEntry principleAndInterestDebitJournalEntry = createJournalEntry(11, testContext.principleAndInterestAccount,
JournalEntryType.DEBIT);
JournalEntry principleAndInterestCreditJournalEntry = createJournalEntry(12, testContext.principleAndInterestAccount,
JournalEntryType.CREDIT);
JournalEntry feeAndPenaltyDebitJournalEntry = createJournalEntry(13, testContext.feeAndPenaltyAccount, JournalEntryType.DEBIT);
JournalEntry feeAndPenaltyCreditJournalEntry = createJournalEntry(14, testContext.feeAndPenaltyAccount, JournalEntryType.CREDIT);
JournalEntry transferDebitJournalEntry = createJournalEntry(15, testContext.transferAccount, JournalEntryType.DEBIT);
JournalEntry transferCreditJournalEntry = createJournalEntry(16, testContext.transferAccount, JournalEntryType.CREDIT);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(false), eq(testContext.principleAndInterestAccount))).thenReturn(principleAndInterestCreditJournalEntry);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(false), eq(testContext.feeAndPenaltyAccount))).thenReturn(feeAndPenaltyCreditJournalEntry);
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
any(), any(), eq(false))).thenReturn(transferDebitJournalEntry);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(true), eq(testContext.principleAndInterestAccount))).thenReturn(principleAndInterestDebitJournalEntry);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(true), eq(testContext.feeAndPenaltyAccount))).thenReturn(feeAndPenaltyDebitJournalEntry);
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
any(), any(), eq(true))).thenReturn(transferCreditJournalEntry);
// when
testContext.testSubject.createJournalEntriesForBuybackAssetTransfer(loan, transfer);
// then
ArgumentCaptor<ExternalAssetOwnerJournalEntryMapping> ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerJournalEntryMapping.class);
verify(testContext.externalAssetOwnerJournalEntryMappingRepository, times(2))
.saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture());
List<ExternalAssetOwnerJournalEntryMapping> capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor
.getAllValues();
List<ExternalAssetOwnerJournalEntryMapping> expectedOwnerJournalEntryMappings = List.of(
ownerJournalEntryMapping(principleAndInterestCreditJournalEntry, previousOwner),
ownerJournalEntryMapping(feeAndPenaltyCreditJournalEntry, previousOwner));
assertNotNull(capturedOwnerJournalEntryMappings);
assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings);
ArgumentCaptor<ExternalAssetOwnerTransferJournalEntryMapping> transferJournalEntryMappingArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransferJournalEntryMapping.class);
verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(6))
.saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture());
List<ExternalAssetOwnerTransferJournalEntryMapping> capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor
.getAllValues();
List<ExternalAssetOwnerTransferJournalEntryMapping> expectedTransferJournalEntryMappings = List.of(
transferJournalEntryMapping(principleAndInterestDebitJournalEntry, transfer),
transferJournalEntryMapping(feeAndPenaltyDebitJournalEntry, transfer),
transferJournalEntryMapping(transferCreditJournalEntry, transfer),
transferJournalEntryMapping(principleAndInterestCreditJournalEntry, transfer),
transferJournalEntryMapping(feeAndPenaltyCreditJournalEntry, transfer),
transferJournalEntryMapping(transferDebitJournalEntry, transfer));
assertNotNull(capturedTransferJournalEntryMappings);
assertTransferJournalEntryMappings(expectedTransferJournalEntryMappings, capturedTransferJournalEntryMappings);
}
@Test
void createJournalEntriesForBuybackAssetTransferOfOverpaidLoan() {
// given
TestContext testContext = new TestContext();
Loan loan = testContext.createMockedOverpaidLoan();
ExternalAssetOwner previousOwner = new ExternalAssetOwner();
ExternalAssetOwnerTransfer transfer = new ExternalAssetOwnerTransfer();
transfer.setOwner(previousOwner);
JournalEntry overpaidDebitJournalEntry = createJournalEntry(11, testContext.overpaymentAccount, JournalEntryType.DEBIT);
JournalEntry overpaidCreditJournalEntry = createJournalEntry(12, testContext.overpaymentAccount, JournalEntryType.CREDIT);
JournalEntry transferDebitJournalEntry = createJournalEntry(15, testContext.transferAccount, JournalEntryType.DEBIT);
JournalEntry transferCreditJournalEntry = createJournalEntry(16, testContext.transferAccount, JournalEntryType.CREDIT);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(false), eq(testContext.overpaymentAccount))).thenReturn(overpaidCreditJournalEntry);
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
any(), any(), eq(false))).thenReturn(transferDebitJournalEntry);
when(testContext.investorAccountingHelper.createCreditJournalEntryOrReversalForInvestor(any(), any(), any(), any(), any(), any(),
eq(true), eq(testContext.overpaymentAccount))).thenReturn(overpaidDebitJournalEntry);
when(testContext.investorAccountingHelper.createDebitJournalEntryOrReversalForInvestor(any(), any(), anyInt(), any(), any(), any(),
any(), any(), eq(true))).thenReturn(transferCreditJournalEntry);
// when
testContext.testSubject.createJournalEntriesForBuybackAssetTransfer(loan, transfer);
// then
ArgumentCaptor<ExternalAssetOwnerJournalEntryMapping> ownerJournalEntryMappingArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerJournalEntryMapping.class);
verify(testContext.externalAssetOwnerJournalEntryMappingRepository, times(1))
.saveAndFlush(ownerJournalEntryMappingArgumentCaptor.capture());
List<ExternalAssetOwnerJournalEntryMapping> capturedOwnerJournalEntryMappings = ownerJournalEntryMappingArgumentCaptor
.getAllValues();
List<ExternalAssetOwnerJournalEntryMapping> expectedOwnerJournalEntryMappings = List
.of(ownerJournalEntryMapping(overpaidDebitJournalEntry, previousOwner));
assertNotNull(capturedOwnerJournalEntryMappings);
assertOwnerJournalEntryMappings(expectedOwnerJournalEntryMappings, capturedOwnerJournalEntryMappings);
ArgumentCaptor<ExternalAssetOwnerTransferJournalEntryMapping> transferJournalEntryMappingArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransferJournalEntryMapping.class);
verify(testContext.externalAssetOwnerTransferJournalEntryMappingRepository, times(4))
.saveAndFlush(transferJournalEntryMappingArgumentCaptor.capture());
List<ExternalAssetOwnerTransferJournalEntryMapping> capturedTransferJournalEntryMappings = transferJournalEntryMappingArgumentCaptor
.getAllValues();
List<ExternalAssetOwnerTransferJournalEntryMapping> expectedTransferJournalEntryMappings = List.of(
transferJournalEntryMapping(overpaidDebitJournalEntry, transfer),
transferJournalEntryMapping(transferCreditJournalEntry, transfer),
transferJournalEntryMapping(overpaidCreditJournalEntry, transfer),
transferJournalEntryMapping(transferDebitJournalEntry, transfer));
assertNotNull(capturedTransferJournalEntryMappings);
assertTransferJournalEntryMappings(expectedTransferJournalEntryMappings, capturedTransferJournalEntryMappings);
}
private JournalEntry createJournalEntry(long id, GLAccount glAccount, JournalEntryType journalEntryType) {
JournalEntry journalEntry = JournalEntry.createNew(null, null, glAccount, null, null, false, null, journalEntryType, null, null,
null, null, null, null, null, null, null);
journalEntry.setId(id);
return journalEntry;
}
private ExternalAssetOwnerJournalEntryMapping ownerJournalEntryMapping(JournalEntry journalEntry, ExternalAssetOwner owner) {
ExternalAssetOwnerJournalEntryMapping ownerJournalEntryMapping = new ExternalAssetOwnerJournalEntryMapping();
ownerJournalEntryMapping.setJournalEntry(journalEntry);
ownerJournalEntryMapping.setOwner(owner);
return ownerJournalEntryMapping;
}
private void assertOwnerJournalEntryMappings(List<ExternalAssetOwnerJournalEntryMapping> expectedOwnerJournalEntryMappings,
List<ExternalAssetOwnerJournalEntryMapping> actualOwnerJournalEntryMappings) {
assertEquals(expectedOwnerJournalEntryMappings.size(), actualOwnerJournalEntryMappings.size());
for (int i = 0; i < expectedOwnerJournalEntryMappings.size(); i++) {
assertOwnerJournalEntryMapping(expectedOwnerJournalEntryMappings.get(i), actualOwnerJournalEntryMappings.get(i));
}
}
private void assertOwnerJournalEntryMapping(ExternalAssetOwnerJournalEntryMapping expectedOwnerJournalEntryMapping,
ExternalAssetOwnerJournalEntryMapping actualOwnerJournalEntryMapping) {
assertNotNull(actualOwnerJournalEntryMapping);
assertSame(expectedOwnerJournalEntryMapping.getJournalEntry(), actualOwnerJournalEntryMapping.getJournalEntry());
assertSame(expectedOwnerJournalEntryMapping.getOwner(), actualOwnerJournalEntryMapping.getOwner());
}
private ExternalAssetOwnerTransferJournalEntryMapping transferJournalEntryMapping(JournalEntry journalEntry,
ExternalAssetOwnerTransfer transfer) {
ExternalAssetOwnerTransferJournalEntryMapping transferJournalEntryMapping = new ExternalAssetOwnerTransferJournalEntryMapping();
transferJournalEntryMapping.setJournalEntry(journalEntry);
transferJournalEntryMapping.setOwnerTransfer(transfer);
return transferJournalEntryMapping;
}
private void assertTransferJournalEntryMappings(
List<ExternalAssetOwnerTransferJournalEntryMapping> expectedTransferJournalEntryMappings,
List<ExternalAssetOwnerTransferJournalEntryMapping> actualTransferJournalEntryMappings) {
assertEquals(expectedTransferJournalEntryMappings.size(), actualTransferJournalEntryMappings.size());
for (int i = 0; i < expectedTransferJournalEntryMappings.size(); i++) {
assertTransferJournalEntryMapping(expectedTransferJournalEntryMappings.get(i), actualTransferJournalEntryMappings.get(i));
}
}
private void assertTransferJournalEntryMapping(ExternalAssetOwnerTransferJournalEntryMapping expectedTransferJournalEntryMapping,
ExternalAssetOwnerTransferJournalEntryMapping actualTransferJournalEntryMapping) {
assertNotNull(actualTransferJournalEntryMapping);
assertSame(expectedTransferJournalEntryMapping.getJournalEntry(), actualTransferJournalEntryMapping.getJournalEntry());
assertSame(expectedTransferJournalEntryMapping.getOwnerTransfer(), actualTransferJournalEntryMapping.getOwnerTransfer());
}
private static class TestContext {
private static final Long LOAN_ID = 3001L;
private static final Long LOAN_PRODUCT_ID = 3002L;
private static final Long TRANSFER_ID = 3003L;
@Mock
private InvestorAccountingHelper investorAccountingHelper;
@Mock
private ExternalAssetOwnerTransferJournalEntryMappingRepository externalAssetOwnerTransferJournalEntryMappingRepository;
@Mock
private ExternalAssetOwnerJournalEntryMappingRepository externalAssetOwnerJournalEntryMappingRepository;
@Mock
private FinancialActivityAccountRepositoryWrapper financialActivityAccountRepository;
@InjectMocks
private AccountingServiceImpl testSubject;
private final GLAccount principleAndInterestAccount = new GLAccount();
private final GLAccount feeAndPenaltyAccount = new GLAccount();
private final GLAccount transferAccount = new GLAccount();
private final GLAccount overpaymentAccount = new GLAccount();
TestContext() {
MockitoAnnotations.openMocks(this);
setupAccounts();
}
public Loan createMockedLoan() {
Loan loan = mock(Loan.class);
when(loan.getId()).thenReturn(LOAN_ID);
when(loan.productId()).thenReturn(LOAN_PRODUCT_ID);
Office office = Office.headOffice("office", LocalDate.of(2024, 9, 27), new ExternalId("officeId"));
when(loan.getOffice()).thenReturn(office);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanSummary.getTotalPrincipalOutstanding()).thenReturn(BigDecimal.ONE);
when(loanSummary.getTotalInterestOutstanding()).thenReturn(BigDecimal.ONE);
when(loanSummary.getTotalFeeChargesOutstanding()).thenReturn(BigDecimal.ONE);
when(loanSummary.getTotalPenaltyChargesOutstanding()).thenReturn(BigDecimal.ONE);
when(loan.getSummary()).thenReturn(loanSummary);
return loan;
}
public Loan createMockedOverpaidLoan() {
Loan loan = mock(Loan.class);
when(loan.getId()).thenReturn(LOAN_ID);
when(loan.productId()).thenReturn(LOAN_PRODUCT_ID);
when(loan.getStatus()).thenReturn(LoanStatus.OVERPAID);
Office office = Office.headOffice("office", LocalDate.of(2024, 9, 27), new ExternalId("officeId"));
when(loan.getOffice()).thenReturn(office);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loan.getSummary()).thenReturn(loanSummary);
when(loan.getTotalOverpaid()).thenReturn(BigDecimal.ONE);
return loan;
}
private void setupAccounts() {
principleAndInterestAccount.setId(1L);
feeAndPenaltyAccount.setId(2L);
transferAccount.setId(3L);
overpaymentAccount.setId(4L);
FinancialActivityAccount financialActivityAccount = new FinancialActivityAccount(transferAccount,
AccountingConstants.FinancialActivity.ASSET_TRANSFER.getValue());
when(financialActivityAccountRepository
.findByFinancialActivityTypeWithNotFoundDetection(AccountingConstants.FinancialActivity.ASSET_TRANSFER.getValue()))
.thenReturn(financialActivityAccount);
lenient().when(investorAccountingHelper.getLinkedGLAccountForLoanProduct(LOAN_PRODUCT_ID,
AccountingConstants.AccrualAccountsForLoan.LOAN_PORTFOLIO.getValue())).thenReturn(principleAndInterestAccount);
lenient()
.when(investorAccountingHelper.getLinkedGLAccountForLoanProduct(LOAN_PRODUCT_ID,
AccountingConstants.AccrualAccountsForLoan.INTEREST_RECEIVABLE.getValue()))
.thenReturn(principleAndInterestAccount);
lenient().when(investorAccountingHelper.getLinkedGLAccountForLoanProduct(LOAN_PRODUCT_ID,
AccountingConstants.AccrualAccountsForLoan.FEES_RECEIVABLE.getValue())).thenReturn(feeAndPenaltyAccount);
lenient().when(investorAccountingHelper.getLinkedGLAccountForLoanProduct(LOAN_PRODUCT_ID,
AccountingConstants.AccrualAccountsForLoan.PENALTIES_RECEIVABLE.getValue())).thenReturn(feeAndPenaltyAccount);
lenient().when(investorAccountingHelper.getLinkedGLAccountForLoanProduct(LOAN_PRODUCT_ID,
AccountingConstants.AccrualAccountsForLoan.OVERPAYMENT.getValue())).thenReturn(overpaymentAccount);
}
}
}
@@ -0,0 +1,80 @@
/**
* 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.investor.service;
import static org.apache.fineract.investor.data.attribute.SettlementModelExternalAssetOwnerLoanProductAttribute.DEFAULT_SETTLEMENT;
import static org.apache.fineract.investor.data.attribute.SettlementModelExternalAssetOwnerLoanProductAttribute.DELAYED_SETTLEMENT;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.mockito.Mockito.when;
import java.util.List;
import java.util.stream.Stream;
import org.apache.fineract.infrastructure.core.service.Page;
import org.apache.fineract.investor.data.ExternalTransferLoanProductAttributesData;
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.MockitoAnnotations;
public class DelayedSettlementAttributeServiceImplTest {
private static final Long LOAN_PRODUCT_ID = 1L;
private static Stream<Arguments> attributesDataProvider() {
ExternalTransferLoanProductAttributesData enabledAttributesData = new ExternalTransferLoanProductAttributesData();
enabledAttributesData.setAttributeValue(DELAYED_SETTLEMENT.getAttributeValue());
ExternalTransferLoanProductAttributesData disabledAttributesData = new ExternalTransferLoanProductAttributesData();
disabledAttributesData.setAttributeValue(DEFAULT_SETTLEMENT.getAttributeValue());
return Stream.of(Arguments.of(new Page(List.of(enabledAttributesData), 1), true),
Arguments.of(new Page(List.of(disabledAttributesData), 1), false), Arguments.of(new Page(List.of(), 0), false));
}
@ParameterizedTest
@MethodSource("attributesDataProvider")
void isEnabled(final Page<ExternalTransferLoanProductAttributesData> attributesDataPage, final boolean expectedResult) {
// given
TestContext testContext = new TestContext();
when(testContext.externalAssetOwnerLoanProductAttributesReadService.retrieveAllLoanProductAttributesByLoanProductId(LOAN_PRODUCT_ID,
DELAYED_SETTLEMENT.getAttributeKey())).thenReturn(attributesDataPage);
// when
boolean result = testContext.testSubject.isEnabled(LOAN_PRODUCT_ID);
// then
assertEquals(expectedResult, result);
}
private static class TestContext {
@Mock
private ExternalAssetOwnerLoanProductAttributesReadService externalAssetOwnerLoanProductAttributesReadService;
@InjectMocks
private DelayedSettlementAttributeServiceImpl testSubject;
TestContext() {
MockitoAnnotations.openMocks(this);
}
}
}
@@ -0,0 +1,932 @@
/**
* 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.investor.service;
import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.lenient;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyNoInteractions;
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
import com.google.gson.JsonElement;
import edu.umd.cs.findbugs.annotations.SuppressFBWarnings;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.ZoneId;
import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import org.apache.commons.lang3.RandomStringUtils;
import org.apache.fineract.cob.data.LoanDataForExternalTransfer;
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.PlatformApiDataValidationException;
import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.investor.data.ExternalTransferRequestParameters;
import org.apache.fineract.investor.data.ExternalTransferStatus;
import org.apache.fineract.investor.domain.ExternalAssetOwner;
import org.apache.fineract.investor.domain.ExternalAssetOwnerRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
import org.apache.fineract.investor.exception.ExternalAssetOwnerInitiateTransferException;
import org.apache.fineract.portfolio.loanaccount.domain.LoanRepository;
import org.apache.fineract.portfolio.loanaccount.domain.LoanStatus;
import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException;
import org.junit.Assert;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
import org.springframework.data.jpa.domain.Specification;
@ExtendWith(MockitoExtension.class)
public class ExternalAssetOwnersWriteServiceTest {
final LocalDate actualDate = LocalDate.now(ZoneId.systemDefault());
private static final LocalDate FUTURE_DATE_9999_12_31 = LocalDate.of(9999, 12, 31);
@BeforeEach
public void setUp() {
ThreadLocalContextUtil.setBusinessDates(new HashMap<>(Map.of(BUSINESS_DATE, actualDate)));
}
@Test
public void testIntermediarySaleLoanByLoanIdHappyPath() {
TestContext testContext = new TestContext();
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransfer.class);
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
when(testContext.externalAssetOwnerRepository.findByExternalId(any(ExternalId.class)))
.thenReturn(Optional.of(testContext.externalAssetOwner));
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
// when
testContext.externalAssetOwnersWriteServiceImpl.intermediarySaleLoanByLoanId(command);
// then
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(externalAssetOwnerTransferArgumentCaptor.capture());
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
assertAssertOwnerTransferValues(testContext, externalAssetOwnerTransferArgumentCaptor.getValue(),
ExternalTransferStatus.PENDING_INTERMEDIATE);
}
@Test
public void testIntermediarySaleLoanByLoanIdDelayedSettlementIsNotEnabled() {
TestContext testContext = new TestContext();
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(false);
// when
ExternalAssetOwnerInitiateTransferException thrownException = Assert.assertThrows(ExternalAssetOwnerInitiateTransferException.class,
() -> testContext.externalAssetOwnersWriteServiceImpl.intermediarySaleLoanByLoanId(command));
// then
verify(testContext.externalAssetOwnerTransferRepository, times(0)).saveAndFlush(any(ExternalAssetOwnerTransfer.class));
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
Assertions.assertEquals(thrownException.getMessage(), "Delayed Settlement Configuration is not enabled for the loan product: "
+ testContext.loanDataForExternalTransfer.getLoanProductShortName());
}
@ParameterizedTest
@MethodSource("effectiveTransferDataProviderIntermediarySaleTests")
public void testValidateEffectiveTransferForIntermediarySale(final String testName,
final List<ExternalAssetOwnerTransfer> externalAssetOwnerTransferList, final String expectedErrorString) {
TestContext testContext = new TestContext();
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
any(LocalDate.class))).thenReturn(externalAssetOwnerTransferList);
// when
ExternalAssetOwnerInitiateTransferException thrownException = Assert.assertThrows(ExternalAssetOwnerInitiateTransferException.class,
() -> testContext.externalAssetOwnersWriteServiceImpl.intermediarySaleLoanByLoanId(command));
// then
verify(testContext.externalAssetOwnerTransferRepository, times(0)).saveAndFlush(any(ExternalAssetOwnerTransfer.class));
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
Assertions.assertEquals(thrownException.getMessage(), expectedErrorString);
}
@ParameterizedTest
@MethodSource("loanStatusValidationDataProviderValidActive")
public void testValidateValidActiveLoanStatus(final String testName, final LoanStatus loanStatus) {
TestContext testContext = new TestContext();
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransfer.class);
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
when(testContext.externalAssetOwnerRepository.findByExternalId(any(ExternalId.class)))
.thenReturn(Optional.of(testContext.externalAssetOwner));
when(testContext.loanDataForExternalTransfer.getLoanStatus()).thenReturn(loanStatus.getValue());
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
// when
testContext.externalAssetOwnersWriteServiceImpl.intermediarySaleLoanByLoanId(command);
// then
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(externalAssetOwnerTransferArgumentCaptor.capture());
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
assertAssertOwnerTransferValues(testContext, externalAssetOwnerTransferArgumentCaptor.getValue(),
ExternalTransferStatus.PENDING_INTERMEDIATE);
}
@ParameterizedTest
@MethodSource("loanStatusValidationDataProviderInvalidActive")
public void testValidateInvalidActiveLoanStatus(final String testName, final LoanStatus loanStatus) {
final TestContext testContext = new TestContext();
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.loanDataForExternalTransfer.getLoanStatus()).thenReturn(loanStatus.getValue());
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
// when
ExternalAssetOwnerInitiateTransferException exception = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
() -> testContext.externalAssetOwnersWriteServiceImpl.intermediarySaleLoanByLoanId(command));
assertEquals(exception.getMessage(), String.format("Loan status %s is not valid for transfer.", loanStatus.name()));
// then
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
verify(testContext.loanRepository, times(1)).findLoanDataForExternalTransferByLoanId(testContext.loanId);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
verify(testContext.externalAssetOwnerTransferRepository, times(1)).exists(any(Specification.class));
verifyNoInteractions(testContext.externalAssetOwnerRepository);
}
@ParameterizedTest
@MethodSource("loanStatusValidationDataProviderValidDelayedSettlement")
public void testValidateValidDelayedSettlementLoanStatus(final String testName, final LoanStatus loanStatus) {
final ExternalAssetOwnerTransfer effectiveTransferForDelayedSettlement = new ExternalAssetOwnerTransfer();
effectiveTransferForDelayedSettlement.setStatus(ExternalTransferStatus.ACTIVE_INTERMEDIATE);
List<ExternalAssetOwnerTransfer> assetOwnerTransfers = new ArrayList<>();
assetOwnerTransfers.add(effectiveTransferForDelayedSettlement);
final TestContext testContext = new TestContext();
final ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransfer.class);
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.loanDataForExternalTransfer.getLoanStatus()).thenReturn(loanStatus.getValue());
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
any(LocalDate.class))).thenReturn(assetOwnerTransfers);
// when
CommandProcessingResult result = testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command);
// then
verify(testContext.externalAssetOwnerRepository).findByExternalId(any(ExternalId.class));
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(externalAssetOwnerTransferArgumentCaptor.capture());
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
verify(testContext.externalAssetOwnerTransferRepository).findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
any(LocalDate.class));
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
ExternalAssetOwnerTransfer savedTransfer = externalAssetOwnerTransferArgumentCaptor.getValue();
assertAssertOwnerTransferValues(testContext, savedTransfer, ExternalTransferStatus.PENDING);
assertEquals(savedTransfer.getId(), result.getResourceId());
assertEquals(savedTransfer.getExternalId(), result.getResourceExternalId());
assertEquals(savedTransfer.getLoanId(), result.getSubResourceId());
assertEquals(savedTransfer.getExternalLoanId(), result.getSubResourceExternalId());
}
@ParameterizedTest
@MethodSource("loanStatusValidationDataProviderInvalidDelayedSettlement")
public void testValidateInvalidDelayedSettlementLoanStatus(final String testName, final LoanStatus loanStatus) {
final TestContext testContext = new TestContext();
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.loanDataForExternalTransfer.getLoanStatus()).thenReturn(loanStatus.getValue());
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
// when
ExternalAssetOwnerInitiateTransferException exception = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
assertEquals(exception.getMessage(), String.format("Loan status %s is not valid for transfer.", loanStatus.name()));
// then
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
verify(testContext.loanRepository, times(1)).findLoanDataForExternalTransferByLoanId(testContext.loanId);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
verify(testContext.externalAssetOwnerTransferRepository, times(1)).exists(any(Specification.class));
verifyNoInteractions(testContext.externalAssetOwnerRepository);
}
@ParameterizedTest
@MethodSource("effectiveTransferDataProvider")
public void verifyWhenLoanSaleIsInitiatedThenAssetOwnerTransferIsCreated(
final List<ExternalAssetOwnerTransfer> externalAssetOwnerTransferList, final boolean isDelayedSettlementEnabled) {
final TestContext testContext = new TestContext();
final ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransfer.class);
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(isDelayedSettlementEnabled);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
any(LocalDate.class))).thenReturn(externalAssetOwnerTransferList);
// when
CommandProcessingResult result = testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command);
// then
verify(testContext.externalAssetOwnerRepository).findByExternalId(any(ExternalId.class));
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(externalAssetOwnerTransferArgumentCaptor.capture());
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
verify(testContext.externalAssetOwnerTransferRepository).findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
any(LocalDate.class));
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
ExternalAssetOwnerTransfer savedTransfer = externalAssetOwnerTransferArgumentCaptor.getValue();
assertAssertOwnerTransferValues(testContext, savedTransfer, ExternalTransferStatus.PENDING);
assertEquals(savedTransfer.getId(), result.getResourceId());
assertEquals(savedTransfer.getExternalId(), result.getResourceExternalId());
assertEquals(savedTransfer.getLoanId(), result.getSubResourceId());
assertEquals(savedTransfer.getExternalLoanId(), result.getSubResourceExternalId());
}
@Test
public void validateSettlementDateInThePastTest() {
TestContext testContext = new TestContext();
final JsonElement jsonCommandElement = testContext.fromJsonHelper.parse(testContext.jsonCommand);
ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransfer.class);
lenient().when(
testContext.fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, jsonCommandElement))
.thenReturn(LocalDate.EPOCH);
lenient().when(testContext.fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE,
jsonCommandElement, testContext.DATE_FORMAT, Locale.GERMANY)).thenReturn(LocalDate.EPOCH);
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
// when
ExternalAssetOwnerInitiateTransferException thrownException = Assert.assertThrows(ExternalAssetOwnerInitiateTransferException.class,
() -> testContext.externalAssetOwnersWriteServiceImpl.intermediarySaleLoanByLoanId(command));
// then
verify(testContext.externalAssetOwnerTransferRepository, times(0)).saveAndFlush(externalAssetOwnerTransferArgumentCaptor.capture());
verify(testContext.externalAssetOwnerRepository, times(0)).saveAndFlush(any(ExternalAssetOwner.class));
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
Assertions.assertEquals(thrownException.getMessage(), "Settlement date cannot be in the past");
}
private static Stream<Arguments> effectiveTransferDataProvider() {
final ExternalAssetOwnerTransfer effectiveTransferForDelayedSettlement = new ExternalAssetOwnerTransfer();
effectiveTransferForDelayedSettlement.setStatus(ExternalTransferStatus.ACTIVE_INTERMEDIATE);
return Stream.of(Arguments.of(Collections.emptyList(), false), Arguments.of(List.of(effectiveTransferForDelayedSettlement), true));
}
private static Stream<Arguments> effectiveTransferDataProviderIntermediarySaleTests() {
final ExternalAssetOwnerTransfer activeIntermediate = new ExternalAssetOwnerTransfer();
activeIntermediate.setStatus(ExternalTransferStatus.ACTIVE_INTERMEDIATE);
final ExternalAssetOwnerTransfer active = new ExternalAssetOwnerTransfer();
active.setStatus(ExternalTransferStatus.ACTIVE);
final ExternalAssetOwnerTransfer pendingIntermediate = new ExternalAssetOwnerTransfer();
pendingIntermediate.setStatus(ExternalTransferStatus.PENDING_INTERMEDIATE);
return Stream.of(
Arguments.of("Incorrect State", List.of(activeIntermediate),
String.format("This loan cannot be sold, because it is incorrect state! (transferId = %s)",
activeIntermediate.getId())),
Arguments.of("Already In Progress", List.of(activeIntermediate, active),
"This loan cannot be sold, there is already an in progress transfer"),
Arguments.of("Already Pending Intermediary", List.of(pendingIntermediate),
"External asset owner transfer is already in PENDING_INTERMEDIATE state for this loan"),
Arguments.of("Already Owned by External Asset Owner", List.of(active),
"This loan cannot be sold, because it is owned by an external asset owner"));
}
private static Stream<Arguments> loanStatusValidationDataProviderValidActive() {
return Stream.of(Arguments.of("Active Loan Status", LoanStatus.ACTIVE),
Arguments.of("Transfer In Progress Loan Status", LoanStatus.TRANSFER_IN_PROGRESS),
Arguments.of("Transfer On Hold Loan Status", LoanStatus.TRANSFER_ON_HOLD));
}
private static Stream<Arguments> loanStatusValidationDataProviderValidDelayedSettlement() {
return Stream.of(Arguments.of("Active Loan Status", LoanStatus.ACTIVE),
Arguments.of("Transfer On Hold Loan Status", LoanStatus.TRANSFER_ON_HOLD),
Arguments.of("Transfer In Progress Status", LoanStatus.TRANSFER_IN_PROGRESS),
Arguments.of("Overpaid Loan Status", LoanStatus.OVERPAID),
Arguments.of("Closed Obligations Met Loan Status", LoanStatus.CLOSED_OBLIGATIONS_MET));
}
private static Stream<Arguments> loanStatusValidationDataProviderInvalidActive() {
return Stream.of(Arguments.of("Invalid Loan Status", LoanStatus.INVALID), Arguments.of("Overpaid Loan Status", LoanStatus.OVERPAID),
Arguments.of("Approved Loan Status", LoanStatus.APPROVED), Arguments.of("Rejected Loan Status", LoanStatus.REJECTED),
Arguments.of("Submitted and Pending Approval Loan Status", LoanStatus.SUBMITTED_AND_PENDING_APPROVAL),
Arguments.of("Withdrawn By Client Loan Status", LoanStatus.WITHDRAWN_BY_CLIENT),
Arguments.of("Closed Written Off Loan Status", LoanStatus.CLOSED_WRITTEN_OFF),
Arguments.of("Closed Reschedule Outstanding Amount Loan Status", LoanStatus.CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT),
Arguments.of("Closed Obligations Met Loan Status", LoanStatus.CLOSED_OBLIGATIONS_MET));
}
private static Stream<Arguments> loanStatusValidationDataProviderInvalidDelayedSettlement() {
return Stream.of(Arguments.of("Invalid Loan Status", LoanStatus.INVALID), Arguments.of("Approved Loan Status", LoanStatus.APPROVED),
Arguments.of("Rejected Loan Status", LoanStatus.REJECTED),
Arguments.of("Submitted and Pending Approval Loan Status", LoanStatus.SUBMITTED_AND_PENDING_APPROVAL),
Arguments.of("Withdrawn By Client Loan Status", LoanStatus.WITHDRAWN_BY_CLIENT),
Arguments.of("Closed Written Off Loan Status", LoanStatus.CLOSED_WRITTEN_OFF),
Arguments.of("Closed Reschedule Outstanding Amount Loan Status", LoanStatus.CLOSED_RESCHEDULE_OUTSTANDING_AMOUNT));
}
@Test
public void verifyWhenLoanSaleIsInitiatedWithoutOwnerThenAssetOwnerTransferIsCreated() {
final TestContext testContext = new TestContext();
final ArgumentCaptor<ExternalAssetOwnerTransfer> externalAssetOwnerTransferArgumentCaptor = ArgumentCaptor
.forClass(ExternalAssetOwnerTransfer.class);
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(false);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
when(testContext.externalAssetOwnerRepository.findByExternalId(any(ExternalId.class))).thenReturn(Optional.empty());
// when
CommandProcessingResult result = testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command);
// then
verify(testContext.externalAssetOwnerRepository).findByExternalId(any(ExternalId.class));
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(externalAssetOwnerTransferArgumentCaptor.capture());
verify(testContext.externalAssetOwnerRepository).saveAndFlush(any(ExternalAssetOwner.class));
verify(testContext.externalAssetOwnerTransferRepository).findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
any(LocalDate.class));
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
ExternalAssetOwnerTransfer savedTransfer = externalAssetOwnerTransferArgumentCaptor.getValue();
assertAssertOwnerTransferValues(testContext, savedTransfer, ExternalTransferStatus.PENDING);
assertEquals(savedTransfer.getId(), result.getResourceId());
assertEquals(savedTransfer.getExternalId(), result.getResourceExternalId());
assertEquals(savedTransfer.getLoanId(), result.getSubResourceId());
assertEquals(savedTransfer.getExternalLoanId(), result.getSubResourceExternalId());
}
@Test
public void verifyWhenLoanIsNotFoundThenExceptionIsThrown() {
final TestContext testContext = new TestContext();
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId)).thenReturn(Optional.empty());
// when
assertThrows(LoanNotFoundException.class, () -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
// then
verify(testContext.fromApiJsonHelper, times(1)).parse(command.json());
verifyNoMoreInteractions(testContext.loanRepository);
verifyNoInteractions(testContext.externalAssetOwnerRepository, testContext.externalAssetOwnerTransferRepository,
testContext.delayedSettlementAttributeService);
}
@ParameterizedTest
@MethodSource("invalidFieldsDataProvider")
public void verifyWhenFieldValueInvalidThenExceptionIsThrown(final String ownerExternalId, final String transferExternalId,
final String purchaseRatio, final LocalDate settlementDate) {
final TestContext testContext = new TestContext();
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
final JsonElement jsonCommandElement = testContext.fromJsonHelper.parse(testContext.jsonCommand);
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(false);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
when(testContext.fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, jsonCommandElement))
.thenReturn(ownerExternalId);
when(testContext.fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, jsonCommandElement))
.thenReturn(transferExternalId);
when(testContext.fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, jsonCommandElement))
.thenReturn(purchaseRatio);
when(testContext.fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, jsonCommandElement))
.thenReturn(settlementDate);
// when
assertThrows(PlatformApiDataValidationException.class,
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
// then
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
verifyNoMoreInteractions(testContext.loanRepository);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
verifyNoInteractions(testContext.externalAssetOwnerRepository, testContext.externalAssetOwnerTransferRepository);
}
private static Stream<Arguments> invalidFieldsDataProvider() {
// ownerExternalId, transferExternalId, purchaseRatio, settlementDate
return Stream.of(
// settlement date cannot be null
Arguments.of("value", "value", "value", null),
// purchaseRatio cannot be null
Arguments.of("value", "value", null, LocalDate.now(ZoneId.systemDefault()).plusDays(1)),
// purchaseRatio length cannot be > 50
Arguments.of("value", "value", RandomStringUtils.randomAlphanumeric(51), LocalDate.now(ZoneId.systemDefault()).plusDays(1)),
// transferExternalId length cannot be > 100
Arguments.of("value", RandomStringUtils.randomAlphanumeric(101), "value",
LocalDate.now(ZoneId.systemDefault()).plusDays(1)),
// ownerExternalId cannot be null
Arguments.of(null, "value", "value", LocalDate.now(ZoneId.systemDefault()).plusDays(1)),
// ownerExternalId length cannot be > 100
Arguments.of(RandomStringUtils.randomAlphanumeric(101), "value", "value",
LocalDate.now(ZoneId.systemDefault()).plusDays(1)));
}
@Test
public void verifyWhenTransferExternalIdExistsThenExceptionIsThrown() {
final TestContext testContext = new TestContext();
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(false);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
when(testContext.externalAssetOwnerTransferRepository.exists(any(Specification.class))).thenReturn(true);
// when
assertThrows(ExternalAssetOwnerInitiateTransferException.class,
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
// then
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
verify(testContext.loanRepository, times(1)).findLoanDataForExternalTransferByLoanId(testContext.loanId);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
verify(testContext.externalAssetOwnerTransferRepository, times(1)).exists(any(Specification.class));
verifyNoInteractions(testContext.externalAssetOwnerRepository);
}
@ParameterizedTest
@MethodSource("delayedSettlementFlagDataProvider")
public void verifyWhenTooManyEffectiveTransfersThenExceptionIsThrown(final boolean isDelayedSettlementEnabled) {
final TestContext testContext = new TestContext();
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(isDelayedSettlementEnabled);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
any(LocalDate.class))).thenReturn(List.of(new ExternalAssetOwnerTransfer(), new ExternalAssetOwnerTransfer()));
// when
ExternalAssetOwnerInitiateTransferException exception = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
assertEquals(exception.getMessage(), "This loan cannot be sold, there is already an in progress transfer");
// then
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
verify(testContext.externalAssetOwnerTransferRepository).exists(any(Specification.class));
verify(testContext.externalAssetOwnerTransferRepository, times(1)).findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
any(LocalDate.class));
verify(testContext.externalAssetOwnerRepository).findByExternalId(any(ExternalId.class));
}
private static Stream<Arguments> delayedSettlementFlagDataProvider() {
return Stream.of(Arguments.of(false), Arguments.of(true));
}
@Test
public void verifyWhenNoEffectiveTransfersWithDelayedSettlementThenExceptionIsThrown() {
final TestContext testContext = new TestContext();
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
any(LocalDate.class))).thenReturn(Collections.emptyList());
// when
ExternalAssetOwnerInitiateTransferException exception = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
assertEquals(exception.getMessage(), "This loan cannot be sold, no effective transfer found.");
// then
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
verify(testContext.externalAssetOwnerTransferRepository).exists(any(Specification.class));
verify(testContext.externalAssetOwnerTransferRepository, times(1)).findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
any(LocalDate.class));
verify(testContext.externalAssetOwnerRepository).findByExternalId(any(ExternalId.class));
}
@ParameterizedTest
@MethodSource("invalidTransferStatusDataProvider")
public void verifyWhenInvalidTransferStatusThenExceptionIsThrown(final ExternalTransferStatus externalTransferStatus,
final boolean isDelayedSettlementEnabled, final String expectedExceptionMessage) {
final TestContext testContext = new TestContext();
// given
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
final ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
externalAssetOwnerTransfer.setStatus(externalTransferStatus);
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(isDelayedSettlementEnabled);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
any(LocalDate.class))).thenReturn(List.of(externalAssetOwnerTransfer));
// when
ExternalAssetOwnerInitiateTransferException exception = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
() -> testContext.externalAssetOwnersWriteServiceImpl.saleLoanByLoanId(command));
assertEquals(exception.getMessage(), expectedExceptionMessage);
// then
verify(testContext.fromApiJsonHelper, times(2)).parse(command.json());
verify(testContext.loanRepository).findLoanDataForExternalTransferByLoanId(testContext.loanId);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
verify(testContext.externalAssetOwnerTransferRepository).exists(any(Specification.class));
verify(testContext.externalAssetOwnerTransferRepository, times(1)).findEffectiveTransfersOrderByIdDesc(eq(testContext.loanId),
any(LocalDate.class));
verify(testContext.externalAssetOwnerRepository).findByExternalId(any(ExternalId.class));
}
private static Stream<Arguments> invalidTransferStatusDataProvider() {
return Stream.of(
Arguments.of(ExternalTransferStatus.PENDING, false,
"External asset owner transfer is already in PENDING state for this loan"),
Arguments.of(ExternalTransferStatus.ACTIVE, false,
"This loan cannot be sold, because it is owned by an external asset owner"),
Arguments.of(ExternalTransferStatus.PENDING_INTERMEDIATE, true,
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state."),
Arguments.of(ExternalTransferStatus.ACTIVE, true,
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state."),
Arguments.of(ExternalTransferStatus.DECLINED, true,
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state."),
Arguments.of(ExternalTransferStatus.PENDING, true,
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state."),
Arguments.of(ExternalTransferStatus.BUYBACK, true,
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state."),
Arguments.of(ExternalTransferStatus.CANCELLED, true,
"This loan cannot be sold, because it is not in ACTIVE-INTERMEDIATE state."));
}
@ParameterizedTest
@MethodSource("buybackValidationWithDelaySettlementSuccessfulDataProvider")
void buybackLoanByLoanIdWhenDelaySettlementEnabledSuccess(final List<ExternalTransferStatus> transferStatuses,
final ExternalTransferStatus expectedStatus) {
// given
TestContext testContext = new TestContext();
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
List<ExternalAssetOwnerTransfer> transfers = transferStatuses.stream()
.map(transferStatus -> createExternalAssetOwnerTransfer(testContext, transferStatus)).toList();
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(testContext.loanId, actualDate))
.thenReturn(transfers);
// when
CommandProcessingResult result = testContext.externalAssetOwnersWriteServiceImpl.buybackLoanByLoanId(command);
// then
ArgumentCaptor<ExternalAssetOwnerTransfer> savedTransferCaptor = ArgumentCaptor.forClass(ExternalAssetOwnerTransfer.class);
verify(testContext.externalAssetOwnerTransferRepository).saveAndFlush(savedTransferCaptor.capture());
ExternalAssetOwnerTransfer savedTransfer = savedTransferCaptor.getValue();
assertNotNull(savedTransfer);
assertFalse(transfers.isEmpty());
ExternalAssetOwnerTransfer expectedEffectiveTransfer = transfers.get(0);
assertEquals(testContext.transferExternalId, savedTransfer.getExternalId().getValue());
assertEquals(expectedEffectiveTransfer.getOwner(), savedTransfer.getOwner());
assertEquals(expectedStatus, savedTransfer.getStatus());
assertEquals(expectedEffectiveTransfer.getLoanId(), savedTransfer.getLoanId());
assertEquals(expectedEffectiveTransfer.getExternalLoanId(), savedTransfer.getExternalLoanId());
assertEquals(testContext.settlementDate, savedTransfer.getSettlementDate());
assertEquals(actualDate, savedTransfer.getEffectiveDateFrom());
assertEquals(FUTURE_DATE_9999_12_31, savedTransfer.getEffectiveDateTo());
assertEquals(expectedEffectiveTransfer.getPurchasePriceRatio(), savedTransfer.getPurchasePriceRatio());
assertEquals(savedTransfer.getId(), result.getResourceId());
assertEquals(savedTransfer.getExternalId(), result.getResourceExternalId());
assertEquals(savedTransfer.getLoanId(), result.getSubResourceId());
assertEquals(savedTransfer.getExternalLoanId(), result.getSubResourceExternalId());
verifyNoInteractions(testContext.externalAssetOwnerRepository);
}
private static Stream<Arguments> buybackValidationWithDelaySettlementSuccessfulDataProvider() {
return Stream.of(Arguments.of(List.of(ExternalTransferStatus.ACTIVE_INTERMEDIATE), ExternalTransferStatus.BUYBACK_INTERMEDIATE),
Arguments.of(List.of(ExternalTransferStatus.ACTIVE), ExternalTransferStatus.BUYBACK));
}
@ParameterizedTest
@MethodSource("buybackValidationWithDelaySettlementFailureDataProvider")
void buybackLoanByLoanIdWhenDelaySettlementEnabledFailure(final List<ExternalTransferStatus> transferStatuses,
final String expectedExceptionMessage) {
// given
TestContext testContext = new TestContext();
final JsonCommand command = createJsonCommand(testContext.jsonCommand, testContext.loanId);
when(testContext.delayedSettlementAttributeService.isEnabled(testContext.loanProductId)).thenReturn(true);
when(testContext.loanRepository.findLoanDataForExternalTransferByLoanId(testContext.loanId))
.thenReturn(Optional.of(testContext.loanDataForExternalTransfer));
List<ExternalAssetOwnerTransfer> transfers = transferStatuses.stream()
.map(transferStatus -> createExternalAssetOwnerTransfer(testContext, transferStatus)).toList();
when(testContext.externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(testContext.loanId, actualDate))
.thenReturn(transfers);
// when
ExternalAssetOwnerInitiateTransferException actualException = assertThrows(ExternalAssetOwnerInitiateTransferException.class,
() -> testContext.externalAssetOwnersWriteServiceImpl.buybackLoanByLoanId(command));
// then
assertEquals(expectedExceptionMessage, actualException.getMessage());
verify(testContext.externalAssetOwnerTransferRepository, never()).saveAndFlush(any());
verifyNoInteractions(testContext.externalAssetOwnerRepository);
verify(testContext.delayedSettlementAttributeService).isEnabled(testContext.loanProductId);
}
private static Stream<Arguments> buybackValidationWithDelaySettlementFailureDataProvider() {
return Stream.of(
Arguments.of(Collections.emptyList(), "This loan cannot be bought back, it is not owned by an external asset owner"),
Arguments.of(List.of(ExternalTransferStatus.PENDING_INTERMEDIATE),
"This loan cannot be bought back, effective transfer is not in right state: PENDING_INTERMEDIATE"),
Arguments.of(List.of(ExternalTransferStatus.PENDING),
"This loan cannot be bought back, effective transfer is not in right state: PENDING"),
Arguments.of(List.of(ExternalTransferStatus.DECLINED),
"This loan cannot be bought back, effective transfer is not in right state: DECLINED"),
Arguments.of(List.of(ExternalTransferStatus.BUYBACK),
"This loan cannot be bought back, effective transfer is not in right state: BUYBACK"),
Arguments.of(List.of(ExternalTransferStatus.BUYBACK_INTERMEDIATE),
"This loan cannot be bought back, effective transfer is not in right state: BUYBACK_INTERMEDIATE"),
Arguments.of(List.of(ExternalTransferStatus.CANCELLED),
"This loan cannot be bought back, effective transfer is not in right state: CANCELLED"),
Arguments.of(List.of(ExternalTransferStatus.ACTIVE_INTERMEDIATE, ExternalTransferStatus.PENDING),
"This loan cannot be bought back, external asset owner sale is pending"),
Arguments.of(List.of(ExternalTransferStatus.PENDING, ExternalTransferStatus.ACTIVE_INTERMEDIATE),
"This loan cannot be bought back, external asset owner sale is pending"),
Arguments.of(List.of(ExternalTransferStatus.ACTIVE_INTERMEDIATE, ExternalTransferStatus.BUYBACK_INTERMEDIATE),
"This loan cannot be bought back, external asset owner buyback transfer is already in progress"),
Arguments.of(List.of(ExternalTransferStatus.BUYBACK_INTERMEDIATE, ExternalTransferStatus.ACTIVE_INTERMEDIATE),
"This loan cannot be bought back, external asset owner buyback transfer is already in progress"),
Arguments.of(List.of(ExternalTransferStatus.ACTIVE, ExternalTransferStatus.BUYBACK),
"This loan cannot be bought back, external asset owner buyback transfer is already in progress"),
Arguments.of(List.of(ExternalTransferStatus.BUYBACK, ExternalTransferStatus.ACTIVE),
"This loan cannot be bought back, external asset owner buyback transfer is already in progress"));
}
/**
* Helper method to create {@link JsonCommand} object from json command string.
*
* @param jsonCommand
* the json command string
* @param loanId
* the loan id
* @return the {@link JsonCommand} object.
*/
private JsonCommand createJsonCommand(final String jsonCommand, final Long loanId) {
return new JsonCommand(null, jsonCommand, null, null, null, null, null, null, null, loanId, null, null, null, null, null, null,
null, null);
}
/**
* Helper method to create {@link ExternalAssetOwnerTransfer} object.
*
* @param status
* the {@link ExternalTransferStatus}
* @return the {@link ExternalAssetOwnerTransfer} object.
*/
private ExternalAssetOwnerTransfer createExternalAssetOwnerTransfer(final TestContext testContext,
final ExternalTransferStatus status) {
ExternalAssetOwnerTransfer transfer = new ExternalAssetOwnerTransfer();
transfer.setExternalId(new ExternalId(RandomStringUtils.randomAlphanumeric(10)));
transfer.setOwner(new ExternalAssetOwner());
transfer.setStatus(status);
transfer.setLoanId(testContext.loanId);
transfer.setExternalLoanId(new ExternalId(testContext.externalLoanId));
transfer.setExternalGroupId(new ExternalId(testContext.externalLoanId));
transfer.setPurchasePriceRatio(TestContext.PURCHASE_RATIO.toString());
return transfer;
}
/**
* Asserts on the {@link ExternalAssetOwnerTransfer} object values against test values.
*
* @param testContext
* the test context with expected values.
* @param externalAssetOwnerTransfer
* the {@link ExternalAssetOwnerTransfer} object.
* @param expectedTransferStatus
* the expected transfer status.
*/
private void assertAssertOwnerTransferValues(final TestContext testContext, final ExternalAssetOwnerTransfer externalAssetOwnerTransfer,
final ExternalTransferStatus expectedTransferStatus) {
assertEquals(testContext.loanId, externalAssetOwnerTransfer.getLoanId());
assertEquals(testContext.externalLoanId, externalAssetOwnerTransfer.getExternalLoanId().getValue());
assertEquals(testContext.ownerExternalId, externalAssetOwnerTransfer.getOwner().getExternalId().getValue());
assertEquals(testContext.transferExternalId, externalAssetOwnerTransfer.getExternalId().getValue());
assertEquals(testContext.transferExternalGroupId, externalAssetOwnerTransfer.getExternalGroupId().getValue());
assertEquals(expectedTransferStatus, externalAssetOwnerTransfer.getStatus());
assertEquals(TestContext.PURCHASE_RATIO.toString(), externalAssetOwnerTransfer.getPurchasePriceRatio());
assertEquals(testContext.settlementDate, externalAssetOwnerTransfer.getSettlementDate());
assertEquals(actualDate, externalAssetOwnerTransfer.getEffectiveDateFrom());
assertEquals(FUTURE_DATE_9999_12_31, externalAssetOwnerTransfer.getEffectiveDateTo());
}
@SuppressFBWarnings({ "VA_FORMAT_STRING_USES_NEWLINE" })
static class TestContext {
@Mock
private ExternalAssetOwnerTransferRepository externalAssetOwnerTransferRepository;
@Mock
private ExternalAssetOwnerRepository externalAssetOwnerRepository;
@Mock
private FromJsonHelper fromApiJsonHelper;
@Mock
private LoanRepository loanRepository;
@Mock
private DelayedSettlementAttributeService delayedSettlementAttributeService;
@Mock
private LoanDataForExternalTransfer loanDataForExternalTransfer;
@InjectMocks
private ExternalAssetOwnersWriteServiceImpl externalAssetOwnersWriteServiceImpl;
private static final BigDecimal PURCHASE_RATIO = BigDecimal.valueOf(Float.parseFloat(RandomStringUtils.randomNumeric(1, 3)) / 100)
.setScale(2, RoundingMode.HALF_UP);
private static final String DATE_FORMAT = "yyyy-MM-dd";
private static final String LOCALE = "de_DE";
private final FromJsonHelper fromJsonHelper = new FromJsonHelper();
private final ExternalAssetOwner externalAssetOwner = new ExternalAssetOwner();
private final Long loanId = Long.valueOf(RandomStringUtils.randomNumeric(2));
private final String externalLoanId = RandomStringUtils.randomAlphanumeric(10);
private final Long loanProductId = Long.valueOf(RandomStringUtils.randomNumeric(2));
private final String loanProductShortName = RandomStringUtils.randomAlphanumeric(10);
private final String ownerExternalId = RandomStringUtils.randomAlphanumeric(10);
private final String transferExternalId = RandomStringUtils.randomAlphanumeric(10);
private final String transferExternalGroupId = RandomStringUtils.randomAlphanumeric(10);
private final LocalDate settlementDate = LocalDate.parse("9999-08-22");
private final String jsonCommand = String.format("""
{
"settlementDate": "%s",
"ownerExternalId": "%s",
"transferExternalId": "%s",
"transferExternalGroupId": "%s",
"purchasePriceRatio": "%s",
"dateFormat": "%s",
"locale": "%s"
}
""", settlementDate, ownerExternalId, transferExternalId, transferExternalGroupId, PURCHASE_RATIO, DATE_FORMAT, LOCALE);
@SuppressFBWarnings("CT_CONSTRUCTOR_THROW")
TestContext() {
MockitoAnnotations.openMocks(this);
final JsonElement jsonCommandElement = fromJsonHelper.parse(jsonCommand);
externalAssetOwner.setExternalId(new ExternalId(ownerExternalId));
lenient().when(fromApiJsonHelper.parse(anyString())).thenReturn(jsonCommandElement);
lenient().when(fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.OWNER_EXTERNAL_ID, jsonCommandElement))
.thenReturn(ownerExternalId);
lenient().when(fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_ID, jsonCommandElement))
.thenReturn(transferExternalId);
lenient().when(
fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.TRANSFER_EXTERNAL_GROUP_ID, jsonCommandElement))
.thenReturn(transferExternalGroupId);
lenient().when(fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.PURCHASE_PRICE_RATIO, jsonCommandElement))
.thenReturn(PURCHASE_RATIO.toString());
lenient().when(fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.DATEFORMAT, jsonCommandElement))
.thenReturn(DATE_FORMAT);
lenient().when(fromApiJsonHelper.extractStringNamed(ExternalTransferRequestParameters.LOCALE, jsonCommandElement))
.thenReturn(LOCALE);
lenient().when(fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, jsonCommandElement))
.thenReturn(settlementDate);
lenient().when(fromApiJsonHelper.extractLocalDateNamed(ExternalTransferRequestParameters.SETTLEMENT_DATE, jsonCommandElement,
DATE_FORMAT, Locale.GERMANY)).thenReturn(settlementDate);
lenient().when(externalAssetOwnerRepository.findByExternalId(any(ExternalId.class)))
.thenReturn(Optional.of(externalAssetOwner));
lenient().when(externalAssetOwnerTransferRepository.findEffectiveTransfersOrderByIdDesc(eq(loanId), any(LocalDate.class)))
.thenReturn(Collections.emptyList());
lenient().when(externalAssetOwnerRepository.saveAndFlush(any(ExternalAssetOwner.class))).thenReturn(externalAssetOwner);
lenient().when(loanDataForExternalTransfer.getId()).thenReturn(loanId);
lenient().when(loanDataForExternalTransfer.getExternalId()).thenReturn(new ExternalId(externalLoanId));
lenient().when(loanDataForExternalTransfer.getLoanStatus()).thenReturn(LoanStatus.ACTIVE.getValue());
lenient().when(loanDataForExternalTransfer.getLoanProductId()).thenReturn(loanProductId);
lenient().when(loanDataForExternalTransfer.getLoanProductShortName()).thenReturn(loanProductShortName);
}
}
}
@@ -19,8 +19,6 @@
package org.apache.fineract.investor.service;
import static org.apache.fineract.infrastructure.businessdate.domain.BusinessDateType.BUSINESS_DATE;
import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK;
import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.ArgumentMatchers.any;
@@ -36,10 +34,12 @@ import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.stream.Stream;
import org.apache.fineract.infrastructure.core.service.ThreadLocalContextUtil;
import org.apache.fineract.infrastructure.event.business.domain.BusinessEvent;
import org.apache.fineract.infrastructure.event.business.domain.loan.LoanAccountSnapshotBusinessEvent;
import org.apache.fineract.infrastructure.event.business.service.BusinessEventNotifierService;
import org.apache.fineract.investor.data.ExternalTransferStatus;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferLoanMappingRepository;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransferRepository;
@@ -50,6 +50,9 @@ import org.jetbrains.annotations.NotNull;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
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.ArgumentCaptor;
import org.mockito.Mock;
import org.mockito.Mockito;
@@ -80,38 +83,88 @@ public class LoanAccountOwnerTransferServiceTest {
}
@Test
public void verifyWhenCancelPendingSaleAndBuybackTransferThenBusinessEventsAreSent() {
public void verifyWhenCancelPendingAndIntermediateSaleAndBuybackTransferThenBusinessEventsAreSent() {
// given
final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L);
ExternalAssetOwnerTransfer pendingSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer pendingBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer pendingIntermediateTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer pendingBuyBackIntermediateTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer cancelledSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer cancelledBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer cancelledIntermediateTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer cancelledBuyBackIntermediateSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
List<ExternalAssetOwnerTransfer> response = List.of(pendingSaleTransfer, pendingBuybackTransfer);
List<ExternalAssetOwnerTransfer> response = List.of(pendingSaleTransfer, pendingBuybackTransfer, pendingIntermediateTransfer,
pendingBuyBackIntermediateTransfer);
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(ASC, "id")))).thenReturn(response);
when(externalAssetOwnerTransferRepository.save(any(ExternalAssetOwnerTransfer.class))).thenReturn(pendingSaleTransfer)
.thenReturn(cancelledSaleTransfer).thenReturn(pendingBuybackTransfer).thenReturn(cancelledBuybackTransfer);
.thenReturn(cancelledSaleTransfer).thenReturn(pendingBuybackTransfer).thenReturn(cancelledBuybackTransfer)
.thenReturn(pendingIntermediateTransfer).thenReturn(cancelledIntermediateTransfer)
.thenReturn(pendingBuyBackIntermediateTransfer).thenReturn(cancelledBuyBackIntermediateSaleTransfer);
// when
underTest.handleLoanClosedOrOverpaid(loanForProcessing);
// then
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(2);
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(4);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, cancelledSaleTransfer);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing, cancelledBuybackTransfer);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 2, loanForProcessing, cancelledIntermediateTransfer);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 3, loanForProcessing, cancelledBuyBackIntermediateSaleTransfer);
}
@Test
public void verifyWhenDeclinePendingSaleTransferThenBusinessEventIsSent() {
public void verifyWhenDeclineCancelPendingAndIntermediateSaleAndBuybackTransferThenBusinessEventsAreSent() {
// given
final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L);
ExternalAssetOwnerTransfer pendingSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(pendingSaleTransfer.getStatus()).thenReturn(PENDING);
when(pendingSaleTransfer.getSettlementDate()).thenReturn(actualDate.minusDays(1));
ExternalAssetOwnerTransfer pendingBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer pendingIntermediateTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer pendingBuyBackIntermediateTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(pendingSaleTransfer.getSettlementDate()).thenReturn(actualDate.minusDays(1));
when(pendingBuybackTransfer.getSettlementDate()).thenReturn(actualDate.minusDays(1));
when(pendingIntermediateTransfer.getSettlementDate()).thenReturn(actualDate);
ExternalAssetOwnerTransfer declinedSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer cancelledBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer cancelledIntermediateTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
ExternalAssetOwnerTransfer cancelledBuyBackIntermediateSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
List<ExternalAssetOwnerTransfer> response = List.of(pendingSaleTransfer, pendingBuybackTransfer, pendingIntermediateTransfer,
pendingBuyBackIntermediateTransfer);
when(externalAssetOwnerTransferRepository.findAll(any(Specification.class), eq(Sort.by(ASC, "id")))).thenReturn(response);
when(externalAssetOwnerTransferRepository.save(any(ExternalAssetOwnerTransfer.class))).thenReturn(pendingSaleTransfer)
.thenReturn(declinedSaleTransfer).thenReturn(pendingBuybackTransfer).thenReturn(cancelledBuybackTransfer)
.thenReturn(pendingIntermediateTransfer).thenReturn(cancelledIntermediateTransfer)
.thenReturn(pendingBuyBackIntermediateTransfer).thenReturn(cancelledBuyBackIntermediateSaleTransfer);
// when
underTest.handleLoanClosedOrOverpaid(loanForProcessing);
// then
ArgumentCaptor<BusinessEvent<?>> businessEventArgumentCaptor = verifyBusinessEvents(4);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, pendingSaleTransfer);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing, cancelledBuybackTransfer);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 2, loanForProcessing, cancelledIntermediateTransfer);
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 3, loanForProcessing, cancelledBuyBackIntermediateSaleTransfer);
}
@ParameterizedTest
@MethodSource("pendingStatusDataProvider")
public void verifyWhenDeclinePendingSaleTransferThenBusinessEventIsSent(final ExternalTransferStatus pendingStatus) {
// given
final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L);
ExternalAssetOwnerTransfer pendingSaleTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(pendingSaleTransfer.getStatus()).thenReturn(pendingStatus);
ExternalAssetOwnerTransfer declineTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
List<ExternalAssetOwnerTransfer> response = List.of(pendingSaleTransfer);
@@ -127,8 +180,9 @@ public class LoanAccountOwnerTransferServiceTest {
verifyLoanTransferBusinessEvent(businessEventArgumentCaptor, 0, loanForProcessing, declineTransfer);
}
@Test
public void verifyWhenExecutePendingBuybackTransferThenBusinessEventIsSent() {
@ParameterizedTest
@MethodSource("buybackStatusDataProvider")
public void verifyWhenExecutePendingBuybackTransferThenBusinessEventIsSent(final ExternalTransferStatus buybackStatus) {
// given
final Loan loanForProcessing = Mockito.mock(Loan.class);
when(loanForProcessing.getId()).thenReturn(1L);
@@ -136,7 +190,7 @@ public class LoanAccountOwnerTransferServiceTest {
when(loanForProcessing.getSummary()).thenReturn(loanSummary);
ExternalAssetOwnerTransfer pendingBuybackTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
when(pendingBuybackTransfer.getStatus()).thenReturn(BUYBACK);
when(pendingBuybackTransfer.getStatus()).thenReturn(buybackStatus);
ExternalAssetOwnerTransfer activeTransfer = Mockito.mock(ExternalAssetOwnerTransfer.class);
List<ExternalAssetOwnerTransfer> response = List.of(pendingBuybackTransfer);
@@ -154,6 +208,14 @@ public class LoanAccountOwnerTransferServiceTest {
verifyLoanAccountSnapshotBusinessEvent(businessEventArgumentCaptor, 1, loanForProcessing);
}
private static Stream<Arguments> pendingStatusDataProvider() {
return Stream.of(Arguments.of(ExternalTransferStatus.PENDING_INTERMEDIATE), Arguments.of(ExternalTransferStatus.PENDING));
}
private static Stream<Arguments> buybackStatusDataProvider() {
return Stream.of(Arguments.of(ExternalTransferStatus.BUYBACK_INTERMEDIATE), Arguments.of(ExternalTransferStatus.BUYBACK));
}
@NotNull
private ArgumentCaptor<BusinessEvent<?>> verifyBusinessEvents(int expectedBusinessEvents) {
@SuppressWarnings("unchecked")
@@ -0,0 +1,163 @@
/**
* 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.investor.service;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertTrue;
import static org.mockito.Mockito.when;
import java.math.BigDecimal;
import java.util.stream.Stream;
import org.apache.fineract.investor.data.ExternalTransferStatus;
import org.apache.fineract.investor.data.ExternalTransferSubStatus;
import org.apache.fineract.investor.domain.ExternalAssetOwnerTransfer;
import org.apache.fineract.portfolio.loanaccount.domain.Loan;
import org.apache.fineract.portfolio.loanaccount.domain.LoanSummary;
import org.apache.fineract.portfolio.loanproduct.domain.LoanProduct;
import org.junit.jupiter.api.Test;
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.Mockito;
import org.mockito.MockitoAnnotations;
import org.mockito.junit.jupiter.MockitoExtension;
@ExtendWith(MockitoExtension.class)
public class LoanTransferabilityServiceImplTest {
private static final Long LOAN_PRODUCT_ID = 1L;
private static Stream<Arguments> amountDataProvider() {
return Stream.of(Arguments.of(BigDecimal.ONE, true), Arguments.of(BigDecimal.ZERO, false),
Arguments.of(BigDecimal.ONE.negate(), false));
}
@ParameterizedTest
@MethodSource("amountDataProvider")
void isTransferableWhenDelayedSettlementDisabled(final BigDecimal loanOutstandingAmount, final boolean expectedResult) {
// given
TestContext testContext = new TestContext();
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanSummary.getTotalOutstanding()).thenReturn(loanOutstandingAmount);
Loan loan = Mockito.mock(Loan.class);
when(loan.getLoanProduct()).thenReturn(loanProduct);
when(loan.getSummary()).thenReturn(loanSummary);
when(testContext.delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(false);
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
externalAssetOwnerTransfer.setStatus(ExternalTransferStatus.PENDING);
// when
boolean result = testContext.testSubject.isTransferable(loan, externalAssetOwnerTransfer);
// then
assertEquals(expectedResult, result);
}
@ParameterizedTest
@MethodSource("amountDataProvider")
void isTransferableWhenDelayedSettlementEnabledAndSellingToIntermediate(final BigDecimal loanOutstandingAmount,
final boolean expectedResult) {
// given
TestContext testContext = new TestContext();
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
LoanSummary loanSummary = Mockito.mock(LoanSummary.class);
when(loanSummary.getTotalOutstanding()).thenReturn(loanOutstandingAmount);
Loan loan = Mockito.mock(Loan.class);
when(loan.getLoanProduct()).thenReturn(loanProduct);
when(loan.getSummary()).thenReturn(loanSummary);
when(testContext.delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(true);
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
externalAssetOwnerTransfer.setStatus(ExternalTransferStatus.PENDING_INTERMEDIATE);
// when
boolean result = testContext.testSubject.isTransferable(loan, externalAssetOwnerTransfer);
// then
assertEquals(expectedResult, result);
}
@Test
void isTransferableWhenDelayedSettlementEnabledAndSellingToInvestor() {
// given
TestContext testContext = new TestContext();
LoanProduct loanProduct = Mockito.mock(LoanProduct.class);
when(loanProduct.getId()).thenReturn(LOAN_PRODUCT_ID);
Loan loan = Mockito.mock(Loan.class);
when(loan.getLoanProduct()).thenReturn(loanProduct);
when(testContext.delayedSettlementAttributeService.isEnabled(LOAN_PRODUCT_ID)).thenReturn(true);
ExternalAssetOwnerTransfer externalAssetOwnerTransfer = new ExternalAssetOwnerTransfer();
externalAssetOwnerTransfer.setStatus(ExternalTransferStatus.PENDING);
// when
boolean result = testContext.testSubject.isTransferable(loan, externalAssetOwnerTransfer);
// then
assertTrue(result);
}
private static Stream<Arguments> declinedSubStatusDataProvider() {
return Stream.of(Arguments.of(BigDecimal.ONE, ExternalTransferSubStatus.BALANCE_NEGATIVE),
Arguments.of(BigDecimal.ZERO, ExternalTransferSubStatus.BALANCE_ZERO),
Arguments.of(null, ExternalTransferSubStatus.BALANCE_ZERO));
}
@ParameterizedTest
@MethodSource("declinedSubStatusDataProvider")
void getDeclinedSubStatus(final BigDecimal totalOverpaidAmount, final ExternalTransferSubStatus expectedSubStatus) {
// given
TestContext testContext = new TestContext();
Loan loan = Mockito.mock(Loan.class);
when(loan.getTotalOverpaid()).thenReturn(totalOverpaidAmount);
// when
ExternalTransferSubStatus result = testContext.testSubject.getDeclinedSubStatus(loan);
// then
assertEquals(expectedSubStatus, result);
}
private static class TestContext {
@Mock
private DelayedSettlementAttributeService delayedSettlementAttributeService;
@InjectMocks
private LoanTransferabilityServiceImpl testSubject;
TestContext() {
MockitoAnnotations.openMocks(this);
}
}
}
@@ -19,9 +19,13 @@
package org.apache.fineract.investor.service.serialization.serializer.investor;
import static org.apache.fineract.investor.data.ExternalTransferStatus.ACTIVE;
import static org.apache.fineract.investor.data.ExternalTransferStatus.ACTIVE_INTERMEDIATE;
import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK;
import static org.apache.fineract.investor.data.ExternalTransferStatus.BUYBACK_INTERMEDIATE;
import static org.apache.fineract.investor.data.ExternalTransferStatus.CANCELLED;
import static org.apache.fineract.investor.data.ExternalTransferStatus.DECLINED;
import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING;
import static org.apache.fineract.investor.data.ExternalTransferStatus.PENDING_INTERMEDIATE;
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_NEGATIVE;
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.BALANCE_ZERO;
import static org.apache.fineract.investor.data.ExternalTransferSubStatus.SAMEDAY_TRANSFERS;
@@ -67,35 +71,61 @@ public class InvestorBusinessEventSerializerTest {
@Test
public void testSerializationSellOK() {
doTest(ACTIVE, null, "SALE", "EXECUTED", null);
doTest(ACTIVE, null, null, "SALE", "EXECUTED", null);
}
@Test
public void testSerializationBuybackOK() {
doTest(BUYBACK, null, "BUYBACK", "EXECUTED", null);
doTest(BUYBACK, null, null, "BUYBACK", "EXECUTED", null);
}
@Test
public void testSerializationIntermediarySaleOK() {
doTest(ACTIVE_INTERMEDIATE, null, null, "INTERMEDIARYSALE", "EXECUTED", null);
}
@Test
public void testSerializationBuybackIntermediateOK() {
doTest(BUYBACK_INTERMEDIATE, null, null, "BUYBACK", "EXECUTED", null);
}
@Test
public void testSerializationDeclinedNegativeBalance() {
doTest(DECLINED, BALANCE_NEGATIVE, "SALE", "DECLINED", "BALANCE_NEGATIVE");
doTest(DECLINED, BALANCE_NEGATIVE, PENDING, "SALE", "DECLINED", "BALANCE_NEGATIVE");
}
@Test
public void testSerializationDeclinedBalanceZero() {
doTest(DECLINED, BALANCE_ZERO, "SALE", "DECLINED", "BALANCE_ZERO");
doTest(DECLINED, BALANCE_ZERO, PENDING, "SALE", "DECLINED", "BALANCE_ZERO");
}
@Test
public void testSerializationDeclinedPendingIntermediateNegativeBalance() {
doTest(DECLINED, BALANCE_NEGATIVE, PENDING_INTERMEDIATE, "INTERMEDIARYSALE", "DECLINED", "BALANCE_NEGATIVE");
}
@Test
public void testSerializationDeclinedPendingIntermediateBalanceZero() {
doTest(DECLINED, BALANCE_ZERO, PENDING_INTERMEDIATE, "INTERMEDIARYSALE", "DECLINED", "BALANCE_ZERO");
}
@Test
public void testSerializationCancelledSameDayTransfer() {
doTest(CANCELLED, SAMEDAY_TRANSFERS, "SALE", "CANCELLED", "SAMEDAY_TRANSFERS");
doTest(CANCELLED, SAMEDAY_TRANSFERS, PENDING, "SALE", "CANCELLED", "SAMEDAY_TRANSFERS");
}
private void doTest(ExternalTransferStatus status, ExternalTransferSubStatus subStatus, String expectedType, String expectedStatus,
String expectedReason) {
@Test
public void testSerializationCancelledPendingIntermediateSameDayTransfer() {
doTest(CANCELLED, SAMEDAY_TRANSFERS, PENDING_INTERMEDIATE, "INTERMEDIARYSALE", "CANCELLED", "SAMEDAY_TRANSFERS");
}
private void doTest(ExternalTransferStatus status, ExternalTransferSubStatus subStatus, ExternalTransferStatus firstTransferStatus,
String expectedType, String expectedStatus, String expectedReason) {
// given
ExternalAssetOwnersReadService mockReadService = Mockito.mock(ExternalAssetOwnersReadService.class);
when(mockReadService.retrieveTransferData(123L)).thenReturn(createTransferData(status, subStatus));
when(mockReadService.retrieveFirstTransferByExternalId(any(ExternalId.class))).thenReturn(createTransferData(ACTIVE, null));
when(mockReadService.retrieveFirstTransferByExternalId(any(ExternalId.class)))
.thenReturn(createTransferData(firstTransferStatus, null));
Loan loan = Mockito.mock(Loan.class);
when(loan.getCurrency()).thenReturn(new MonetaryCurrency("EUR", 2, 1));
List<LoanCharge> loanCharges = createMockCharges();