This is an automated email from the ASF dual-hosted git repository.
arnold pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new c57f44d3f0 FINERACT-2418: Add originators details to external business
events
c57f44d3f0 is described below
commit c57f44d3f0fc266d9b11b11d29a83a9c7b056861
Author: mariiaKraievska <[email protected]>
AuthorDate: Mon Feb 2 15:50:24 2026 +0200
FINERACT-2418: Add originators details to external business events
---
fineract-loan-origination/dependencies.gradle | 4 +
.../domain/LoanOriginatorMappingRepository.java | 11 ++
.../LoanAccountDataV1OriginatorEnricher.java | 72 ++++++++
.../enricher/LoanOriginatorAvroMapper.java | 70 ++++++++
.../LoanTransactionDataV1OriginatorEnricher.java | 72 ++++++++
.../LoanAccountDataV1OriginatorEnricherTest.java | 198 +++++++++++++++++++++
.../enricher/LoanOriginatorAvroMapperTest.java | 122 +++++++++++++
...oanTransactionDataV1OriginatorEnricherTest.java | 165 +++++++++++++++++
8 files changed, 714 insertions(+)
diff --git a/fineract-loan-origination/dependencies.gradle
b/fineract-loan-origination/dependencies.gradle
index dee9b9db49..8a0d0643c9 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 257029fc12..c658d37f45 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<LoanOriginatorMapping, Long>,
JpaSpecificationExecutor<LoanOriginatorMapping> {
List<LoanOriginatorMapping> 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<LoanOriginatorMapping>
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 0000000000..85d3059fff
--- /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<LoanAccountDataV1> {
+
+ private final LoanOriginatorMappingRepository
loanOriginatorMappingRepository;
+ private final LoanOriginatorAvroMapper loanOriginatorAvroMapper;
+
+ @Override
+ public boolean isDataTypeSupported(final Class<LoanAccountDataV1>
dataType) {
+ return dataType.isAssignableFrom(LoanAccountDataV1.class);
+ }
+
+ @Override
+ public void enrich(final LoanAccountDataV1 data) {
+ if (data == null || data.getId() == null) {
+ return;
+ }
+
+ final List<LoanOriginatorMapping> mappings =
loanOriginatorMappingRepository.findByLoanIdWithOriginatorDetails(data.getId());
+ if (mappings == null || mappings.isEmpty()) {
+ return;
+ }
+
+ final List<OriginatorDetailsV1> 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 0000000000..0e04ae3ed7
--- /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 0000000000..721932d3ee
--- /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<LoanTransactionDataV1> {
+
+ private final LoanOriginatorMappingRepository
loanOriginatorMappingRepository;
+ private final LoanOriginatorAvroMapper loanOriginatorAvroMapper;
+
+ @Override
+ public boolean isDataTypeSupported(final Class<LoanTransactionDataV1>
dataType) {
+ return dataType.isAssignableFrom(LoanTransactionDataV1.class);
+ }
+
+ @Override
+ public void enrich(final LoanTransactionDataV1 data) {
+ if (data == null || data.getLoanId() == null) {
+ return;
+ }
+
+ final List<LoanOriginatorMapping> mappings =
loanOriginatorMappingRepository.findByLoanIdWithOriginatorDetails(data.getLoanId());
+ if (mappings == null || mappings.isEmpty()) {
+ return;
+ }
+
+ final List<OriginatorDetailsV1> 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 0000000000..0dee5f95ad
--- /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<LoanOriginatorMapping> 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<LoanOriginatorMapping> 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<LoanOriginatorMapping> 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 0000000000..9ee9bab614
--- /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 0000000000..901b84fc32
--- /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<LoanOriginatorMapping> 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<LoanOriginatorMapping> 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();
+ }
+}