diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/domain/LoanOriginatorRepository.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/domain/LoanOriginatorRepository.java index 09c29692403..99bf4a5c1bc 100644 --- a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/domain/LoanOriginatorRepository.java +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/domain/LoanOriginatorRepository.java @@ -23,6 +23,8 @@ import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.JpaSpecificationExecutor; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; public interface LoanOriginatorRepository extends JpaRepository, JpaSpecificationExecutor { @@ -31,4 +33,13 @@ public interface LoanOriginatorRepository extends JpaRepository findByStatus(LoanOriginatorStatus status); + + @Query("SELECT lo FROM LoanOriginator lo LEFT JOIN FETCH lo.originatorType LEFT JOIN FETCH lo.channelType") + List findAllWithCodeValues(); + + @Query("SELECT lo FROM LoanOriginator lo LEFT JOIN FETCH lo.originatorType LEFT JOIN FETCH lo.channelType WHERE lo.id = :id") + Optional findByIdWithCodeValues(@Param("id") Long id); + + @Query("SELECT lo FROM LoanOriginator lo LEFT JOIN FETCH lo.originatorType LEFT JOIN FETCH lo.channelType WHERE lo.externalId = :externalId") + Optional findByExternalIdWithCodeValues(@Param("externalId") ExternalId externalId); } diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorCannotBeDeletedException.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorCannotBeDeletedException.java new file mode 100644 index 00000000000..a5c6fe265e5 --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorCannotBeDeletedException.java @@ -0,0 +1,29 @@ +/** + * 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 LoanOriginatorCannotBeDeletedException extends AbstractPlatformDomainRuleException { + + public LoanOriginatorCannotBeDeletedException(Long id) { + super("error.msg.loan.originator.cannot.be.deleted.mapped.to.loan", + "Loan Originator with id " + id + " cannot be deleted as it is mapped to one or more loans", id); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorDuplicateExternalIdException.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorDuplicateExternalIdException.java new file mode 100644 index 00000000000..33612c182ba --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorDuplicateExternalIdException.java @@ -0,0 +1,29 @@ +/** + * 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 LoanOriginatorDuplicateExternalIdException extends AbstractPlatformDomainRuleException { + + public LoanOriginatorDuplicateExternalIdException(String externalId) { + super("error.msg.loan.originator.duplicate.external.id", "Loan Originator with external id '" + externalId + "' already exists", + externalId); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorInvalidStatusException.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorInvalidStatusException.java new file mode 100644 index 00000000000..3b8630dbf1b --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorInvalidStatusException.java @@ -0,0 +1,34 @@ +/** + * 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 LoanOriginatorInvalidStatusException extends AbstractPlatformDomainRuleException { + + public LoanOriginatorInvalidStatusException(String status) { + super("error.msg.loan.originator.invalid.status", + "Invalid loan originator status: " + status + ". Valid values are: ACTIVE, PENDING, INACTIVE", status); + } + + public LoanOriginatorInvalidStatusException(String status, Throwable cause) { + super("error.msg.loan.originator.invalid.status", + "Invalid loan originator status: " + status + ". Valid values are: ACTIVE, PENDING, INACTIVE", status, cause); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorNotFoundException.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorNotFoundException.java new file mode 100644 index 00000000000..eec740b2069 --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorNotFoundException.java @@ -0,0 +1,33 @@ +/** + * 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.AbstractPlatformResourceNotFoundException; + +public class LoanOriginatorNotFoundException extends AbstractPlatformResourceNotFoundException { + + public LoanOriginatorNotFoundException(Long id) { + super("error.msg.loan.originator.id.not.found", "Loan Originator with id " + id + " not found", id); + } + + public LoanOriginatorNotFoundException(String externalId) { + super("error.msg.loan.originator.external.id.not.found", "Loan Originator with external id " + externalId + " not found", + externalId); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/CreateLoanOriginatorCommandHandler.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/CreateLoanOriginatorCommandHandler.java new file mode 100644 index 00000000000..cd8fe7196d8 --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/CreateLoanOriginatorCommandHandler.java @@ -0,0 +1,42 @@ +/** + * 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.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanorigination.service.LoanOriginatorWritePlatformService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +@Service +@CommandType(entity = "LOAN_ORIGINATOR", action = "CREATE") +@RequiredArgsConstructor +@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", havingValue = "true") +public class CreateLoanOriginatorCommandHandler implements NewCommandSourceHandler { + + private final LoanOriginatorWritePlatformService writePlatformService; + + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.create(command); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/DeleteLoanOriginatorCommandHandler.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/DeleteLoanOriginatorCommandHandler.java new file mode 100644 index 00000000000..383a1828643 --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/DeleteLoanOriginatorCommandHandler.java @@ -0,0 +1,42 @@ +/** + * 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.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanorigination.service.LoanOriginatorWritePlatformService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +@Service +@CommandType(entity = "LOAN_ORIGINATOR", action = "DELETE") +@RequiredArgsConstructor +@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", havingValue = "true") +public class DeleteLoanOriginatorCommandHandler implements NewCommandSourceHandler { + + private final LoanOriginatorWritePlatformService writePlatformService; + + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.delete(command.entityId()); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/UpdateLoanOriginatorCommandHandler.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/UpdateLoanOriginatorCommandHandler.java new file mode 100644 index 00000000000..960fe8794fa --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/UpdateLoanOriginatorCommandHandler.java @@ -0,0 +1,43 @@ + +/** + * 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.handler; + +import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.annotation.CommandType; +import org.apache.fineract.commands.handler.NewCommandSourceHandler; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.portfolio.loanorigination.service.LoanOriginatorWritePlatformService; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; + +@Service +@CommandType(entity = "LOAN_ORIGINATOR", action = "UPDATE") +@RequiredArgsConstructor +@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", havingValue = "true") +public class UpdateLoanOriginatorCommandHandler implements NewCommandSourceHandler { + + private final LoanOriginatorWritePlatformService writePlatformService; + + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.update(command.entityId(), command); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/mapper/LoanOriginatorMapper.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/mapper/LoanOriginatorMapper.java new file mode 100644 index 00000000000..c891c6e0cb0 --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/mapper/LoanOriginatorMapper.java @@ -0,0 +1,40 @@ +/** + * 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.mapper; + +import java.util.List; +import org.apache.fineract.infrastructure.core.config.MapstructMapperConfig; +import org.apache.fineract.portfolio.loanorigination.data.LoanOriginatorData; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginator; +import org.mapstruct.Mapper; +import org.mapstruct.Mapping; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; + +@Mapper(config = MapstructMapperConfig.class) +@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", havingValue = "true") +public interface LoanOriginatorMapper { + + @Mapping(target = "originatorTypeId", source = "originatorType.id") + @Mapping(target = "channelTypeId", source = "channelType.id") + @Mapping(target = "externalId", source = "externalId") + @Mapping(target = "status", expression = "java(entity.getStatus().getValue())") + LoanOriginatorData toData(LoanOriginator entity); + + List toDataList(List entities); +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/serialization/LoanOriginatorDataValidator.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/serialization/LoanOriginatorDataValidator.java new file mode 100644 index 00000000000..dba7eeb67aa --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/serialization/LoanOriginatorDataValidator.java @@ -0,0 +1,121 @@ +/** + * 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.CREATE_REQUEST_PARAMS; +import static org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.EXTERNAL_ID_PARAM; +import static org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.NAME_PARAM; +import static org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.RESOURCE_NAME; +import static org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.STATUS_PARAM; +import static org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.UPDATE_REQUEST_PARAMS; + +import com.google.gson.JsonElement; +import com.google.gson.reflect.TypeToken; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.commons.lang3.StringUtils; +import org.apache.fineract.infrastructure.core.data.ApiParameterError; +import org.apache.fineract.infrastructure.core.data.DataValidatorBuilder; +import org.apache.fineract.infrastructure.core.exception.InvalidJsonException; +import org.apache.fineract.infrastructure.core.exception.PlatformApiDataValidationException; +import org.apache.fineract.infrastructure.core.serialization.FromJsonHelper; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorStatus; +import org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorInvalidStatusException; +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 LoanOriginatorDataValidator { + + private final FromJsonHelper fromApiJsonHelper; + + public void validateForCreate(final String json) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, CREATE_REQUEST_PARAMS); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource(RESOURCE_NAME); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final String externalId = this.fromApiJsonHelper.extractStringNamed(EXTERNAL_ID_PARAM, element); + baseDataValidator.reset().parameter(EXTERNAL_ID_PARAM).value(externalId).notBlank().notExceedingLengthOf(100); + + final String name = this.fromApiJsonHelper.extractStringNamed(NAME_PARAM, element); + baseDataValidator.reset().parameter(NAME_PARAM).value(name).ignoreIfNull().notExceedingLengthOf(255); + + if (this.fromApiJsonHelper.parameterExists(STATUS_PARAM, element)) { + final String status = this.fromApiJsonHelper.extractStringNamed(STATUS_PARAM, element); + baseDataValidator.reset().parameter(STATUS_PARAM).value(status).notBlank(); + validateStatus(status); + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + public void validateForUpdate(final String json) { + if (StringUtils.isBlank(json)) { + throw new InvalidJsonException(); + } + + final Type typeOfMap = new TypeToken>() {}.getType(); + this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, UPDATE_REQUEST_PARAMS); + + final List dataValidationErrors = new ArrayList<>(); + final DataValidatorBuilder baseDataValidator = new DataValidatorBuilder(dataValidationErrors).resource(RESOURCE_NAME); + + final JsonElement element = this.fromApiJsonHelper.parse(json); + + final String name = this.fromApiJsonHelper.extractStringNamed(NAME_PARAM, element); + baseDataValidator.reset().parameter(NAME_PARAM).value(name).ignoreIfNull().notExceedingLengthOf(255); + + if (this.fromApiJsonHelper.parameterExists(STATUS_PARAM, element)) { + final String status = this.fromApiJsonHelper.extractStringNamed(STATUS_PARAM, element); + baseDataValidator.reset().parameter(STATUS_PARAM).value(status).notBlank(); + validateStatus(status); + } + + throwExceptionIfValidationWarningsExist(dataValidationErrors); + } + + private void validateStatus(final String status) { + if (status != null) { + try { + LoanOriginatorStatus.fromString(status); + } catch (IllegalArgumentException e) { + throw new LoanOriginatorInvalidStatusException(status, e); + } + } + } + + private void throwExceptionIfValidationWarningsExist(final List dataValidationErrors) { + if (!dataValidationErrors.isEmpty()) { + throw new PlatformApiDataValidationException(dataValidationErrors); + } + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorReadPlatformServiceImpl.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorReadPlatformServiceImpl.java index dd9f84c3c6e..9913858760d 100644 --- a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorReadPlatformServiceImpl.java +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorReadPlatformServiceImpl.java @@ -19,33 +19,50 @@ package org.apache.fineract.portfolio.loanorigination.service; import java.util.List; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.portfolio.loanorigination.data.LoanOriginatorData; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginator; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorRepository; +import org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorNotFoundException; +import org.apache.fineract.portfolio.loanorigination.mapper.LoanOriginatorMapper; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service +@RequiredArgsConstructor +@Transactional(readOnly = true) @ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", havingValue = "true") public class LoanOriginatorReadPlatformServiceImpl implements LoanOriginatorReadPlatformService { - private static final String NOT_IMPLEMENTED_MESSAGE = "Not implemented yet"; + private final LoanOriginatorRepository loanOriginatorRepository; + private final LoanOriginatorMapper loanOriginatorMapper; @Override public List retrieveAll() { - throw new UnsupportedOperationException(NOT_IMPLEMENTED_MESSAGE); + final List originators = this.loanOriginatorRepository.findAllWithCodeValues(); + return this.loanOriginatorMapper.toDataList(originators); } @Override - public LoanOriginatorData retrieveById(Long id) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED_MESSAGE); + public LoanOriginatorData retrieveById(final Long id) { + final LoanOriginator originator = this.loanOriginatorRepository.findByIdWithCodeValues(id) + .orElseThrow(() -> new LoanOriginatorNotFoundException(id)); + return this.loanOriginatorMapper.toData(originator); } @Override - public LoanOriginatorData retrieveByExternalId(String externalId) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED_MESSAGE); + public LoanOriginatorData retrieveByExternalId(final String externalId) { + final LoanOriginator originator = this.loanOriginatorRepository.findByExternalIdWithCodeValues(new ExternalId(externalId)) + .orElseThrow(() -> new LoanOriginatorNotFoundException(externalId)); + return this.loanOriginatorMapper.toData(originator); } @Override - public Long resolveIdByExternalId(String externalId) { - throw new UnsupportedOperationException(NOT_IMPLEMENTED_MESSAGE); + public Long resolveIdByExternalId(final String externalId) { + final LoanOriginator originator = this.loanOriginatorRepository.findByExternalId(new ExternalId(externalId)) + .orElseThrow(() -> new LoanOriginatorNotFoundException(externalId)); + return originator.getId(); } } diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorWritePlatformServiceImpl.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorWritePlatformServiceImpl.java new file mode 100644 index 00000000000..04801ea596b --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorWritePlatformServiceImpl.java @@ -0,0 +1,154 @@ +/** + * 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.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.NAME_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 static org.apache.fineract.portfolio.loanorigination.api.LoanOriginatorApiConstants.STATUS_PARAM; + +import java.util.LinkedHashMap; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.apache.fineract.infrastructure.codes.domain.CodeValue; +import org.apache.fineract.infrastructure.codes.domain.CodeValueRepositoryWrapper; +import org.apache.fineract.infrastructure.core.api.JsonCommand; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResultBuilder; +import org.apache.fineract.infrastructure.core.domain.ExternalId; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginator; +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.LoanOriginatorCannotBeDeletedException; +import org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorDuplicateExternalIdException; +import org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorNotFoundException; +import org.apache.fineract.portfolio.loanorigination.serialization.LoanOriginatorDataValidator; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +@Transactional +@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", havingValue = "true") +public class LoanOriginatorWritePlatformServiceImpl implements LoanOriginatorWritePlatformService { + + private final LoanOriginatorRepository loanOriginatorRepository; + private final LoanOriginatorMappingRepository loanOriginatorMappingRepository; + private final LoanOriginatorDataValidator loanOriginatorDataValidator; + private final CodeValueRepositoryWrapper codeValueRepositoryWrapper; + + @Override + public CommandProcessingResult create(final JsonCommand command) { + this.loanOriginatorDataValidator.validateForCreate(command.json()); + + final String externalIdValue = command.stringValueOfParameterNamed(EXTERNAL_ID_PARAM); + final ExternalId externalId = new ExternalId(externalIdValue); + + if (this.loanOriginatorRepository.existsByExternalId(externalId)) { + throw new LoanOriginatorDuplicateExternalIdException(externalIdValue); + } + + final String name = command.stringValueOfParameterNamed(NAME_PARAM); + + final String statusValue = command.stringValueOfParameterNamed(STATUS_PARAM); + final LoanOriginatorStatus status = (statusValue != null && !statusValue.isEmpty()) ? LoanOriginatorStatus.fromString(statusValue) + : LoanOriginatorStatus.ACTIVE; + + final CodeValue originatorType = resolveCodeValue(command, ORIGINATOR_TYPE_ID_PARAM, ORIGINATOR_TYPE_CODE_NAME); + final CodeValue channelType = resolveCodeValue(command, CHANNEL_TYPE_ID_PARAM, CHANNEL_TYPE_CODE_NAME); + + final LoanOriginator originator = LoanOriginator.create(externalId, name, status, originatorType, channelType); + this.loanOriginatorRepository.saveAndFlush(originator); + + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(originator.getId()) + .withEntityExternalId(externalId).build(); + } + + @Override + public CommandProcessingResult update(final Long id, final JsonCommand command) { + this.loanOriginatorDataValidator.validateForUpdate(command.json()); + + final LoanOriginator originator = this.loanOriginatorRepository.findById(id) + .orElseThrow(() -> new LoanOriginatorNotFoundException(id)); + + final Map changes = new LinkedHashMap<>(); + + if (command.isChangeInStringParameterNamed(NAME_PARAM, originator.getName())) { + final String newName = command.stringValueOfParameterNamed(NAME_PARAM); + originator.setName(newName); + changes.put(NAME_PARAM, newName); + } + + if (command.isChangeInStringParameterNamed(STATUS_PARAM, originator.getStatus().getValue())) { + final String newStatusValue = command.stringValueOfParameterNamed(STATUS_PARAM); + final LoanOriginatorStatus newStatus = LoanOriginatorStatus.fromString(newStatusValue); + originator.setStatus(newStatus); + changes.put(STATUS_PARAM, newStatusValue); + } + + final Long currentOriginatorTypeId = originator.getOriginatorType() != null ? originator.getOriginatorType().getId() : null; + if (command.isChangeInLongParameterNamed(ORIGINATOR_TYPE_ID_PARAM, currentOriginatorTypeId)) { + final CodeValue newOriginatorType = resolveCodeValue(command, ORIGINATOR_TYPE_ID_PARAM, ORIGINATOR_TYPE_CODE_NAME); + originator.setOriginatorType(newOriginatorType); + changes.put(ORIGINATOR_TYPE_ID_PARAM, newOriginatorType != null ? newOriginatorType.getId() : null); + } + + final Long currentChannelTypeId = originator.getChannelType() != null ? originator.getChannelType().getId() : null; + if (command.isChangeInLongParameterNamed(CHANNEL_TYPE_ID_PARAM, currentChannelTypeId)) { + final CodeValue newChannelType = resolveCodeValue(command, CHANNEL_TYPE_ID_PARAM, CHANNEL_TYPE_CODE_NAME); + originator.setChannelType(newChannelType); + changes.put(CHANNEL_TYPE_ID_PARAM, newChannelType != null ? newChannelType.getId() : null); + } + + if (!changes.isEmpty()) { + this.loanOriginatorRepository.saveAndFlush(originator); + } + + return new CommandProcessingResultBuilder().withCommandId(command.commandId()).withEntityId(originator.getId()) + .withEntityExternalId(originator.getExternalId()).with(changes).build(); + } + + @Override + public CommandProcessingResult delete(final Long id) { + final LoanOriginator originator = this.loanOriginatorRepository.findById(id) + .orElseThrow(() -> new LoanOriginatorNotFoundException(id)); + + if (this.loanOriginatorMappingRepository.existsByOriginatorId(id)) { + throw new LoanOriginatorCannotBeDeletedException(id); + } + + final ExternalId externalId = originator.getExternalId(); + this.loanOriginatorRepository.delete(originator); + + return new CommandProcessingResultBuilder().withEntityId(id).withEntityExternalId(externalId).build(); + } + + private CodeValue resolveCodeValue(final JsonCommand command, final String paramName, final String codeName) { + final Long codeValueId = command.longValueOfParameterNamed(paramName); + if (codeValueId == null) { + return null; + } + return this.codeValueRepositoryWrapper.findOneByCodeNameAndIdWithNotFoundDetection(codeName, codeValueId); + } +} diff --git a/fineract-loan-origination/src/main/resources/db/changelog/tenant/module/loanorigination/module-changelog-master.xml b/fineract-loan-origination/src/main/resources/db/changelog/tenant/module/loanorigination/module-changelog-master.xml index 6bc107ecab9..9c8f305f093 100644 --- a/fineract-loan-origination/src/main/resources/db/changelog/tenant/module/loanorigination/module-changelog-master.xml +++ b/fineract-loan-origination/src/main/resources/db/changelog/tenant/module/loanorigination/module-changelog-master.xml @@ -23,4 +23,5 @@ xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd"> + diff --git a/fineract-loan-origination/src/main/resources/db/changelog/tenant/module/loanorigination/parts/0002_permissions.xml b/fineract-loan-origination/src/main/resources/db/changelog/tenant/module/loanorigination/parts/0002_permissions.xml new file mode 100644 index 00000000000..1d95b6c4370 --- /dev/null +++ b/fineract-loan-origination/src/main/resources/db/changelog/tenant/module/loanorigination/parts/0002_permissions.xml @@ -0,0 +1,78 @@ + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'CREATE_LOAN_ORIGINATOR' + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'READ_LOAN_ORIGINATOR' + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'UPDATE_LOAN_ORIGINATOR' + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'DELETE_LOAN_ORIGINATOR' + + + + + + + + + + + diff --git a/fineract-provider/src/main/resources/application.properties b/fineract-provider/src/main/resources/application.properties index 93130efaeb1..77c4ef0daff 100644 --- a/fineract-provider/src/main/resources/application.properties +++ b/fineract-provider/src/main/resources/application.properties @@ -212,7 +212,7 @@ fineract.sampling.resetPeriodSec=${FINERACT_SAMPLING_RESET_PERIOD_IN_SEC:60} #Modules fineract.module.self-service.enabled=${FINERACT_MODULE_SELF_SERVICE_ENABLED:false} fineract.module.investor.enabled=${FINERACT_MODULE_INVESTOR_ENABLED:true} -fineract.module.loan-origination.enabled=${FINERACT_MODULE_LOAN_ORIGINATION_ENABLED:false} +fineract.module.loan-origination.enabled=${FINERACT_MODULE_LOAN_ORIGINATION_ENABLED:true} fineract.insecure-http-client=${FINERACT_INSECURE_HTTP_CLIENT:true} fineract.client-connect-timeout=${FINERACT_CLIENT_CONNECT_TIMEOUT:30} diff --git a/fineract-provider/src/test/resources/application-test.properties b/fineract-provider/src/test/resources/application-test.properties index e5142d84680..d708c5b6103 100644 --- a/fineract-provider/src/test/resources/application-test.properties +++ b/fineract-provider/src/test/resources/application-test.properties @@ -125,8 +125,7 @@ fineract.sampling.sampledClasses= fineract.module.investor.enabled=true fineract.module.self-service.enabled=true -# TODO: activate this module when the implementation is ready -fineract.module.loan-origination.enabled=false +fineract.module.loan-origination.enabled=true # sql validation diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanOriginatorApiTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanOriginatorApiTest.java new file mode 100644 index 00000000000..576936b9ab8 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanOriginatorApiTest.java @@ -0,0 +1,280 @@ +/** + * 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; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import io.restassured.builder.RequestSpecBuilder; +import io.restassured.builder.ResponseSpecBuilder; +import io.restassured.http.ContentType; +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.util.HashMap; +import java.util.List; +import org.apache.fineract.integrationtests.common.Utils; +import org.apache.fineract.integrationtests.common.loans.LoanOriginatorHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +@Order(1) +public class LoanOriginatorApiTest { + + private RequestSpecification requestSpec; + private ResponseSpecification responseSpec; + private ResponseSpecification responseSpec400; + private ResponseSpecification responseSpec403; + private ResponseSpecification responseSpec404; + + @BeforeEach + public void setup() { + Utils.initializeRESTAssured(); + this.requestSpec = new RequestSpecBuilder().setContentType(ContentType.JSON).build(); + this.requestSpec.header("Authorization", "Basic " + Utils.loginIntoServerAndGetBase64EncodedAuthenticationKey()); + + this.responseSpec = new ResponseSpecBuilder().expectStatusCode(200).build(); + this.responseSpec400 = new ResponseSpecBuilder().expectStatusCode(400).build(); + this.responseSpec403 = new ResponseSpecBuilder().expectStatusCode(403).build(); + this.responseSpec404 = new ResponseSpecBuilder().expectStatusCode(404).build(); + } + + // ==================== CRUD LIFECYCLE TESTS ==================== + + @Test + public void testCreateOriginatorWithMinimalData() { + final String externalId = LoanOriginatorHelper.generateUniqueExternalId(); + + // Create with only externalId + final Integer originatorId = LoanOriginatorHelper.createOriginator(requestSpec, responseSpec, externalId); + + assertNotNull(originatorId, "Originator ID should not be null"); + + // Verify created data + final HashMap originator = LoanOriginatorHelper.getOriginatorById(requestSpec, responseSpec, originatorId); + + assertEquals(externalId, originator.get("externalId")); + assertEquals("ACTIVE", originator.get("status"), "Default status should be ACTIVE"); + + // Cleanup + LoanOriginatorHelper.deleteOriginator(requestSpec, responseSpec, originatorId); + } + + @Test + public void testCreateOriginatorWithAllFields() { + final String externalId = LoanOriginatorHelper.generateUniqueExternalId(); + final String name = "Test Originator"; + final String status = "PENDING"; + + final Integer originatorId = LoanOriginatorHelper.createOriginator(requestSpec, responseSpec, externalId, name, status, null, null); + + assertNotNull(originatorId); + + final HashMap originator = LoanOriginatorHelper.getOriginatorById(requestSpec, responseSpec, originatorId); + + assertEquals(externalId, originator.get("externalId")); + assertEquals(name, originator.get("name")); + assertEquals(status, originator.get("status")); + + // Cleanup + LoanOriginatorHelper.deleteOriginator(requestSpec, responseSpec, originatorId); + } + + @Test + public void testRetrieveOriginatorById() { + final String externalId = LoanOriginatorHelper.generateUniqueExternalId(); + final Integer originatorId = LoanOriginatorHelper.createOriginator(requestSpec, responseSpec, externalId); + + final HashMap originator = LoanOriginatorHelper.getOriginatorById(requestSpec, responseSpec, originatorId); + + assertNotNull(originator); + assertEquals(originatorId, originator.get("id")); + assertEquals(externalId, originator.get("externalId")); + + // Cleanup + LoanOriginatorHelper.deleteOriginator(requestSpec, responseSpec, originatorId); + } + + @Test + public void testRetrieveOriginatorByExternalId() { + final String externalId = LoanOriginatorHelper.generateUniqueExternalId(); + final Integer originatorId = LoanOriginatorHelper.createOriginator(requestSpec, responseSpec, externalId); + + final HashMap originator = LoanOriginatorHelper.getOriginatorByExternalId(requestSpec, responseSpec, externalId); + + assertNotNull(originator); + assertEquals(originatorId, originator.get("id")); + assertEquals(externalId, originator.get("externalId")); + + // Cleanup + LoanOriginatorHelper.deleteOriginator(requestSpec, responseSpec, originatorId); + } + + @Test + public void testRetrieveAllOriginators() { + final String externalId1 = LoanOriginatorHelper.generateUniqueExternalId(); + final String externalId2 = LoanOriginatorHelper.generateUniqueExternalId(); + + final Integer originatorId1 = LoanOriginatorHelper.createOriginator(requestSpec, responseSpec, externalId1); + final Integer originatorId2 = LoanOriginatorHelper.createOriginator(requestSpec, responseSpec, externalId2); + + final List> originators = LoanOriginatorHelper.getAllOriginators(requestSpec, responseSpec); + + assertNotNull(originators); + assertTrue(originators.size() >= 2, "Should have at least 2 originators"); + + // Cleanup + LoanOriginatorHelper.deleteOriginator(requestSpec, responseSpec, originatorId1); + LoanOriginatorHelper.deleteOriginator(requestSpec, responseSpec, originatorId2); + } + + @Test + public void testUpdateOriginatorPartially() { + final String externalId = LoanOriginatorHelper.generateUniqueExternalId(); + final Integer originatorId = LoanOriginatorHelper.createOriginator(requestSpec, responseSpec, externalId, "Original Name", "ACTIVE", + null, null); + + // Update only the name + final HashMap updateResult = LoanOriginatorHelper.updateOriginator(requestSpec, responseSpec, originatorId, + "Updated Name", null, null, null); + + assertNotNull(updateResult); + assertTrue(updateResult.containsKey("changes"), "Response should contain changes"); + + // Verify update + final HashMap originator = LoanOriginatorHelper.getOriginatorById(requestSpec, responseSpec, originatorId); + assertEquals("Updated Name", originator.get("name")); + assertEquals("ACTIVE", originator.get("status"), "Status should remain unchanged"); + + // Cleanup + LoanOriginatorHelper.deleteOriginator(requestSpec, responseSpec, originatorId); + } + + @Test + public void testUpdateOriginatorByExternalId() { + final String externalId = LoanOriginatorHelper.generateUniqueExternalId(); + LoanOriginatorHelper.createOriginator(requestSpec, responseSpec, externalId, "Original Name", "ACTIVE", null, null); + + // Update by external ID + final HashMap updateResult = LoanOriginatorHelper.updateOriginatorByExternalId(requestSpec, responseSpec, + externalId, "Updated via ExternalId", null, null, null); + + assertNotNull(updateResult); + + // Verify update + final HashMap originator = LoanOriginatorHelper.getOriginatorByExternalId(requestSpec, responseSpec, externalId); + assertEquals("Updated via ExternalId", originator.get("name")); + + // Cleanup + LoanOriginatorHelper.deleteOriginatorByExternalId(requestSpec, responseSpec, externalId); + } + + @Test + public void testDeleteOriginator() { + final String externalId = LoanOriginatorHelper.generateUniqueExternalId(); + final Integer originatorId = LoanOriginatorHelper.createOriginator(requestSpec, responseSpec, externalId); + + // Delete + final Integer deletedId = LoanOriginatorHelper.deleteOriginator(requestSpec, responseSpec, originatorId); + assertEquals(originatorId, deletedId); + + // Verify deleted - should return 404 + LoanOriginatorHelper.getOriginatorById(requestSpec, responseSpec404, originatorId); + } + + @Test + public void testDeleteOriginatorByExternalId() { + final String externalId = LoanOriginatorHelper.generateUniqueExternalId(); + final Integer originatorId = LoanOriginatorHelper.createOriginator(requestSpec, responseSpec, externalId); + + // Delete by external ID + final Integer deletedId = LoanOriginatorHelper.deleteOriginatorByExternalId(requestSpec, responseSpec, externalId); + assertEquals(originatorId, deletedId); + + // Verify deleted - should return 404 + LoanOriginatorHelper.getOriginatorByExternalId(requestSpec, responseSpec404, externalId); + } + + // ==================== VALIDATION ERROR TESTS ==================== + + @Test + public void testCreateOriginatorWithMissingExternalId() { + final String invalidJson = "{ \"name\": \"Test\" }"; + + LoanOriginatorHelper.createOriginatorExpectingError(requestSpec, responseSpec400, invalidJson); + } + + @Test + public void testCreateOriginatorWithDuplicateExternalId() { + final String externalId = LoanOriginatorHelper.generateUniqueExternalId(); + + // Create first originator + final Integer originatorId = LoanOriginatorHelper.createOriginator(requestSpec, responseSpec, externalId); + + // Attempt to create duplicate - should return 403 + final String duplicateJson = "{ \"externalId\": \"" + externalId + "\" }"; + LoanOriginatorHelper.createOriginatorExpectingError(requestSpec, responseSpec403, duplicateJson); + + // Cleanup + LoanOriginatorHelper.deleteOriginator(requestSpec, responseSpec, originatorId); + } + + @Test + public void testCreateOriginatorWithInvalidStatus() { + final String externalId = LoanOriginatorHelper.generateUniqueExternalId(); + final String invalidJson = "{ \"externalId\": \"" + externalId + "\", \"status\": \"INVALID\" }"; + + LoanOriginatorHelper.createOriginatorExpectingError(requestSpec, responseSpec403, invalidJson); + } + + @Test + public void testGetOriginatorByNonExistentId() { + LoanOriginatorHelper.getOriginatorById(requestSpec, responseSpec404, 999999); + } + + @Test + public void testGetOriginatorByNonExistentExternalId() { + LoanOriginatorHelper.getOriginatorByExternalId(requestSpec, responseSpec404, "NON-EXISTENT-EXTERNAL-ID"); + } + + @Test + public void testUpdateNonExistentOriginator() { + final String json = "{ \"name\": \"Updated\" }"; + Utils.performServerPut(requestSpec, responseSpec404, "/fineract-provider/api/v1/loan-originators/999999?" + Utils.TENANT_IDENTIFIER, + json, ""); + } + + @Test + public void testDeleteNonExistentOriginator() { + Utils.performServerDelete(requestSpec, responseSpec404, + "/fineract-provider/api/v1/loan-originators/999999?" + Utils.TENANT_IDENTIFIER, ""); + } + + // ==================== CODE VALUE VALIDATION TESTS ==================== + + @Test + public void testCreateOriginatorWithInvalidCodeValueId() { + final String externalId = LoanOriginatorHelper.generateUniqueExternalId(); + // Use an ID that doesn't exist + final String json = "{ \"externalId\": \"" + externalId + "\", \"originatorTypeId\": 999999 }"; + + LoanOriginatorHelper.createOriginatorExpectingError(requestSpec, responseSpec404, json); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanOriginatorHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanOriginatorHelper.java new file mode 100644 index 00000000000..716fc722580 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanOriginatorHelper.java @@ -0,0 +1,166 @@ +/** + * 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.common.loans; + +import io.restassured.specification.RequestSpecification; +import io.restassured.specification.ResponseSpecification; +import java.util.HashMap; +import java.util.List; +import java.util.UUID; +import org.apache.fineract.integrationtests.common.Utils; + +public final class LoanOriginatorHelper { + + private static final String LOAN_ORIGINATOR_URL = "/fineract-provider/api/v1/loan-originators"; + + private LoanOriginatorHelper() {} + + // ========== CREATE ========== + + public static Integer createOriginator(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final String externalId) { + return createOriginator(requestSpec, responseSpec, externalId, null, null, null, null); + } + + public static Integer createOriginator(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final String externalId, final String name, final String status, final Long originatorTypeId, final Long channelTypeId) { + final String json = buildCreateJson(externalId, name, status, originatorTypeId, channelTypeId); + return Utils.performServerPost(requestSpec, responseSpec, LOAN_ORIGINATOR_URL + "?" + Utils.TENANT_IDENTIFIER, json, "resourceId"); + } + + public static HashMap createOriginatorWithFullResponse(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final String externalId) { + final String json = buildCreateJson(externalId, null, null, null, null); + return Utils.performServerPost(requestSpec, responseSpec, LOAN_ORIGINATOR_URL + "?" + Utils.TENANT_IDENTIFIER, json, ""); + } + + public static List> createOriginatorExpectingError(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final String json) { + return Utils.performServerPost(requestSpec, responseSpec, LOAN_ORIGINATOR_URL + "?" + Utils.TENANT_IDENTIFIER, json, "errors"); + } + + // ========== READ ========== + + public static HashMap getOriginatorById(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final Integer originatorId) { + return Utils.performServerGet(requestSpec, responseSpec, LOAN_ORIGINATOR_URL + "/" + originatorId + "?" + Utils.TENANT_IDENTIFIER, + ""); + } + + public static HashMap getOriginatorByExternalId(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final String externalId) { + return Utils.performServerGet(requestSpec, responseSpec, + LOAN_ORIGINATOR_URL + "/external-id/" + externalId + "?" + Utils.TENANT_IDENTIFIER, ""); + } + + public static List> getAllOriginators(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec) { + return Utils.performServerGet(requestSpec, responseSpec, LOAN_ORIGINATOR_URL + "?" + Utils.TENANT_IDENTIFIER, ""); + } + + // ========== UPDATE ========== + + public static HashMap updateOriginator(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final Integer originatorId, final String name, final String status, final Long originatorTypeId, final Long channelTypeId) { + final String json = buildUpdateJson(name, status, originatorTypeId, channelTypeId); + return Utils.performServerPut(requestSpec, responseSpec, LOAN_ORIGINATOR_URL + "/" + originatorId + "?" + Utils.TENANT_IDENTIFIER, + json, ""); + } + + public static HashMap updateOriginatorByExternalId(final RequestSpecification requestSpec, + final ResponseSpecification responseSpec, final String externalId, final String name, final String status, + final Long originatorTypeId, final Long channelTypeId) { + final String json = buildUpdateJson(name, status, originatorTypeId, channelTypeId); + return Utils.performServerPut(requestSpec, responseSpec, + LOAN_ORIGINATOR_URL + "/external-id/" + externalId + "?" + Utils.TENANT_IDENTIFIER, json, ""); + } + + // ========== DELETE ========== + + public static Integer deleteOriginator(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final Integer originatorId) { + return Utils.performServerDelete(requestSpec, responseSpec, + LOAN_ORIGINATOR_URL + "/" + originatorId + "?" + Utils.TENANT_IDENTIFIER, "resourceId"); + } + + public static Integer deleteOriginatorByExternalId(final RequestSpecification requestSpec, final ResponseSpecification responseSpec, + final String externalId) { + return Utils.performServerDelete(requestSpec, responseSpec, + LOAN_ORIGINATOR_URL + "/external-id/" + externalId + "?" + Utils.TENANT_IDENTIFIER, "resourceId"); + } + + // ========== JSON BUILDERS ========== + + private static String buildCreateJson(final String externalId, final String name, final String status, final Long originatorTypeId, + final Long channelTypeId) { + final StringBuilder json = new StringBuilder("{"); + json.append("\"externalId\": \"").append(externalId).append("\""); + if (name != null) { + json.append(", \"name\": \"").append(name).append("\""); + } + if (status != null) { + json.append(", \"status\": \"").append(status).append("\""); + } + if (originatorTypeId != null) { + json.append(", \"originatorTypeId\": ").append(originatorTypeId); + } + if (channelTypeId != null) { + json.append(", \"channelTypeId\": ").append(channelTypeId); + } + json.append("}"); + return json.toString(); + } + + private static String buildUpdateJson(final String name, final String status, final Long originatorTypeId, final Long channelTypeId) { + final StringBuilder json = new StringBuilder("{"); + boolean first = true; + if (name != null) { + json.append("\"name\": \"").append(name).append("\""); + first = false; + } + if (status != null) { + if (!first) { + json.append(", "); + } + json.append("\"status\": \"").append(status).append("\""); + first = false; + } + if (originatorTypeId != null) { + if (!first) { + json.append(", "); + } + json.append("\"originatorTypeId\": ").append(originatorTypeId); + first = false; + } + if (channelTypeId != null) { + if (!first) { + json.append(", "); + } + json.append("\"channelTypeId\": ").append(channelTypeId); + } + json.append("}"); + return json.toString(); + } + + // ========== UTILITIES ========== + + public static String generateUniqueExternalId() { + return "EXT-" + UUID.randomUUID().toString().substring(0, 8); + } +}