This is an automated email from the ASF dual-hosted git repository.

adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git


The following commit(s) were added to refs/heads/develop by this push:
     new 99861ce93a FINERACT-2418: add originator during the loan application
99861ce93a is described below

commit 99861ce93ac994facae3262a22270da83da0a4b6
Author: Attila Budai <[email protected]>
AuthorDate: Wed Feb 4 13:25:33 2026 +0100

    FINERACT-2418: add originator during the loan application
---
 .../data/LoanApplicationOriginatorData.java        |  35 ++++
 .../LoanOriginatorCreationNotAllowedException.java |  30 +++
 .../LoanApplicationOriginatorDataValidator.java    | 120 ++++++++++++
 .../service/LoanOriginatorLinkingServiceImpl.java  | 158 +++++++++++++++
 .../service/LoanOriginatorLinkingService.java      |  35 ++++
 .../service/LoanOriginatorLinkingServiceNoOp.java  |  37 ++++
 ...ationWritePlatformServiceJpaRepositoryImpl.java |   8 +
 .../starter/LoanAccountConfiguration.java          |   6 +-
 .../savings/SavingsImportHandlerTest.java          |  21 +-
 .../helpers/FeignGlobalConfigurationHelper.java    |  61 ++++++
 .../client/feign/helpers/FeignLoanHelper.java      |  43 ++++
 .../FeignLoanOriginatorDuringApplicationTest.java  | 218 +++++++++++++++++++++
 12 files changed, 767 insertions(+), 5 deletions(-)

diff --git 
a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/data/LoanApplicationOriginatorData.java
 
