FINERACT-2418: add origination api implementation

This commit is contained in:
Attila Budai
2026-01-28 17:01:57 +01:00
committed by Arnold Gálovics
parent ba978c591b
commit 82e6a237a6
23 changed files with 1612 additions and 19 deletions
@@ -20,6 +20,10 @@ package org.apache.fineract.portfolio.loanorigination.api;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.media.ArraySchema;
import io.swagger.v3.oas.annotations.media.Content;
import io.swagger.v3.oas.annotations.media.Schema;
import io.swagger.v3.oas.annotations.parameters.RequestBody;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.ws.rs.Consumes;
@@ -58,7 +62,8 @@ public class LoanOriginatorApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create a new loan originator", description = "Creates a new loan originator record. Requires CREATE_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanOriginatorApiResourceSwagger.PostLoanOriginatorsRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanOriginatorApiResourceSwagger.PostLoanOriginatorsResponse.class)))
@ApiResponse(responseCode = "400", description = "Required parameter is missing or incorrect format")
@ApiResponse(responseCode = "403", description = "Duplicate external ID or insufficient permissions")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
@@ -70,7 +75,7 @@ public class LoanOriginatorApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List all loan originators", description = "Retrieves all loan originator records. Requires READ_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(array = @ArraySchema(schema = @Schema(implementation = LoanOriginatorApiResourceSwagger.GetLoanOriginatorsResponse.class))))
@ApiResponse(responseCode = "403", description = "Insufficient permissions")
public List<LoanOriginatorData> retrieveAll() {
this.context.authenticatedUser().validateHasReadPermission(LoanOriginatorApiConstants.RESOURCE_NAME);
@@ -83,7 +88,7 @@ public class LoanOriginatorApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a loan originator by ID", description = "Retrieves a loan originator by its internal ID. Requires READ_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanOriginatorApiResourceSwagger.GetLoanOriginatorsResponse.class)))
@ApiResponse(responseCode = "403", description = "Insufficient permissions")
@ApiResponse(responseCode = "404", description = "Originator not found")
public LoanOriginatorData retrieveOne(@PathParam("originatorId") @Parameter(description = "originatorId") final Long originatorId) {
@@ -97,7 +102,7 @@ public class LoanOriginatorApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a loan originator by external ID", description = "Retrieves a loan originator by its external ID. Requires READ_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanOriginatorApiResourceSwagger.GetLoanOriginatorsResponse.class)))
@ApiResponse(responseCode = "403", description = "Insufficient permissions")
@ApiResponse(responseCode = "404", description = "Originator not found")
public LoanOriginatorData retrieveByExternalId(
@@ -112,7 +117,8 @@ public class LoanOriginatorApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update a loan originator by ID", description = "Updates a loan originator by its internal ID. Requires UPDATE_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanOriginatorApiResourceSwagger.PutLoanOriginatorsRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanOriginatorApiResourceSwagger.PutLoanOriginatorsResponse.class)))
@ApiResponse(responseCode = "400", description = "Incorrect format")
@ApiResponse(responseCode = "403", description = "Insufficient permissions")
@ApiResponse(responseCode = "404", description = "Originator not found")
@@ -128,7 +134,8 @@ public class LoanOriginatorApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update a loan originator by external ID", description = "Updates a loan originator by its external ID. Requires UPDATE_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@RequestBody(required = true, content = @Content(schema = @Schema(implementation = LoanOriginatorApiResourceSwagger.PutLoanOriginatorsRequest.class)))
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanOriginatorApiResourceSwagger.PutLoanOriginatorsResponse.class)))
@ApiResponse(responseCode = "400", description = "Incorrect format")
@ApiResponse(responseCode = "403", description = "Insufficient permissions")
@ApiResponse(responseCode = "404", description = "Originator not found")
@@ -147,7 +154,7 @@ public class LoanOriginatorApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a loan originator by ID", description = "Deletes a loan originator by its internal ID. Requires DELETE_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanOriginatorApiResourceSwagger.DeleteLoanOriginatorsResponse.class)))
@ApiResponse(responseCode = "403", description = "Originator is mapped to loans and cannot be deleted, or insufficient permissions")
@ApiResponse(responseCode = "404", description = "Originator not found")
public CommandProcessingResult delete(@PathParam("originatorId") @Parameter(description = "originatorId") final Long originatorId) {
@@ -160,7 +167,7 @@ public class LoanOriginatorApiResource {
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a loan originator by external ID", description = "Deletes a loan originator by its external ID. Requires DELETE_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "200", description = "OK", content = @Content(schema = @Schema(implementation = LoanOriginatorApiResourceSwagger.DeleteLoanOriginatorsResponse.class)))
@ApiResponse(responseCode = "403", description = "Originator is mapped to loans and cannot be deleted, or insufficient permissions")
@ApiResponse(responseCode = "404", description = "Originator not found")
public CommandProcessingResult deleteByExternalId(
@@ -0,0 +1,104 @@
/**
* 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.api;
import io.swagger.v3.oas.annotations.media.Schema;
final class LoanOriginatorApiResourceSwagger {
private LoanOriginatorApiResourceSwagger() {}
@Schema(description = "GetLoanOriginatorsResponse")
public static final class GetLoanOriginatorsResponse {
private GetLoanOriginatorsResponse() {}
@Schema(example = "1")
public Long id;
@Schema(example = "EXT-001")
public String externalId;
@Schema(example = "Main Branch Originator")
public String name;
@Schema(example = "ACTIVE")
public String status;
@Schema(example = "1")
public Long originatorTypeId;
@Schema(example = "1")
public Long channelTypeId;
}
@Schema(description = "PostLoanOriginatorsRequest")
public static final class PostLoanOriginatorsRequest {
private PostLoanOriginatorsRequest() {}
@Schema(example = "EXT-001")
public String externalId;
@Schema(example = "Main Branch Originator", requiredMode = Schema.RequiredMode.REQUIRED)
public String name;
@Schema(example = "ACTIVE")
public String status;
@Schema(example = "1")
public Long originatorTypeId;
@Schema(example = "1")
public Long channelTypeId;
}
@Schema(description = "PostLoanOriginatorsResponse")
public static final class PostLoanOriginatorsResponse {
private PostLoanOriginatorsResponse() {}
@Schema(example = "1")
public Long resourceId;
}
@Schema(description = "PutLoanOriginatorsRequest")
public static final class PutLoanOriginatorsRequest {
private PutLoanOriginatorsRequest() {}
@Schema(example = "Updated Name")
public String name;
@Schema(example = "INACTIVE")
public String status;
@Schema(example = "2")
public Long originatorTypeId;
@Schema(example = "2")
public Long channelTypeId;
}
@Schema(description = "PutLoanOriginatorsResponse")
public static final class PutLoanOriginatorsResponse {
private PutLoanOriginatorsResponse() {}
@Schema(example = "1")
public Long resourceId;
}
@Schema(description = "DeleteLoanOriginatorsResponse")
public static final class DeleteLoanOriginatorsResponse {
private DeleteLoanOriginatorsResponse() {}
@Schema(example = "1")
public Long resourceId;
}
}
@@ -23,6 +23,8 @@ import java.util.Optional;
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<LoanOriginator, Long>, JpaSpecificationExecutor<LoanOriginator> {
@@ -31,4 +33,13 @@ public interface LoanOriginatorRepository extends JpaRepository<LoanOriginator,
boolean existsByExternalId(ExternalId externalId);
List<LoanOriginator> findByStatus(LoanOriginatorStatus status);
@Query("SELECT lo FROM LoanOriginator lo LEFT JOIN FETCH lo.originatorType LEFT JOIN FETCH lo.channelType")
List<LoanOriginator> findAllWithCodeValues();
@Query("SELECT lo FROM LoanOriginator lo LEFT JOIN FETCH lo.originatorType LEFT JOIN FETCH lo.channelType WHERE lo.id = :id")
Optional<LoanOriginator> 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<LoanOriginator> findByExternalIdWithCodeValues(@Param("externalId") ExternalId externalId);
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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());
}
}
@@ -0,0 +1,45 @@
/**
* 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;
import org.springframework.transaction.annotation.Transactional;
@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;
@Transactional
@Override
public CommandProcessingResult processCommand(final JsonCommand command) {
return this.writePlatformService.update(command.entityId(), command);
}
}
@@ -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<LoanOriginatorData> toDataList(List<LoanOriginator> entities);
}
@@ -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<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, CREATE_REQUEST_PARAMS);
final List<ApiParameterError> 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<Map<String, Object>>() {}.getType();
this.fromApiJsonHelper.checkForUnsupportedParameters(typeOfMap, json, UPDATE_REQUEST_PARAMS);
final List<ApiParameterError> 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<ApiParameterError> dataValidationErrors) {
if (!dataValidationErrors.isEmpty()) {
throw new PlatformApiDataValidationException(dataValidationErrors);
}
}
}
@@ -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<LoanOriginatorData> retrieveAll() {
throw new UnsupportedOperationException(NOT_IMPLEMENTED_MESSAGE);
final List<LoanOriginator> 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();
}
}
@@ -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<String, Object> 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);
}
}
@@ -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">
<include relativeToChangelogFile="true" file="parts/0001_initial_schema.xml"/>
<include relativeToChangelogFile="true" file="parts/0002_permissions.xml"/>
</databaseChangeLog>
@@ -0,0 +1,78 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
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.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
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">
<changeSet id="loan-origination-perm-001" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(1) FROM m_permission WHERE code = 'CREATE_LOAN_ORIGINATOR'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="CREATE_LOAN_ORIGINATOR"/>
<column name="entity_name" value="LOAN_ORIGINATOR"/>
<column name="action_name" value="CREATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet id="loan-origination-perm-002" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(1) FROM m_permission WHERE code = 'READ_LOAN_ORIGINATOR'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="READ_LOAN_ORIGINATOR"/>
<column name="entity_name" value="LOAN_ORIGINATOR"/>
<column name="action_name" value="READ"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet id="loan-origination-perm-003" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(1) FROM m_permission WHERE code = 'UPDATE_LOAN_ORIGINATOR'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="UPDATE_LOAN_ORIGINATOR"/>
<column name="entity_name" value="LOAN_ORIGINATOR"/>
<column name="action_name" value="UPDATE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet id="loan-origination-perm-004" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(1) FROM m_permission WHERE code = 'DELETE_LOAN_ORIGINATOR'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="DELETE_LOAN_ORIGINATOR"/>
<column name="entity_name" value="LOAN_ORIGINATOR"/>
<column name="action_name" value="DELETE"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>