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);
+ }
+}