b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/data/LoanApplicationOriginatorData.java
new file mode 100644
index 0000000000..3770d51e41
--- /dev/null
+++ 
b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/data/LoanApplicationOriginatorData.java
@@ -0,0 +1,35 @@
+/**
+ * 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.data;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class LoanApplicationOriginatorData {
+
+    private Long id;
+    private String externalId;
+    private String name;
+    private Long typeId;
+    private Long channelTypeId;
+}
diff --git 
a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorCreationNotAllowedException.java
 
b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorCreationNotAllowedException.java
new file mode 100644
index 0000000000..7d4460931d
--- /dev/null
+++ 
b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorCreationNotAllowedException.java
@@ -0,0 +1,30 @@
+/**
+ * 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.exception;
+
+import 
org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException;
+
+public class LoanOriginatorCreationNotAllowedException extends 
AbstractPlatformDomainRuleException {
+
+    public LoanOriginatorCreationNotAllowedException(String externalId) {
+        super("error.msg.loan.originator.creation.not.allowed", "Cannot create 
originator with externalId '" + externalId
+                + "' during loan application. Global configuration 
'enable-originator-creation-during-loan-application' is disabled.",
+                externalId);
+    }
+}
diff --git 
a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/serialization/LoanApplicationOriginatorDataValidator.java
 
b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/serialization/LoanApplicationOriginatorDataValidator.java
new file mode 100644
index 0000000000..bd12da69fb
--- /dev/null
+++ 
b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/serialization/LoanApplicationOriginatorDataValidator.java
@@ -0,0 +1,120 @@
+/**
+ * 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.serialization;
+
+import static 
org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.CHANNEL_TYPE_CODE_NAME;
+import static 
org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.CHANNEL_TYPE_ID_PARAM;
+import static 
org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.EXTERNAL_ID_PARAM;
+import static 
org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.ORIGINATOR_TYPE_CODE_NAME;
+import static 
org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.ORIGINATOR_TYPE_ID_PARAM;
+
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import java.util.ArrayList;
+import java.util.List;
+import lombok.RequiredArgsConstructor;
+import 
org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper;
+import 
org.apache.fineract.infrastructure.codes.exception.CodeValueNotFoundException;
+import org.apache.fineract.infrastructure.core.data.ApiParameterError;
+import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder;
+import 
org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException;
+import 
org.apache.fineract.portfolio.loanorigination.data.LoanApplicationOriginatorData;
+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 LoanApplicationOriginatorDataValidator {
+
+    private static final String RESOURCE_NAME = "loan.originator";
+    private static final String ID_PARAM = "id";
+    private static final String NAME_PARAM = "name";
+
+    private final CodeValueRepositoryWrapper codeValueRepositoryWrapper;
+
+    public LoanApplicationOriginatorData validateAndExtract(JsonObject 
jsonObject) {
+        final List<ApiParameterError> dataValidationErrors = new ArrayList<>();
+        final DataValidatorBuilder baseDataValidator = new 
DataValidatorBuilder(dataValidationErrors).resource(RESOURCE_NAME);
+
+        final Long id = extractLong(jsonObject, ID_PARAM);
+        final String externalId = extractString(jsonObject, EXTERNAL_ID_PARAM);
+
+        if (id == null && (externalId == null || externalId.isBlank())) {
+            
baseDataValidator.reset().parameter(ID_PARAM).failWithCode("or.externalId.required",
+                    "Either 'id' or 'externalId' must be provided for 
originator");
+        }
+
+        final String name = extractString(jsonObject, NAME_PARAM);
+        
baseDataValidator.reset().parameter(NAME_PARAM).value(name).ignoreIfNull().notExceedingLengthOf(255);
+
+        final Long typeId = extractLong(jsonObject, ORIGINATOR_TYPE_ID_PARAM);
+        if (typeId != null) {
+            validateCodeValue(typeId, ORIGINATOR_TYPE_CODE_NAME, 
ORIGINATOR_TYPE_ID_PARAM, baseDataValidator);
+        }
+
+        final Long channelTypeId = extractLong(jsonObject, 
CHANNEL_TYPE_ID_PARAM);
+        if (channelTypeId != null) {
+            validateCodeValue(channelTypeId, CHANNEL_TYPE_CODE_NAME, 
CHANNEL_TYPE_ID_PARAM, baseDataValidator);
+        }
+
+        throwExceptionIfValidationWarningsExist(dataValidationErrors);
+
+        return new LoanApplicationOriginatorData(id, externalId, name, typeId, 
channelTypeId);
+    }
+
+    private Long extractLong(JsonObject jsonObject, String paramName) {
+        if (jsonObject.has(paramName)) {
+            JsonElement element = jsonObject.get(paramName);
+            if (!element.isJsonNull()) {
+                try {
+                    return element.getAsLong();
+                } catch (NumberFormatException e) {
+                    return null;
+                }
+            }
+        }
+        return null;
+    }
+
+    private String extractString(JsonObject jsonObject, String paramName) {
+        if (jsonObject.has(paramName)) {
+            JsonElement element = jsonObject.get(paramName);
+            if (!element.isJsonNull()) {
+                return element.getAsString();
+            }
+        }
+        return null;
+    }
+
+    private void validateCodeValue(Long codeValueId, String codeName, String 
paramName, DataValidatorBuilder baseDataValidator) {
+        try {
+            
this.codeValueRepositoryWrapper.findOneByCodeNameAndIdWithNotFoundDetection(codeName,
 codeValueId);
+        } catch (CodeValueNotFoundException e) {
+            
baseDataValidator.reset().parameter(paramName).value(codeValueId).failWithCode("invalid.code.value",
+                    "Invalid code value id " + codeValueId + " for " + 
codeName);
+        }
+    }
+
+    private void throwExceptionIfValidationWarningsExist(final 
List<ApiParameterError> dataValidationErrors) {
+        if (!dataValidationErrors.isEmpty()) {
+            throw new PlatformApiDataValidationException(dataValidationErrors);
+        }
+    }
+}
diff --git 
a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorLinkingServiceImpl.java
 
b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorLinkingServiceImpl.java
new file mode 100644
index 0000000000..bc4115a6ed
--- /dev/null
+++ 
b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorLinkingServiceImpl.java
@@ -0,0 +1,158 @@
+/**
+ * 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.service;
+
+import static 
org.apache.fineract.infrastructure.configuration.api.GlobalConfigurationConstants.ENABLE_ORIGINATOR_CREATION_DURING_LOAN_APPLICATION;
+import static 
org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.CHANNEL_TYPE_CODE_NAME;
+import static 
org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.ORIGINATOR_TYPE_CODE_NAME;
+
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import java.util.HashSet;
+import java.util.Optional;
+import java.util.Set;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.fineract.infrastructure.codes.domain.CodeValue;
+import 
org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper;
+import 
org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationProperty;
+import 
org.apache.fineract.infrastructure.configuration.domain.GlobalConfigurationRepositoryWrapper;
+import 
org.apache.fineract.infrastructure.configuration.exception.GlobalConfigurationPropertyNotFoundException;
+import org.apache.fineract.infrastructure.core.domain.ExternalId;
+import 
org.apache.fineract.portfolio.loanaccount.service.LoanOriginatorLinkingService;
+import 
org.apache.fineract.portfolio.loanorigination.data.LoanApplicationOriginatorData;
+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.LoanOriginatorRepository;
+import 
org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorStatus;
+import 
org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorCreationNotAllowedException;
+import 
org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorNotActiveException;
+import 
org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorNotFoundException;
+import 
org.apache.fineract.portfolio.loanorigination.serialization.LoanApplicationOriginatorDataValidator;
+import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+/**
+ * Implementation of {@link LoanOriginatorLinkingService} that handles 
processing of originators during loan
+ * application. This service is active only when the loan-origination module 
is enabled.
+ */
+@Slf4j
+@Service("loanOriginatorLinkingServiceImpl")
+@RequiredArgsConstructor
+@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", 
havingValue = "true")
+public class LoanOriginatorLinkingServiceImpl implements 
LoanOriginatorLinkingService {
+
+    private final LoanOriginatorRepository loanOriginatorRepository;
+    private final LoanOriginatorMappingRepository 
loanOriginatorMappingRepository;
+    private final LoanApplicationOriginatorDataValidator validator;
+    private final GlobalConfigurationRepositoryWrapper 
globalConfigurationRepository;
+    private final CodeValueRepositoryWrapper codeValueRepositoryWrapper;
+
+    @Transactional
+    @Override
+    public void processOriginatorsForLoanApplication(Long loanId, JsonArray 
originatorsArray) {
+        if (originatorsArray == null || originatorsArray.isEmpty()) {
+            return;
+        }
+
+        log.debug("Processing {} originators for loan application {}", 
originatorsArray.size(), loanId);
+
+        Set<Long> attachedOriginatorIds = new HashSet<>();
+
+        for (JsonElement element : originatorsArray) {
+            if (!element.isJsonObject()) {
+                continue;
+            }
+
+            JsonObject jsonObject = element.getAsJsonObject();
+            LoanApplicationOriginatorData originatorData = 
validator.validateAndExtract(jsonObject);
+            LoanOriginator originator = 
resolveOrCreateOriginator(originatorData);
+
+            if (attachedOriginatorIds.contains(originator.getId())) {
+                log.debug("Originator {} already attached to loan {}, skipping 
duplicate", originator.getId(), loanId);
+                continue;
+            }
+
+            if (originator.getStatus() != LoanOriginatorStatus.ACTIVE) {
+                throw new LoanOriginatorNotActiveException(originator.getId(), 
originator.getStatus().getValue());
+            }
+
+            if 
(!loanOriginatorMappingRepository.existsByLoanIdAndOriginatorId(loanId, 
originator.getId())) {
+                LoanOriginatorMapping mapping = 
LoanOriginatorMapping.create(loanId, originator);
+                loanOriginatorMappingRepository.save(mapping);
+                log.debug("Attached originator {} to loan {}", 
originator.getId(), loanId);
+            }
+
+            attachedOriginatorIds.add(originator.getId());
+        }
+    }
+
+    private LoanOriginator 
resolveOrCreateOriginator(LoanApplicationOriginatorData originatorData) {
+        if (originatorData.getId() != null) {
+            return loanOriginatorRepository.findById(originatorData.getId())
+                    .orElseThrow(() -> new 
LoanOriginatorNotFoundException(originatorData.getId()));
+        }
+
+        String externalId = originatorData.getExternalId();
+        Optional<LoanOriginator> existingOriginator = 
loanOriginatorRepository.findByExternalId(new ExternalId(externalId));
+
+        if (existingOriginator.isPresent()) {
+            return existingOriginator.get();
+        }
+
+        if (!isOriginatorCreationDuringLoanApplicationEnabled()) {
+            throw new LoanOriginatorCreationNotAllowedException(externalId);
+        }
+
+        return createNewOriginator(originatorData);
+    }
+
+    private boolean isOriginatorCreationDuringLoanApplicationEnabled() {
+        try {
+            GlobalConfigurationProperty config = globalConfigurationRepository
+                    
.findOneByNameWithNotFoundDetection(ENABLE_ORIGINATOR_CREATION_DURING_LOAN_APPLICATION);
+            return config.isEnabled();
+        } catch (GlobalConfigurationPropertyNotFoundException e) {
+            log.warn("Global configuration '{}' not found, defaulting to 
disabled", ENABLE_ORIGINATOR_CREATION_DURING_LOAN_APPLICATION);
+            return false;
+        }
+    }
+
+    private LoanOriginator createNewOriginator(LoanApplicationOriginatorData 
data) {
+        log.info("Creating new originator with externalId: {} during loan 
application", data.getExternalId());
+
+        CodeValue originatorType = resolveCodeValue(data.getTypeId(), 
ORIGINATOR_TYPE_CODE_NAME);
+        CodeValue channelType = resolveCodeValue(data.getChannelTypeId(), 
CHANNEL_TYPE_CODE_NAME);
+
+        LoanOriginator originator = LoanOriginator.create(new 
ExternalId(data.getExternalId()), data.getName(), LoanOriginatorStatus.ACTIVE,
+                originatorType, channelType);
+
+        return loanOriginatorRepository.saveAndFlush(originator);
+    }
+
+    private CodeValue resolveCodeValue(Long codeValueId, String codeName) {
+        if (codeValueId == null) {
+            return null;
+        }
+        return 
codeValueRepositoryWrapper.findOneByCodeNameAndIdWithNotFoundDetection(codeName,
 codeValueId);
+    }
+}
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanOriginatorLinkingService.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanOriginatorLinkingService.java
new file mode 100644
index 0000000000..4d585c2647
--- /dev/null
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanOriginatorLinkingService.java
@@ -0,0 +1,35 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanaccount.service;
+
+import com.google.gson.JsonArray;
+
+public interface LoanOriginatorLinkingService {
+
+    /**
+     * Process originators provided during loan application. Creates new 
originators if allowed by global config, then
+     * attaches them to the loan.
+     *
+     * @param loanId
+     *            the loan ID to attach originators to
+     * @param originatorsArray
+     *            JSON array of originator data from loan request
+     */
+    void processOriginatorsForLoanApplication(Long loanId, JsonArray 
originatorsArray);
+}
diff --git 
a/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanOriginatorLinkingServiceNoOp.java
 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanOriginatorLinkingServiceNoOp.java
new file mode 100644
index 0000000000..5f41e37271
--- /dev/null
+++ 
b/fineract-loan/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanOriginatorLinkingServiceNoOp.java
@@ -0,0 +1,37 @@
+/**
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied. See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.fineract.portfolio.loanaccount.service;
+
+import com.google.gson.JsonArray;
+import 
org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
+import org.springframework.stereotype.Service;
+
+/**
+ * No-op implementation of {@link LoanOriginatorLinkingService} that is used 
when the loan-origination module is
+ * disabled. When originator data is provided during loan application, this 
implementation silently ignores it.
+ */
+@Service
+@ConditionalOnMissingBean(name = "loanOriginatorLinkingServiceImpl")
+public class LoanOriginatorLinkingServiceNoOp implements 
LoanOriginatorLinkingService {
+
+    @Override
+    public void processOriginatorsForLoanApplication(Long loanId, JsonArray 
originatorsArray) {
+        // No-op when loan-origination module is not enabled
+    }
+}
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java
index a27ea347fe..69ac18b198 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/service/LoanApplicationWritePlatformServiceJpaRepositoryImpl.java
@@ -129,6 +129,7 @@ public class 
LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
     private final LoanAccrualsProcessingService loanAccrualsProcessingService;
     private final LoanDownPaymentTransactionValidator 
loanDownPaymentTransactionValidator;
     private final LoanScheduleService loanScheduleService;
+    private final LoanOriginatorLinkingService loanOriginatorLinkingService;
 
     @Transactional
     @Override
@@ -166,6 +167,13 @@ public class 
LoanApplicationWritePlatformServiceJpaRepositoryImpl implements Loa
             // Check mandatory datatable entries were created
             
this.entityDatatableChecksWritePlatformService.runTheCheckForProduct(loan.getId(),
 EntityTables.LOAN.getName(),
                     StatusEnum.CREATE.getValue(), 
EntityTables.LOAN.getForeignKeyColumnNameOnDatatable(), loan.productId());
+            // Process originators if provided
+            if (command.parameterExists(LoanApiConstants.ORIGINATORS_PARAM)) {
+                final JsonArray originatorsArray = 
command.arrayOfParameterNamed(LoanApiConstants.ORIGINATORS_PARAM);
+                if (originatorsArray != null && !originatorsArray.isEmpty()) {
+                    
this.loanOriginatorLinkingService.processOriginatorsForLoanApplication(loan.getId(),
 originatorsArray);
+                }
+            }
             // Trigger business event
             businessEventNotifierService.notifyPostBusinessEvent(new 
LoanCreatedBusinessEvent(loan));
             // Building response
diff --git 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
index 0afb7b400c..5a0e5f0139 100644
--- 
a/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
+++ 
b/fineract-provider/src/main/java/org/apache/fineract/portfolio/loanaccount/starter/LoanAccountConfiguration.java
@@ -150,6 +150,7 @@ import 
org.apache.fineract.portfolio.loanaccount.service.LoanDownPaymentHandlerS
 import 
org.apache.fineract.portfolio.loanaccount.service.LoanJournalEntryPoster;
 import 
org.apache.fineract.portfolio.loanaccount.service.LoanMaximumAmountCalculator;
 import org.apache.fineract.portfolio.loanaccount.service.LoanOfficerService;
+import 
org.apache.fineract.portfolio.loanaccount.service.LoanOriginatorLinkingService;
 import 
org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService;
 import 
org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformServiceImpl;
 import org.apache.fineract.portfolio.loanaccount.service.LoanRefundService;
@@ -230,13 +231,14 @@ public class LoanAccountConfiguration {
             EntityDatatableChecksWritePlatformService 
entityDatatableChecksWritePlatformService, GLIMAccountInfoRepository 
glimRepository,
             LoanRepository loanRepository, GSIMReadPlatformService 
gsimReadPlatformService,
             LoanLifecycleStateMachine loanLifecycleStateMachine, 
LoanAccrualsProcessingService loanAccrualsProcessingService,
-            LoanDownPaymentTransactionValidator 
loanDownPaymentTransactionValidator, LoanScheduleService loanScheduleService) {
+            LoanDownPaymentTransactionValidator 
loanDownPaymentTransactionValidator, LoanScheduleService loanScheduleService,
+            LoanOriginatorLinkingService loanOriginatorLinkingService) {
         return new 
LoanApplicationWritePlatformServiceJpaRepositoryImpl(context, 
loanApplicationTransitionValidator,
                 loanApplicationValidator, loanRepositoryWrapper, 
noteRepository, loanAssembler, calendarRepository,
                 calendarInstanceRepository, savingsAccountRepository, 
accountAssociationsRepository, businessEventNotifierService,
                 loanScheduleAssembler, loanUtilService, 
calendarReadPlatformService, entityDatatableChecksWritePlatformService,
                 glimRepository, loanRepository, gsimReadPlatformService, 
loanLifecycleStateMachine, loanAccrualsProcessingService,
-                loanDownPaymentTransactionValidator, loanScheduleService);
+                loanDownPaymentTransactionValidator, loanScheduleService, 
loanOriginatorLinkingService);
     }
 
     @Bean
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/savings/SavingsImportHandlerTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/savings/SavingsImportHandlerTest.java
index 86603aa182..a1f35aad23 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/savings/SavingsImportHandlerTest.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/bulkimport/importhandler/savings/SavingsImportHandlerTest.java
@@ -48,6 +48,8 @@ import 
org.apache.fineract.integrationtests.common.organisation.StaffHelper;
 import 
org.apache.fineract.integrationtests.common.savings.SavingsAccountHelper;
 import 
org.apache.fineract.integrationtests.common.savings.SavingsProductHelper;
 import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.ss.usermodel.Cell;
+import org.apache.poi.ss.usermodel.CellType;
 import org.apache.poi.ss.usermodel.Row;
 import org.apache.poi.ss.usermodel.Sheet;
 import org.apache.poi.ss.usermodel.Workbook;
@@ -145,8 +147,7 @@ public class SavingsImportHandlerTest {
                 
.setCellValue(savingsProductSheet.getRow(1).getCell(10).getStringCellValue());
         firstSavingsRow.createCell(SavingsConstants.DECIMAL_PLACES_COL)
                 
.setCellValue(savingsProductSheet.getRow(1).getCell(11).getNumericCellValue());
-        firstSavingsRow.createCell(SavingsConstants.IN_MULTIPLES_OF_COL)
-                
.setCellValue(savingsProductSheet.getRow(1).getCell(12).getNumericCellValue());
+        safeNumericValueSetter(firstSavingsRow, 
SavingsConstants.IN_MULTIPLES_OF_COL, savingsProductSheet, 1, 12);
         
firstSavingsRow.createCell(SavingsConstants.NOMINAL_ANNUAL_INTEREST_RATE_COL)
                 
.setCellValue(savingsProductSheet.getRow(1).getCell(2).getNumericCellValue());
         
firstSavingsRow.createCell(SavingsConstants.INTEREST_COMPOUNDING_PERIOD_COL)
@@ -181,7 +182,7 @@ public class SavingsImportHandlerTest {
         Assertions.assertNotNull(importDocumentId);
 
         // Wait for the creation of output excel
-        Thread.sleep(10000);
+        Thread.sleep(1000);
 
         // check status column of output excel
         String location = 
savingsAccountHelper.getOutputTemplateLocation(importDocumentId);
@@ -196,4 +197,18 @@ public class SavingsImportHandlerTest {
         Assertions.assertEquals("Imported", 
row.getCell(SavingsConstants.STATUS_COL).getStringCellValue());
         Outputworkbook.close();
     }
+
+    private void safeNumericValueSetter(Row targetRow, int targetColId, Sheet 
sourceSheet, int rowId, int colId) {
+        Row row = sourceSheet.getRow(rowId);
+        if (row == null) {
+            targetRow.createCell(targetColId).setBlank();
+        } else {
+            Cell cell = row.getCell(colId);
+            if (cell == null || cell.getCellType() == CellType.BLANK) {
+                targetRow.createCell(targetColId).setBlank();
+            } else {
+                
targetRow.createCell(targetColId).setCellValue(cell.getNumericCellValue());
+            }
+        }
+    }
 }
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignGlobalConfigurationHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignGlobalConfigurationHelper.java
new file mode 100644
index 0000000000..a2ae1f1cc8
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignGlobalConfigurationHelper.java
@@ -0,0 +1,61 @@
+/**
+ * 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.integrationtests.client.feign.helpers;
+
+import static org.apache.fineract.client.feign.util.FeignCalls.ok;
+
+import java.util.List;
+import org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.client.models.GetGlobalConfigurationsResponse;
+import org.apache.fineract.client.models.GlobalConfigurationPropertyData;
+import org.apache.fineract.client.models.PutGlobalConfigurationsRequest;
+
+public class FeignGlobalConfigurationHelper {
+
+    private final FineractFeignClient fineractClient;
+
+    public FeignGlobalConfigurationHelper(FineractFeignClient fineractClient) {
+        this.fineractClient = fineractClient;
+    }
+
+    public void enableOriginatorCreationDuringLoanApplication() {
+        
updateConfigurationByName("enable-originator-creation-during-loan-application", 
true);
+    }
+
+    public void disableOriginatorCreationDuringLoanApplication() {
+        
updateConfigurationByName("enable-originator-creation-during-loan-application", 
false);
+    }
+
+    public void updateConfigurationByName(String configName, boolean enabled) {
+        Long configId = getConfigurationIdByName(configName);
+        ok(() -> 
fineractClient.globalConfiguration().updateConfiguration1(configId,
+                new PutGlobalConfigurationsRequest().enabled(enabled)));
+    }
+
+    public Long getConfigurationIdByName(String configName) {
+        List<GlobalConfigurationPropertyData> configs = getConfigurationList();
+        return configs.stream().filter(c -> 
configName.equals(c.getName())).findFirst().map(GlobalConfigurationPropertyData::getId)
+                .orElseThrow(() -> new RuntimeException("Configuration not 
found: " + configName));
+    }
+
+    private List<GlobalConfigurationPropertyData> getConfigurationList() {
+        GetGlobalConfigurationsResponse response = ok(() -> 
fineractClient.globalConfiguration().retrieveConfiguration(false));
+        return response.getGlobalConfiguration();
+    }
+}
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignLoanHelper.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignLoanHelper.java
index f000891892..5cf0c3bced 100644
--- 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignLoanHelper.java
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignLoanHelper.java
@@ -18,16 +18,20 @@
  */
 package org.apache.fineract.integrationtests.client.feign.helpers;
 
+import static org.apache.fineract.client.feign.util.FeignCalls.fail;
 import static org.apache.fineract.client.feign.util.FeignCalls.ok;
 
 import java.math.BigDecimal;
+import java.util.List;
 import java.util.Map;
 import org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
 import org.apache.fineract.client.models.GetLoansLoanIdResponse;
 import org.apache.fineract.client.models.PostLoanProductsRequest;
 import org.apache.fineract.client.models.PostLoanProductsResponse;
 import org.apache.fineract.client.models.PostLoansLoanIdRequest;
 import org.apache.fineract.client.models.PostLoansLoanIdResponse;
+import org.apache.fineract.client.models.PostLoansOriginatorData;
 import org.apache.fineract.client.models.PostLoansRequest;
 import org.apache.fineract.client.models.PostLoansResponse;
 
@@ -170,4 +174,43 @@ public class FeignLoanHelper {
                 
.format(org.apache.fineract.integrationtests.common.Utils.getLocalDateOfTenant());
         return createSubmittedLoan(clientId, productId, todayDate, 10000.0, 
12);
     }
+
+    public Long createSubmittedLoanWithOriginators(Long clientId, 
List<PostLoansOriginatorData> originators) {
+        PostLoansRequest request = buildSubmittedLoanRequest(clientId);
+        request.setOriginators(originators);
+        PostLoansResponse response = ok(() -> 
fineractClient.loans().calculateLoanScheduleOrSubmitLoanApplication(request, 
(String) null));
+        return response.getLoanId();
+    }
+
+    public CallFailedRuntimeException 
createSubmittedLoanWithOriginatorsExpectingError(Long clientId,
+            List<PostLoansOriginatorData> originators) {
+        PostLoansRequest request = buildSubmittedLoanRequest(clientId);
+        request.setOriginators(originators);
+        return fail(() -> 
fineractClient.loans().calculateLoanScheduleOrSubmitLoanApplication(request, 
(String) null));
+    }
+
+    private PostLoansRequest buildSubmittedLoanRequest(Long clientId) {
+        Long productId = createSimpleLoanProduct();
+        String todayDate = 
org.apache.fineract.integrationtests.common.Utils.dateFormatter
+                
.format(org.apache.fineract.integrationtests.common.Utils.getLocalDateOfTenant());
+        return new PostLoansRequest()//
+                .clientId(clientId)//
+                .productId(productId)//
+                .loanType("individual")//
+                .submittedOnDate(todayDate)//
+                .expectedDisbursementDate(todayDate)//
+                .principal(BigDecimal.valueOf(10000.0))//
+                .loanTermFrequency(12)//
+                .loanTermFrequencyType(2)//
+                .numberOfRepayments(12)//
+                .repaymentEvery(1)//
+                .repaymentFrequencyType(2)//
+                .interestRatePerPeriod(BigDecimal.ZERO)//
+                .amortizationType(1)//
+                .interestType(0)//
+                .interestCalculationPeriodType(1)//
+                .transactionProcessingStrategyCode("mifos-standard-strategy")//
+                .locale("en")//
+                .dateFormat("dd MMMM yyyy");
+    }
 }
diff --git 
a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanOriginatorDuringApplicationTest.java
 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanOriginatorDuringApplicationTest.java
new file mode 100644
index 0000000000..8fb841e3b1
--- /dev/null
+++ 
b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanOriginatorDuringApplicationTest.java
@@ -0,0 +1,218 @@
+/**
+ * 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.integrationtests.client.feign.tests;
+
+import java.util.List;
+import org.apache.fineract.client.feign.FineractFeignClient;
+import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
+import org.apache.fineract.client.models.PostLoansOriginatorData;
+import org.apache.fineract.integrationtests.client.FeignIntegrationTest;
+import 
org.apache.fineract.integrationtests.client.feign.helpers.FeignClientHelper;
+import 
org.apache.fineract.integrationtests.client.feign.helpers.FeignGlobalConfigurationHelper;
+import 
org.apache.fineract.integrationtests.client.feign.helpers.FeignLoanHelper;
+import 
org.apache.fineract.integrationtests.client.feign.helpers.FeignLoanOriginatorHelper;
+import org.apache.fineract.integrationtests.common.FineractFeignClientHelper;
+import org.junit.jupiter.api.BeforeAll;
+import org.junit.jupiter.api.Order;
+import org.junit.jupiter.api.Test;
+
+@Order(2)
+public class FeignLoanOriginatorDuringApplicationTest extends 
FeignIntegrationTest {
+
+    private static FeignLoanOriginatorHelper originatorHelper;
+    private static FeignClientHelper clientHelper;
+    private static FeignLoanHelper loanHelper;
+    private static FeignGlobalConfigurationHelper configHelper;
+
+    @BeforeAll
+    public static void setup() {
+        FineractFeignClient fineractClient = 
FineractFeignClientHelper.getFineractFeignClient();
+        originatorHelper = new FeignLoanOriginatorHelper(fineractClient);
+        clientHelper = new FeignClientHelper(fineractClient);
+        loanHelper = new FeignLoanHelper(fineractClient);
+        configHelper = new FeignGlobalConfigurationHelper(fineractClient);
+    }
+
+    @Test
+    public void testCreateLoanWithExistingOriginatorById() {
+        final String originatorExternalId = 
FeignLoanOriginatorHelper.generateUniqueExternalId();
+        final Long originatorId = 
originatorHelper.createOriginator(originatorExternalId);
+        final Long clientId = clientHelper.createClient();
+
+        final List<PostLoansOriginatorData> originators = List.of(new 
PostLoansOriginatorData().id(originatorId));
+        final Long loanId = 
loanHelper.createSubmittedLoanWithOriginators(clientId, originators);
+
+        assertThat(loanId).isNotNull();
+        final var loanDetails = 
loanHelper.getLoanDetailsWithAssociations(loanId, "originators");
+        assertThat(loanDetails.getOriginators()).hasSize(1);
+        
assertThat(loanDetails.getOriginators().get(0).getId()).isEqualTo(originatorId);
+
+        originatorHelper.detachOriginatorFromLoan(loanId, originatorId);
+        originatorHelper.deleteOriginator(originatorId);
+    }
+
+    @Test
+    public void testCreateLoanWithExistingOriginatorByExternalId() {
+        final String originatorExternalId = 
FeignLoanOriginatorHelper.generateUniqueExternalId();
+        final Long originatorId = 
originatorHelper.createOriginator(originatorExternalId);
+        final Long clientId = clientHelper.createClient();
+
+        final List<PostLoansOriginatorData> originators = List.of(new 
PostLoansOriginatorData().externalId(originatorExternalId));
+        final Long loanId = 
loanHelper.createSubmittedLoanWithOriginators(clientId, originators);
+
+        assertThat(loanId).isNotNull();
+        final var loanDetails = 
loanHelper.getLoanDetailsWithAssociations(loanId, "originators");
+        assertThat(loanDetails.getOriginators()).hasSize(1);
+        
assertThat(loanDetails.getOriginators().get(0).getExternalId()).isEqualTo(originatorExternalId);
+
+        originatorHelper.detachOriginatorFromLoan(loanId, originatorId);
+        originatorHelper.deleteOriginator(originatorId);
+    }
+
+    @Test
+    public void testCreateLoanWithNewOriginatorWhenConfigEnabled() {
+        configHelper.enableOriginatorCreationDuringLoanApplication();
+
+        try {
+            final Long clientId = clientHelper.createClient();
+            final String newOriginatorExternalId = 
FeignLoanOriginatorHelper.generateUniqueExternalId();
+
+            final List<PostLoansOriginatorData> originators = List
+                    .of(new 
PostLoansOriginatorData().externalId(newOriginatorExternalId).name("New 
Merchant Created During Loan"));
+            final Long loanId = 
loanHelper.createSubmittedLoanWithOriginators(clientId, originators);
+
+            assertThat(loanId).isNotNull();
+
+            final var createdOriginator = 
originatorHelper.getOriginatorByExternalId(newOriginatorExternalId);
+            assertThat(createdOriginator).isNotNull();
+            assertThat(createdOriginator.getName()).isEqualTo("New Merchant 
Created During Loan");
+            assertThat(createdOriginator.getStatus()).isEqualTo("ACTIVE");
+
+            final var loanDetails = 
loanHelper.getLoanDetailsWithAssociations(loanId, "originators");
+            assertThat(loanDetails.getOriginators()).hasSize(1);
+            
assertThat(loanDetails.getOriginators().get(0).getExternalId()).isEqualTo(newOriginatorExternalId);
+
+            originatorHelper.detachOriginatorFromLoan(loanId, 
createdOriginator.getId());
+            originatorHelper.deleteOriginator(createdOriginator.getId());
+        } finally {
+            configHelper.disableOriginatorCreationDuringLoanApplication();
+        }
+    }
+
+    @Test
+    public void testCreateLoanWithNewOriginatorFailsWhenConfigDisabled() {
+        configHelper.disableOriginatorCreationDuringLoanApplication();
+
+        final Long clientId = clientHelper.createClient();
+        final String nonExistingExternalId = 
FeignLoanOriginatorHelper.generateUniqueExternalId();
+
+        final List<PostLoansOriginatorData> originators = List.of(new 
PostLoansOriginatorData().externalId(nonExistingExternalId));
+
+        final CallFailedRuntimeException exception = 
loanHelper.createSubmittedLoanWithOriginatorsExpectingError(clientId, 
originators);
+
+        assertThat(exception.getStatus()).isIn(403, 404);
+    }
+
+    @Test
+    public void testCreateLoanWithMultipleOriginators() {
+        final String externalId1 = 
FeignLoanOriginatorHelper.generateUniqueExternalId();
+        final String externalId2 = 
FeignLoanOriginatorHelper.generateUniqueExternalId();
+        final Long originatorId1 = 
originatorHelper.createOriginator(externalId1);
+        final Long originatorId2 = 
originatorHelper.createOriginator(externalId2);
+        final Long clientId = clientHelper.createClient();
+
+        final List<PostLoansOriginatorData> originators = List.of(new 
PostLoansOriginatorData().id(originatorId1),
+                new PostLoansOriginatorData().externalId(externalId2));
+        final Long loanId = 
loanHelper.createSubmittedLoanWithOriginators(clientId, originators);
+
+        final var loanDetails = 
loanHelper.getLoanDetailsWithAssociations(loanId, "originators");
+        assertThat(loanDetails.getOriginators()).hasSize(2);
+
+        originatorHelper.detachOriginatorFromLoan(loanId, originatorId1);
+        originatorHelper.detachOriginatorFromLoan(loanId, originatorId2);
+        originatorHelper.deleteOriginator(originatorId1);
+        originatorHelper.deleteOriginator(originatorId2);
+    }
+
+    @Test
+    public void testCreateLoanWithInvalidOriginatorDataReturns400() {
+        final Long clientId = clientHelper.createClient();
+
+        final List<PostLoansOriginatorData> originators = List.of(new 
PostLoansOriginatorData().name("Invalid - no id or externalId"));
+
+        final CallFailedRuntimeException exception = 
loanHelper.createSubmittedLoanWithOriginatorsExpectingError(clientId, 
originators);
+
+        assertThat(exception.getStatus()).isEqualTo(400);
+    }
+
+    @Test
+    public void testCreateLoanWithInactiveOriginatorReturns403() {
+        final String externalId = 
FeignLoanOriginatorHelper.generateUniqueExternalId();
+        final Long originatorId = 
originatorHelper.createOriginator(externalId, "Inactive Originator", 
"INACTIVE");
+        final Long clientId = clientHelper.createClient();
+
+        final List<PostLoansOriginatorData> originators = List.of(new 
PostLoansOriginatorData().id(originatorId));
+
+        final CallFailedRuntimeException exception = 
loanHelper.createSubmittedLoanWithOriginatorsExpectingError(clientId, 
originators);
+
+        assertThat(exception.getStatus()).isEqualTo(403);
+
+        originatorHelper.deleteOriginator(originatorId);
+    }
+
+    @Test
+    public void testCreateLoanWithNonExistingOriginatorIdReturns404() {
+        final Long clientId = clientHelper.createClient();
+
+        final List<PostLoansOriginatorData> originators = List.of(new 
PostLoansOriginatorData().id(999999L));
+
+        final CallFailedRuntimeException exception = 
loanHelper.createSubmittedLoanWithOriginatorsExpectingError(clientId, 
originators);
+
+        assertThat(exception.getStatus()).isEqualTo(404);
+    }
+
+    @Test
+    public void testCreateLoanWithoutOriginatorsStillWorks() {
+        final Long clientId = clientHelper.createClient();
+
+        final Long loanId = loanHelper.createSubmittedLoan(clientId);
+
+        assertThat(loanId).isNotNull();
+
+        final var loanDetails = 
loanHelper.getLoanDetailsWithAssociations(loanId, "originators");
+        assertThat(loanDetails.getOriginators()).isEmpty();
+    }
+
+    @Test
+    public void testCreateLoanWithDuplicateOriginatorInListAttachesOnce() {
+        final String externalId = 
FeignLoanOriginatorHelper.generateUniqueExternalId();
+        final Long originatorId = 
originatorHelper.createOriginator(externalId);
+        final Long clientId = clientHelper.createClient();
+
+        final List<PostLoansOriginatorData> originators = List.of(new 
PostLoansOriginatorData().id(originatorId),
+                new PostLoansOriginatorData().externalId(externalId));
+        final Long loanId = 
loanHelper.createSubmittedLoanWithOriginators(clientId, originators);
+
+        final var loanDetails = 
loanHelper.getLoanDetailsWithAssociations(loanId, "originators");
+        assertThat(loanDetails.getOriginators()).hasSize(1);
+
+        originatorHelper.detachOriginatorFromLoan(loanId, originatorId);
+        originatorHelper.deleteOriginator(originatorId);
+    }
+}

Reply via email to