From c57f44d3f0fc266d9b11b11d29a83a9c7b056861 Mon Sep 17 00:00:00 2001 From: mariiaKraievska Date: Mon, 2 Feb 2026 15:50:24 +0200 Subject: [PATCH] FINERACT-2418: Add originators details to external business events --- fineract-loan-origination/dependencies.gradle | 4 + .../LoanOriginatorMappingRepository.java | 11 + .../LoanAccountDataV1OriginatorEnricher.java | 72 +++++++ .../enricher/LoanOriginatorAvroMapper.java | 70 +++++++ ...anTransactionDataV1OriginatorEnricher.java | 72 +++++++ ...anAccountDataV1OriginatorEnricherTest.java | 198 ++++++++++++++++++ .../LoanOriginatorAvroMapperTest.java | 122 +++++++++++ ...ansactionDataV1OriginatorEnricherTest.java | 165 +++++++++++++++ 8 files changed, 714 insertions(+) create mode 100644 fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanAccountDataV1OriginatorEnricher.java create mode 100644 fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanOriginatorAvroMapper.java create mode 100644 fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanTransactionDataV1OriginatorEnricher.java create mode 100644 fineract-loan-origination/src/test/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanAccountDataV1OriginatorEnricherTest.java create mode 100644 fineract-loan-origination/src/test/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanOriginatorAvroMapperTest.java create mode 100644 fineract-loan-origination/src/test/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanTransactionDataV1OriginatorEnricherTest.java diff --git a/fineract-loan-origination/dependencies.gradle b/fineract-loan-origination/dependencies.gradle index dee9b9db4..8a0d0643c 100644 --- a/fineract-loan-origination/dependencies.gradle +++ b/fineract-loan-origination/dependencies.gradle @@ -25,6 +25,10 @@ dependencies { // implementation dependencies are directly used (compiled against) in src/main (and src/test) // implementation(project(path: ':fineract-core')) + implementation('org.apache.avro:avro') + implementation( + project(path: ':fineract-avro-schemas') + ) implementation(project(path: ':fineract-loan')) implementation( diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/domain/LoanOriginatorMappingRepository.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/domain/LoanOriginatorMappingRepository.java index 257029fc1..c658d37f4 100644 --- a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/domain/LoanOriginatorMappingRepository.java +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/domain/LoanOriginatorMappingRepository.java @@ -22,12 +22,23 @@ import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface LoanOriginatorMappingRepository extends JpaRepository, JpaSpecificationExecutor { List findByLoanId(Long loanId); + @Query(""" + SELECT m FROM LoanOriginatorMapping m + JOIN FETCH m.originator o + LEFT JOIN FETCH o.originatorType + LEFT JOIN FETCH o.channelType + WHERE m.loanId = :loanId + """) + List findByLoanIdWithOriginatorDetails(@Param("loanId") Long loanId); + boolean existsByLoanId(Long loanId); boolean existsByOriginatorId(Long originatorId); diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanAccountDataV1OriginatorEnricher.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanAccountDataV1OriginatorEnricher.java new file mode 100644 index 000000000..85d3059ff --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanAccountDataV1OriginatorEnricher.java @@ -0,0 +1,72 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanorigination.enricher; + +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.avro.loan.v1.LoanAccountDataV1; +import org.apache.fineract.avro.loan.v1.OriginatorDetailsV1; +import org.apache.fineract.infrastructure.core.service.DataEnricher; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginator; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMapping; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMappingRepository; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", havingValue = "true") +public class LoanAccountDataV1OriginatorEnricher implements DataEnricher { + + private final LoanOriginatorMappingRepository loanOriginatorMappingRepository; + private final LoanOriginatorAvroMapper loanOriginatorAvroMapper; + + @Override + public boolean isDataTypeSupported(final Class dataType) { + return dataType.isAssignableFrom(LoanAccountDataV1.class); + } + + @Override + public void enrich(final LoanAccountDataV1 data) { + if (data == null || data.getId() == null) { + return; + } + + final List mappings = loanOriginatorMappingRepository.findByLoanIdWithOriginatorDetails(data.getId()); + if (mappings == null || mappings.isEmpty()) { + return; + } + + final List originators = new ArrayList<>(); + for (LoanOriginatorMapping mapping : mappings) { + final LoanOriginator originator = mapping.getOriginator(); + if (originator != null) { + final OriginatorDetailsV1 originatorDetails = loanOriginatorAvroMapper.toAvro(originator); + if (originatorDetails != null) { + originators.add(originatorDetails); + } + } + } + + if (!originators.isEmpty()) { + data.setOriginators(originators); + } + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanOriginatorAvroMapper.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanOriginatorAvroMapper.java new file mode 100644 index 000000000..0e04ae3ed --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanOriginatorAvroMapper.java @@ -0,0 +1,70 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanorigination.enricher; + +import org.apache.fineract.avro.generic.v1.CodeValueDataV1; +import org.apache.fineract.avro.loan.v1.OriginatorDetailsV1; +import org.apache.fineract.infrastructure.codes.domain.CodeValue; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginator; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +@Component +@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", havingValue = "true") +public class LoanOriginatorAvroMapper { + + /** + * Converts a LoanOriginator entity to OriginatorDetailsV1 Avro + */ + public OriginatorDetailsV1 toAvro(final LoanOriginator originator) { + if (originator == null) { + return null; + } + + final OriginatorDetailsV1.Builder builder = OriginatorDetailsV1.newBuilder(); + + builder.setId(originator.getId()); + builder.setExternalId(originator.getExternalId() != null ? originator.getExternalId().getValue() : null); + builder.setName(originator.getName()); + builder.setStatus(originator.getStatus() != null ? originator.getStatus().getValue() : null); + builder.setOriginatorType(mapCodeValue(originator.getOriginatorType())); + builder.setChannelType(mapCodeValue(originator.getChannelType())); + + return builder.build(); + } + + /** + * Converts a CodeValue entity to CodeValueDataV1 Avro + */ + private CodeValueDataV1 mapCodeValue(CodeValue codeValue) { + if (codeValue == null) { + return null; + } + + CodeValueDataV1.Builder builder = CodeValueDataV1.newBuilder(); + builder.setId(codeValue.getId()); + builder.setName(codeValue.getLabel()); + builder.setPosition(codeValue.getPosition()); + builder.setDescription(codeValue.getDescription()); + builder.setActive(codeValue.isActive()); + builder.setMandatory(codeValue.isMandatory()); + + return builder.build(); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanTransactionDataV1OriginatorEnricher.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanTransactionDataV1OriginatorEnricher.java new file mode 100644 index 000000000..721932d3e --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanTransactionDataV1OriginatorEnricher.java @@ -0,0 +1,72 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanorigination.enricher; + +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.avro.loan.v1.LoanTransactionDataV1; +import org.apache.fineract.avro.loan.v1.OriginatorDetailsV1; +import org.apache.fineract.infrastructure.core.service.DataEnricher; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginator; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMapping; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMappingRepository; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", havingValue = "true") +public class LoanTransactionDataV1OriginatorEnricher implements DataEnricher { + + private final LoanOriginatorMappingRepository loanOriginatorMappingRepository; + private final LoanOriginatorAvroMapper loanOriginatorAvroMapper; + + @Override + public boolean isDataTypeSupported(final Class dataType) { + return dataType.isAssignableFrom(LoanTransactionDataV1.class); + } + + @Override + public void enrich(final LoanTransactionDataV1 data) { + if (data == null || data.getLoanId() == null) { + return; + } + + final List mappings = loanOriginatorMappingRepository.findByLoanIdWithOriginatorDetails(data.getLoanId()); + if (mappings == null || mappings.isEmpty()) { + return; + } + + final List originators = new ArrayList<>(); + for (LoanOriginatorMapping mapping : mappings) { + final LoanOriginator originator = mapping.getOriginator(); + if (originator != null) { + final OriginatorDetailsV1 originatorDetails = loanOriginatorAvroMapper.toAvro(originator); + if (originatorDetails != null) { + originators.add(originatorDetails); + } + } + } + + if (!originators.isEmpty()) { + data.setOriginators(originators); + } + } +} diff --git a/fineract-loan-origination/src/test/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanAccountDataV1OriginatorEnricherTest.java b/fineract-loan-origination/src/test/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanAccountDataV1OriginatorEnricherTest.java new file mode 100644 index 000000000..0dee5f95a --- /dev/null +++ b/fineract-loan-origination/src/test/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanAccountDataV1OriginatorEnricherTest.java @@ -0,0 +1,198 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanorigination.enricher; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import org.apache.fineract.avro.loan.v1.LoanAccountDataV1; +import org.apache.fineract.avro.loan.v1.OriginatorDetailsV1; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginator; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMapping; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMappingRepository; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class LoanAccountDataV1OriginatorEnricherTest { + + @Mock + private LoanOriginatorMappingRepository loanOriginatorMappingRepository; + + @Mock + private LoanOriginatorAvroMapper loanOriginatorAvroMapper; + + @InjectMocks + private LoanAccountDataV1OriginatorEnricher enricher; + + private LoanAccountDataV1 loanAccountData; + private Long loanId; + + @BeforeEach + void setUp() { + loanId = 1L; + loanAccountData = new LoanAccountDataV1(); + loanAccountData.setId(loanId); + } + + @Test + void testIsDataTypeSupported() { + assertTrue(enricher.isDataTypeSupported(LoanAccountDataV1.class)); + } + + @Test + void testEnrich_WithOriginators() { + // Given + final LoanOriginator originator = createTestOriginator(1L, "test-originator-1", "Test Originator 1"); + final LoanOriginatorMapping mapping = LoanOriginatorMapping.create(loanId, originator); + final List mappings = List.of(mapping); + + final OriginatorDetailsV1 originatorDetails = createOriginatorDetailsV1(1L, "test-originator-1", "Test Originator 1"); + + when(loanOriginatorMappingRepository.findByLoanIdWithOriginatorDetails(loanId)).thenReturn(mappings); + when(loanOriginatorAvroMapper.toAvro(originator)).thenReturn(originatorDetails); + + // When + enricher.enrich(loanAccountData); + + // Then + verify(loanOriginatorMappingRepository).findByLoanIdWithOriginatorDetails(loanId); + verify(loanOriginatorAvroMapper).toAvro(originator); + assertNotNull(loanAccountData.getOriginators()); + assertEquals(1, loanAccountData.getOriginators().size()); + assertEquals("test-originator-1", loanAccountData.getOriginators().getFirst().getExternalId()); + } + + @Test + void testEnrich_WithMultipleOriginators() { + // Given + final LoanOriginator originator1 = createTestOriginator(1L, "test-originator-1", "Test Originator 1"); + final LoanOriginator originator2 = createTestOriginator(2L, "test-originator-2", "Test Originator 2"); + final List mappings = List.of(LoanOriginatorMapping.create(loanId, originator1), + LoanOriginatorMapping.create(loanId, originator2)); + + final OriginatorDetailsV1 details1 = createOriginatorDetailsV1(1L, "test-originator-1", "Test Originator 1"); + final OriginatorDetailsV1 details2 = createOriginatorDetailsV1(2L, "test-originator-2", "Test Originator 2"); + + when(loanOriginatorMappingRepository.findByLoanIdWithOriginatorDetails(loanId)).thenReturn(mappings); + when(loanOriginatorAvroMapper.toAvro(originator1)).thenReturn(details1); + when(loanOriginatorAvroMapper.toAvro(originator2)).thenReturn(details2); + + // When + enricher.enrich(loanAccountData); + + // Then + assertNotNull(loanAccountData.getOriginators()); + assertEquals(2, loanAccountData.getOriginators().size()); + } + + @Test + void testEnrich_NoOriginators() { + // Given + when(loanOriginatorMappingRepository.findByLoanIdWithOriginatorDetails(loanId)).thenReturn(Collections.emptyList()); + + // When + enricher.enrich(loanAccountData); + + // Then + verify(loanOriginatorMappingRepository).findByLoanIdWithOriginatorDetails(loanId); + verify(loanOriginatorAvroMapper, never()).toAvro(any()); + assertNull(loanAccountData.getOriginators()); + } + + @Test + void testEnrich_NullLoanId() { + // Given + loanAccountData.setId(null); + + // When + enricher.enrich(loanAccountData); + + // Then + verify(loanOriginatorMappingRepository, never()).findByLoanIdWithOriginatorDetails(any()); + assertNull(loanAccountData.getOriginators()); + } + + @Test + void testEnrich_NullData() { + // When + enricher.enrich(null); + + // Then + verify(loanOriginatorMappingRepository, never()).findByLoanIdWithOriginatorDetails(any()); + } + + @Test + void testEnrich_NullMappings() { + // Given + when(loanOriginatorMappingRepository.findByLoanIdWithOriginatorDetails(loanId)).thenReturn(null); + + // When + enricher.enrich(loanAccountData); + + // Then + verify(loanOriginatorMappingRepository).findByLoanIdWithOriginatorDetails(loanId); + verify(loanOriginatorAvroMapper, never()).toAvro(any()); + assertNull(loanAccountData.getOriginators()); + } + + @Test + void testEnrich_NullOriginatorInMapping() { + // Given + final LoanOriginatorMapping mapping = mock(LoanOriginatorMapping.class); + when(mapping.getOriginator()).thenReturn(null); + final List mappings = List.of(mapping); + + when(loanOriginatorMappingRepository.findByLoanIdWithOriginatorDetails(loanId)).thenReturn(mappings); + + // When + enricher.enrich(loanAccountData); + + // Then + verify(loanOriginatorMappingRepository).findByLoanIdWithOriginatorDetails(loanId); + verify(loanOriginatorAvroMapper, never()).toAvro(any()); + assertNull(loanAccountData.getOriginators()); + } + + private LoanOriginator createTestOriginator(final Long id, final String externalId, final String name) { + final LoanOriginator originator = LoanOriginator.create(new ExternalId(externalId), name, LoanOriginatorStatus.ACTIVE, null, null); + originator.setId(id); + return originator; + } + + private OriginatorDetailsV1 createOriginatorDetailsV1(final Long id, final String externalId, final String name) { + return OriginatorDetailsV1.newBuilder().setId(id).setExternalId(externalId).setName(name).setStatus("ACTIVE") + .setOriginatorType(null).setChannelType(null).build(); + } +} diff --git a/fineract-loan-origination/src/test/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanOriginatorAvroMapperTest.java b/fineract-loan-origination/src/test/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanOriginatorAvroMapperTest.java new file mode 100644 index 000000000..9ee9bab61 --- /dev/null +++ b/fineract-loan-origination/src/test/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanOriginatorAvroMapperTest.java @@ -0,0 +1,122 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanorigination.enricher; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; + +import org.apache.fineract.avro.loan.v1.OriginatorDetailsV1; +import org.apache.fineract.infrastructure.codes.domain.CodeValue; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginator; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +class LoanOriginatorAvroMapperTest { + + private LoanOriginatorAvroMapper mapper; + + @BeforeEach + void setUp() { + mapper = new LoanOriginatorAvroMapper(); + } + + @Test + void testToAvro_WithAllFields() { + // Given + final CodeValue originatorType = createCodeValue(1L, "MERCHANT", "Merchant"); + final CodeValue channelType = createCodeValue(2L, "ONLINE", "Online"); + final LoanOriginator originator = LoanOriginator.create(new ExternalId("test-external-id"), "Test Originator", + LoanOriginatorStatus.ACTIVE, originatorType, channelType); + originator.setId(100L); + + // When + final OriginatorDetailsV1 result = mapper.toAvro(originator); + + // Then + assertNotNull(result); + assertEquals(100L, result.getId()); + assertEquals("test-external-id", result.getExternalId()); + assertEquals("Test Originator", result.getName()); + assertEquals("ACTIVE", result.getStatus()); + assertNotNull(result.getOriginatorType()); + assertEquals(1L, result.getOriginatorType().getId()); + assertEquals("MERCHANT", result.getOriginatorType().getName()); + assertNotNull(result.getChannelType()); + assertEquals(2L, result.getChannelType().getId()); + assertEquals("ONLINE", result.getChannelType().getName()); + } + + @Test + void testToAvro_WithNullFields() { + // Given + final LoanOriginator originator = LoanOriginator.create(new ExternalId("test-external-id"), null, LoanOriginatorStatus.PENDING, + null, null); + originator.setId(200L); + + // When + final OriginatorDetailsV1 result = mapper.toAvro(originator); + + // Then + assertNotNull(result); + assertEquals(200L, result.getId()); + assertEquals("test-external-id", result.getExternalId()); + assertNull(result.getName()); + assertEquals("PENDING", result.getStatus()); + assertNull(result.getOriginatorType()); + assertNull(result.getChannelType()); + } + + @Test + void testToAvro_NullOriginator() { + // When + final OriginatorDetailsV1 result = mapper.toAvro(null); + + // Then + assertNull(result); + } + + @Test + void testToAvro_NullExternalId() { + // Given + final LoanOriginator originator = LoanOriginator.create(null, "Test", LoanOriginatorStatus.ACTIVE, null, null); + originator.setId(300L); + + // When + final OriginatorDetailsV1 result = mapper.toAvro(originator); + + // Then + assertNotNull(result); + assertEquals(300L, result.getId()); + assertNull(result.getExternalId()); + } + + private CodeValue createCodeValue(final Long id, final String label, final String description) { + final CodeValue codeValue = new CodeValue(); + codeValue.setId(id); + codeValue.setLabel(label); + codeValue.setDescription(description); + codeValue.setPosition(1); + codeValue.setActive(true); + codeValue.setMandatory(false); + return codeValue; + } +} diff --git a/fineract-loan-origination/src/test/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanTransactionDataV1OriginatorEnricherTest.java b/fineract-loan-origination/src/test/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanTransactionDataV1OriginatorEnricherTest.java new file mode 100644 index 000000000..901b84fc3 --- /dev/null +++ b/fineract-loan-origination/src/test/java/org/apache/fineract/portfolio/loanorigination/enricher/LoanTransactionDataV1OriginatorEnricherTest.java @@ -0,0 +1,165 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanorigination.enricher; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertNull; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import java.util.Collections; +import java.util.List; +import org.apache.fineract.avro.loan.v1.LoanTransactionDataV1; +import org.apache.fineract.avro.loan.v1.OriginatorDetailsV1; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginator; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMapping; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMappingRepository; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorStatus; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +@ExtendWith(MockitoExtension.class) +class LoanTransactionDataV1OriginatorEnricherTest { + + @Mock + private LoanOriginatorMappingRepository loanOriginatorMappingRepository; + + @Mock + private LoanOriginatorAvroMapper loanOriginatorAvroMapper; + + @InjectMocks + private LoanTransactionDataV1OriginatorEnricher enricher; + + private LoanTransactionDataV1 loanTransactionData; + private Long loanId; + + @BeforeEach + void setUp() { + loanId = 1L; + loanTransactionData = new LoanTransactionDataV1(); + loanTransactionData.setLoanId(loanId); + } + + @Test + void testIsDataTypeSupported() { + assertTrue(enricher.isDataTypeSupported(LoanTransactionDataV1.class)); + } + + @Test + void testEnrich_WithOriginators() { + // Given + final LoanOriginator originator = createTestOriginator(1L, "test-originator-1", "Test Originator 1"); + final LoanOriginatorMapping mapping = LoanOriginatorMapping.create(loanId, originator); + final List mappings = List.of(mapping); + + final OriginatorDetailsV1 originatorDetails = createOriginatorDetailsV1(1L, "test-originator-1", "Test Originator 1"); + + when(loanOriginatorMappingRepository.findByLoanIdWithOriginatorDetails(loanId)).thenReturn(mappings); + when(loanOriginatorAvroMapper.toAvro(originator)).thenReturn(originatorDetails); + + // When + enricher.enrich(loanTransactionData); + + // Then + verify(loanOriginatorMappingRepository).findByLoanIdWithOriginatorDetails(loanId); + verify(loanOriginatorAvroMapper).toAvro(originator); + assertNotNull(loanTransactionData.getOriginators()); + assertEquals(1, loanTransactionData.getOriginators().size()); + assertEquals("test-originator-1", loanTransactionData.getOriginators().getFirst().getExternalId()); + } + + @Test + void testEnrich_WithMultipleOriginators() { + // Given + final LoanOriginator originator1 = createTestOriginator(1L, "test-originator-1", "Test Originator 1"); + final LoanOriginator originator2 = createTestOriginator(2L, "test-originator-2", "Test Originator 2"); + final List mappings = List.of(LoanOriginatorMapping.create(loanId, originator1), + LoanOriginatorMapping.create(loanId, originator2)); + + final OriginatorDetailsV1 details1 = createOriginatorDetailsV1(1L, "test-originator-1", "Test Originator 1"); + final OriginatorDetailsV1 details2 = createOriginatorDetailsV1(2L, "test-originator-2", "Test Originator 2"); + + when(loanOriginatorMappingRepository.findByLoanIdWithOriginatorDetails(loanId)).thenReturn(mappings); + when(loanOriginatorAvroMapper.toAvro(originator1)).thenReturn(details1); + when(loanOriginatorAvroMapper.toAvro(originator2)).thenReturn(details2); + + // When + enricher.enrich(loanTransactionData); + + // Then + assertNotNull(loanTransactionData.getOriginators()); + assertEquals(2, loanTransactionData.getOriginators().size()); + } + + @Test + void testEnrich_NoOriginators() { + // Given + when(loanOriginatorMappingRepository.findByLoanIdWithOriginatorDetails(loanId)).thenReturn(Collections.emptyList()); + + // When + enricher.enrich(loanTransactionData); + + // Then + verify(loanOriginatorMappingRepository).findByLoanIdWithOriginatorDetails(loanId); + verify(loanOriginatorAvroMapper, never()).toAvro(any()); + assertNull(loanTransactionData.getOriginators()); + } + + @Test + void testEnrich_NullLoanId() { + // Given + loanTransactionData.setLoanId(null); + + // When + enricher.enrich(loanTransactionData); + + // Then + verify(loanOriginatorMappingRepository, never()).findByLoanIdWithOriginatorDetails(any()); + assertNull(loanTransactionData.getOriginators()); + } + + @Test + void testEnrich_NullData() { + // When + enricher.enrich(null); + + // Then + verify(loanOriginatorMappingRepository, never()).findByLoanIdWithOriginatorDetails(any()); + } + + private LoanOriginator createTestOriginator(final Long id, final String externalId, final String name) { + final LoanOriginator originator = LoanOriginator.create(new ExternalId(externalId), name, LoanOriginatorStatus.ACTIVE, null, null); + originator.setId(id); + return originator; + } + + private OriginatorDetailsV1 createOriginatorDetailsV1(final Long id, final String externalId, final String name) { + return OriginatorDetailsV1.newBuilder().setId(id).setExternalId(externalId).setName(name).setStatus("ACTIVE") + .setOriginatorType(null).setChannelType(null).build(); + } +}