diff --git a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java index 8c049039f..61a3108f5 100644 --- a/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java +++ b/fineract-client-feign/src/main/java/org/apache/fineract/client/feign/FineractFeignClient.java @@ -96,6 +96,7 @@ import org.apache.fineract.client.feign.services.LoanCollateralApi; import org.apache.fineract.client.feign.services.LoanCollateralManagementApi; import org.apache.fineract.client.feign.services.LoanDisbursementDetailsApi; import org.apache.fineract.client.feign.services.LoanInterestPauseApi; +import org.apache.fineract.client.feign.services.LoanOriginatorsApi; import org.apache.fineract.client.feign.services.LoanProductsApi; import org.apache.fineract.client.feign.services.LoanReschedulingApi; import org.apache.fineract.client.feign.services.LoanTransactionsApi; @@ -532,6 +533,10 @@ public final class FineractFeignClient { return create(LoanInterestPauseApi.class); } + public LoanOriginatorsApi loanOriginators() { + return create(LoanOriginatorsApi.class); + } + public LoanProductsApi loanProducts() { return create(LoanProductsApi.class); } diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/api/LoanOriginatorApiResource.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/api/LoanOriginatorApiResource.java index 9053c4a1f..7a437755d 100644 --- a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/api/LoanOriginatorApiResource.java +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/api/LoanOriginatorApiResource.java @@ -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 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( diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/api/LoanOriginatorApiResourceSwagger.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/api/LoanOriginatorApiResourceSwagger.java new file mode 100644 index 000000000..c80b0d281 --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/api/LoanOriginatorApiResourceSwagger.java @@ -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; + } +} 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 09c296924..99bf4a5c1 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 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, 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 000000000..a5c6fe265 --- /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 000000000..33612c182 --- /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 000000000..3b8630dbf --- /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 000000000..eec740b20 --- /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 000000000..cd8fe7196 --- /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 000000000..383a18286 --- /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 000000000..712e6f674 --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/UpdateLoanOriginatorCommandHandler.java @@ -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); + } +} 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 000000000..c891c6e0c --- /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 000000000..dba7eeb67 --- /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 dd9f84c3c..991385876 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 000000000..04801ea59 --- /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 6bc107eca..9c8f305f0 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 000000000..1d95b6c43 --- /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 93130efae..77c4ef0da 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 e5142d846..d708c5b61 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 000000000..576936b9a --- /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/client/feign/helpers/FeignLoanOriginatorHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignLoanOriginatorHelper.java new file mode 100644 index 000000000..020f3e7a4 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignLoanOriginatorHelper.java @@ -0,0 +1,108 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests.client.feign.helpers; + +import static org.apache.fineract.client.feign.util.FeignCalls.fail; +import static org.apache.fineract.client.feign.util.FeignCalls.ok; + +import java.util.List; +import java.util.UUID; +import org.apache.fineract.client.feign.FineractFeignClient; +import org.apache.fineract.client.feign.util.CallFailedRuntimeException; +import org.apache.fineract.client.models.GetLoanOriginatorsResponse; +import org.apache.fineract.client.models.PostLoanOriginatorsRequest; +import org.apache.fineract.client.models.PostLoanOriginatorsResponse; +import org.apache.fineract.client.models.PutLoanOriginatorsRequest; +import org.apache.fineract.client.models.PutLoanOriginatorsResponse; + +public class FeignLoanOriginatorHelper { + + private final FineractFeignClient fineractClient; + + public FeignLoanOriginatorHelper(FineractFeignClient fineractClient) { + this.fineractClient = fineractClient; + } + + public Long createOriginator(String externalId) { + return createOriginator(new PostLoanOriginatorsRequest().externalId(externalId).name(externalId)); + } + + public Long createOriginator(String externalId, String name, String status) { + return createOriginator(new PostLoanOriginatorsRequest().externalId(externalId).name(name).status(status)); + } + + public Long createOriginator(PostLoanOriginatorsRequest request) { + PostLoanOriginatorsResponse response = ok(() -> fineractClient.loanOriginators().create11(request)); + return response.getResourceId(); + } + + public CallFailedRuntimeException createOriginatorExpectingError(PostLoanOriginatorsRequest request) { + return fail(() -> fineractClient.loanOriginators().create11(request)); + } + + public List getAllOriginators() { + return ok(() -> fineractClient.loanOriginators().retrieveAll28()); + } + + public GetLoanOriginatorsResponse getOriginatorById(Long originatorId) { + return ok(() -> fineractClient.loanOriginators().retrieveOne18(originatorId)); + } + + public CallFailedRuntimeException getOriginatorByIdExpectingError(Long originatorId) { + return fail(() -> fineractClient.loanOriginators().retrieveOne18(originatorId)); + } + + public GetLoanOriginatorsResponse getOriginatorByExternalId(String externalId) { + return ok(() -> fineractClient.loanOriginators().retrieveByExternalId(externalId)); + } + + public CallFailedRuntimeException getOriginatorByExternalIdExpectingError(String externalId) { + return fail(() -> fineractClient.loanOriginators().retrieveByExternalId(externalId)); + } + + public PutLoanOriginatorsResponse updateOriginator(Long originatorId, PutLoanOriginatorsRequest request) { + return ok(() -> fineractClient.loanOriginators().update16(originatorId, request)); + } + + public PutLoanOriginatorsResponse updateOriginatorByExternalId(String externalId, PutLoanOriginatorsRequest request) { + return ok(() -> fineractClient.loanOriginators().updateByExternalId(externalId, request)); + } + + public CallFailedRuntimeException updateOriginatorExpectingError(Long originatorId, PutLoanOriginatorsRequest request) { + return fail(() -> fineractClient.loanOriginators().update16(originatorId, request)); + } + + public Long deleteOriginator(Long originatorId) { + var response = ok(() -> fineractClient.loanOriginators().delete14(originatorId)); + return response.getResourceId(); + } + + public Long deleteOriginatorByExternalId(String externalId) { + var response = ok(() -> fineractClient.loanOriginators().deleteByExternalId(externalId)); + return response.getResourceId(); + } + + public CallFailedRuntimeException deleteOriginatorExpectingError(Long originatorId) { + return fail(() -> fineractClient.loanOriginators().delete14(originatorId)); + } + + public static String generateUniqueExternalId() { + return "EXT-" + UUID.randomUUID().toString().substring(0, 8); + } +} diff --git a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanOriginatorApiTest.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanOriginatorApiTest.java new file mode 100644 index 000000000..e15013727 --- /dev/null +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/tests/FeignLoanOriginatorApiTest.java @@ -0,0 +1,248 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.integrationtests.client.feign.tests; + +import java.util.List; +import org.apache.fineract.client.feign.util.CallFailedRuntimeException; +import org.apache.fineract.client.models.GetLoanOriginatorsResponse; +import org.apache.fineract.client.models.PostLoanOriginatorsRequest; +import org.apache.fineract.client.models.PutLoanOriginatorsRequest; +import org.apache.fineract.client.models.PutLoanOriginatorsResponse; +import org.apache.fineract.integrationtests.client.FeignIntegrationTest; +import org.apache.fineract.integrationtests.client.feign.helpers.FeignLoanOriginatorHelper; +import org.apache.fineract.integrationtests.common.FineractFeignClientHelper; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Order; +import org.junit.jupiter.api.Test; + +@Order(1) +public class FeignLoanOriginatorApiTest extends FeignIntegrationTest { + + private static FeignLoanOriginatorHelper originatorHelper; + + @BeforeAll + public static void setup() { + originatorHelper = new FeignLoanOriginatorHelper(FineractFeignClientHelper.getFineractFeignClient()); + } + + @Test + public void testCreateOriginatorWithMinimalData() { + final String externalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + + final Long originatorId = originatorHelper.createOriginator(externalId); + + assertThat(originatorId).isNotNull(); + + final GetLoanOriginatorsResponse originator = originatorHelper.getOriginatorById(originatorId); + + assertThat(originator.getExternalId()).isEqualTo(externalId); + assertThat(originator.getStatus()).isEqualTo("ACTIVE"); + + originatorHelper.deleteOriginator(originatorId); + } + + @Test + public void testCreateOriginatorWithAllFields() { + final String externalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final String name = "Test Originator"; + final String status = "PENDING"; + + final Long originatorId = originatorHelper.createOriginator(externalId, name, status); + + assertThat(originatorId).isNotNull(); + + final GetLoanOriginatorsResponse originator = originatorHelper.getOriginatorById(originatorId); + + assertThat(originator.getExternalId()).isEqualTo(externalId); + assertThat(originator.getName()).isEqualTo(name); + assertThat(originator.getStatus()).isEqualTo(status); + + originatorHelper.deleteOriginator(originatorId); + } + + @Test + public void testRetrieveOriginatorById() { + final String externalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final Long originatorId = originatorHelper.createOriginator(externalId); + + final GetLoanOriginatorsResponse originator = originatorHelper.getOriginatorById(originatorId); + + assertThat(originator).isNotNull(); + assertThat(originator.getId()).isEqualTo(originatorId); + assertThat(originator.getExternalId()).isEqualTo(externalId); + + originatorHelper.deleteOriginator(originatorId); + } + + @Test + public void testRetrieveOriginatorByExternalId() { + final String externalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final Long originatorId = originatorHelper.createOriginator(externalId); + + final GetLoanOriginatorsResponse originator = originatorHelper.getOriginatorByExternalId(externalId); + + assertThat(originator).isNotNull(); + assertThat(originator.getId()).isEqualTo(originatorId); + assertThat(originator.getExternalId()).isEqualTo(externalId); + + originatorHelper.deleteOriginator(originatorId); + } + + @Test + public void testRetrieveAllOriginators() { + final String externalId1 = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final String externalId2 = FeignLoanOriginatorHelper.generateUniqueExternalId(); + + final Long originatorId1 = originatorHelper.createOriginator(externalId1); + final Long originatorId2 = originatorHelper.createOriginator(externalId2); + + final List originators = originatorHelper.getAllOriginators(); + + assertThat(originators).isNotNull(); + assertThat(originators).hasSizeGreaterThanOrEqualTo(2); + + originatorHelper.deleteOriginator(originatorId1); + originatorHelper.deleteOriginator(originatorId2); + } + + @Test + public void testUpdateOriginatorPartially() { + final String externalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final Long originatorId = originatorHelper.createOriginator(externalId, "Original Name", "ACTIVE"); + + final PutLoanOriginatorsResponse updateResult = originatorHelper.updateOriginator(originatorId, + new PutLoanOriginatorsRequest().name("Updated Name")); + + assertThat(updateResult).isNotNull(); + + final GetLoanOriginatorsResponse originator = originatorHelper.getOriginatorById(originatorId); + assertThat(originator.getName()).isEqualTo("Updated Name"); + assertThat(originator.getStatus()).isEqualTo("ACTIVE"); + + originatorHelper.deleteOriginator(originatorId); + } + + @Test + public void testUpdateOriginatorByExternalId() { + final String externalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + originatorHelper.createOriginator(externalId, "Original Name", "ACTIVE"); + + final PutLoanOriginatorsResponse updateResult = originatorHelper.updateOriginatorByExternalId(externalId, + new PutLoanOriginatorsRequest().name("Updated via ExternalId")); + + assertThat(updateResult).isNotNull(); + + final GetLoanOriginatorsResponse originator = originatorHelper.getOriginatorByExternalId(externalId); + assertThat(originator.getName()).isEqualTo("Updated via ExternalId"); + + originatorHelper.deleteOriginatorByExternalId(externalId); + } + + @Test + public void testDeleteOriginator() { + final String externalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final Long originatorId = originatorHelper.createOriginator(externalId); + + final Long deletedId = originatorHelper.deleteOriginator(originatorId); + assertThat(deletedId).isEqualTo(originatorId); + + final CallFailedRuntimeException exception = originatorHelper.getOriginatorByIdExpectingError(originatorId); + assertThat(exception.getStatus()).isEqualTo(404); + } + + @Test + public void testDeleteOriginatorByExternalId() { + final String externalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final Long originatorId = originatorHelper.createOriginator(externalId); + + final Long deletedId = originatorHelper.deleteOriginatorByExternalId(externalId); + assertThat(deletedId).isEqualTo(originatorId); + + final CallFailedRuntimeException exception = originatorHelper.getOriginatorByExternalIdExpectingError(externalId); + assertThat(exception.getStatus()).isEqualTo(404); + } + + @Test + public void testCreateOriginatorWithOptionalName() { + final String externalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final PostLoanOriginatorsRequest request = new PostLoanOriginatorsRequest().externalId(externalId); + + final Long originatorId = originatorHelper.createOriginator(request); + assertThat(originatorId).isNotNull(); + + originatorHelper.deleteOriginator(originatorId); + } + + @Test + public void testCreateOriginatorWithDuplicateExternalId() { + final String externalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + + final Long originatorId = originatorHelper.createOriginator(externalId); + + final CallFailedRuntimeException exception = originatorHelper + .createOriginatorExpectingError(new PostLoanOriginatorsRequest().externalId(externalId).name("Duplicate")); + assertThat(exception.getStatus()).isEqualTo(403); + + originatorHelper.deleteOriginator(originatorId); + } + + @Test + public void testCreateOriginatorWithInvalidStatus() { + final String externalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final PostLoanOriginatorsRequest request = new PostLoanOriginatorsRequest().externalId(externalId).name("Test").status("INVALID"); + + final CallFailedRuntimeException exception = originatorHelper.createOriginatorExpectingError(request); + assertThat(exception.getStatus()).isEqualTo(403); + } + + @Test + public void testGetOriginatorByNonExistentId() { + final CallFailedRuntimeException exception = originatorHelper.getOriginatorByIdExpectingError(999999L); + assertThat(exception.getStatus()).isEqualTo(404); + } + + @Test + public void testGetOriginatorByNonExistentExternalId() { + final CallFailedRuntimeException exception = originatorHelper.getOriginatorByExternalIdExpectingError("NON-EXISTENT-EXTERNAL-ID"); + assertThat(exception.getStatus()).isEqualTo(404); + } + + @Test + public void testUpdateNonExistentOriginator() { + final CallFailedRuntimeException exception = originatorHelper.updateOriginatorExpectingError(999999L, + new PutLoanOriginatorsRequest().name("Updated")); + assertThat(exception.getStatus()).isEqualTo(404); + } + + @Test + public void testDeleteNonExistentOriginator() { + final CallFailedRuntimeException exception = originatorHelper.deleteOriginatorExpectingError(999999L); + assertThat(exception.getStatus()).isEqualTo(404); + } + + @Test + public void testCreateOriginatorWithInvalidCodeValueId() { + final String externalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final PostLoanOriginatorsRequest request = new PostLoanOriginatorsRequest().externalId(externalId).name("Test") + .originatorTypeId(999999L); + + final CallFailedRuntimeException exception = originatorHelper.createOriginatorExpectingError(request); + assertThat(exception.getStatus()).isEqualTo(404); + } +} 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 000000000..716fc7225 --- /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); + } +}