FINERACT-2418: add originator during the loan application

This commit is contained in:
Attila Budai
2026-02-09 14:40:34 +01:00
committed by Adam Saghy
parent f5951f1a7b
commit 99861ce93a
12 changed files with 767 additions and 5 deletions
@@ -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;
}
@@ -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);
}
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -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);
}
@@ -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
}
}
@@ -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
@@ -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
@@ -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());
}
}
}
}
@@ -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();
}
}
@@ -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");
}
}
@@ -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);
}
}