From 3b5ea96f5ee01c5659e199e68850fd19d0838ccc Mon Sep 17 00:00:00 2001 From: Attila Budai Date: Wed, 28 Jan 2026 19:59:32 +0100 Subject: [PATCH] FINERACT-2418: add originatior loan mapping --- .../service/CommandWrapperBuilder.java | 20 ++ .../api/LoanOriginatorsApiResource.java | 188 ++++++++++++ .../data/LoanOriginatorMappingResponse.java | 46 +++ .../LoanNotInSubmittedStatusException.java | 31 ++ ...iginatorMappingAlreadyExistsException.java | 29 ++ ...oanOriginatorMappingNotFoundException.java | 29 ++ .../LoanOriginatorNotActiveException.java | 30 ++ .../AttachLoanOriginatorCommandHandler.java | 42 +++ .../DetachLoanOriginatorCommandHandler.java | 42 +++ .../LoanOriginatorWritePlatformService.java | 4 + ...oanOriginatorWritePlatformServiceImpl.java | 54 ++++ .../module-changelog-master.xml | 1 + .../parts/0003_mapping_permissions.xml | 50 ++++ .../LoanOriginatorApiTest.java | 280 ------------------ .../client/feign/helpers/FeignLoanHelper.java | 56 ++++ .../helpers/FeignLoanOriginatorHelper.java | 28 ++ .../tests/FeignLoanOriginatorApiTest.java | 100 ++++++- .../common/loans/LoanOriginatorHelper.java | 166 ----------- 18 files changed, 749 insertions(+), 447 deletions(-) create mode 100644 fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/data/LoanOriginatorMappingResponse.java create mode 100644 fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanNotInSubmittedStatusException.java create mode 100644 fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorMappingAlreadyExistsException.java create mode 100644 fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorMappingNotFoundException.java create mode 100644 fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorNotActiveException.java create mode 100644 fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/AttachLoanOriginatorCommandHandler.java create mode 100644 fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/DetachLoanOriginatorCommandHandler.java create mode 100644 fineract-loan-origination/src/main/resources/db/changelog/tenant/module/loanorigination/parts/0003_mapping_permissions.xml delete mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanOriginatorApiTest.java delete mode 100644 integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanOriginatorHelper.java diff --git a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java index ff96583fb..135f93747 100644 --- a/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java +++ b/fineract-core/src/main/java/org/apache/fineract/commands/service/CommandWrapperBuilder.java @@ -3952,4 +3952,24 @@ public class CommandWrapperBuilder { this.href = "/loan-originators/" + originatorId; return this; } + + public CommandWrapperBuilder attachLoanOriginator(final Long loanId, final Long originatorId) { + this.actionName = "ATTACH"; + this.entityName = "LOAN_ORIGINATOR"; + this.entityId = loanId; + this.loanId = loanId; + this.subentityId = originatorId; + this.href = "/loans/" + loanId + "/originators/" + originatorId; + return this; + } + + public CommandWrapperBuilder detachLoanOriginator(final Long loanId, final Long originatorId) { + this.actionName = "DETACH"; + this.entityName = "LOAN_ORIGINATOR"; + this.entityId = loanId; + this.loanId = loanId; + this.subentityId = originatorId; + this.href = "/loans/" + loanId + "/originators/" + originatorId; + return this; + } } diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/api/LoanOriginatorsApiResource.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/api/LoanOriginatorsApiResource.java index 4169e60d0..586a0cc65 100644 --- a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/api/LoanOriginatorsApiResource.java +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/api/LoanOriginatorsApiResource.java @@ -23,17 +23,24 @@ import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.DELETE; import jakarta.ws.rs.GET; +import jakarta.ws.rs.POST; import jakarta.ws.rs.Path; import jakarta.ws.rs.PathParam; import jakarta.ws.rs.Produces; import jakarta.ws.rs.core.MediaType; import lombok.RequiredArgsConstructor; +import org.apache.fineract.commands.domain.CommandWrapper; +import org.apache.fineract.commands.service.CommandWrapperBuilder; +import org.apache.fineract.commands.service.PortfolioCommandSourceWritePlatformService; +import org.apache.fineract.infrastructure.core.data.CommandProcessingResult; import org.apache.fineract.infrastructure.core.domain.ExternalId; import org.apache.fineract.infrastructure.core.service.ExternalIdFactory; import org.apache.fineract.infrastructure.security.service.PlatformSecurityContext; import org.apache.fineract.portfolio.loanaccount.exception.LoanNotFoundException; import org.apache.fineract.portfolio.loanaccount.service.LoanReadPlatformService; +import org.apache.fineract.portfolio.loanorigination.data.LoanOriginatorMappingResponse; import org.apache.fineract.portfolio.loanorigination.data.LoanOriginatorsResponse; import org.apache.fineract.portfolio.loanorigination.service.LoanOriginatorReadPlatformService; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -51,6 +58,7 @@ public class LoanOriginatorsApiResource { private final PlatformSecurityContext context; private final LoanReadPlatformService loanReadPlatformService; private final LoanOriginatorReadPlatformService loanOriginatorReadPlatformService; + private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService; @GET @Path("{loanId}/originators") @@ -90,4 +98,184 @@ public class LoanOriginatorsApiResource { return LoanOriginatorsResponse.of(this.loanOriginatorReadPlatformService.retrieveByLoanId(loanId)); } + + @POST + @Path("{loanId}/originators/{originatorId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Attach originator to loan by IDs", description = "Attaches an originator to a loan. Loan must be in 'Submitted and Pending Approval' status. Requires ATTACH_LOAN_ORIGINATOR permission.") + @ApiResponse(responseCode = "200", description = "OK - Originator attached") + @ApiResponse(responseCode = "403", description = "Loan not in correct status, originator not ACTIVE, duplicate mapping, or insufficient permissions") + @ApiResponse(responseCode = "404", description = "Loan or originator not found") + public LoanOriginatorMappingResponse attachOriginatorToLoan(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + @PathParam("originatorId") @Parameter(description = "originatorId") final Long originatorId) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().attachLoanOriginator(loanId, originatorId).build(); + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return buildMappingResponse(result); + } + + @POST + @Path("{loanId}/originators/external-id/{originatorExternalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Attach originator to loan by loan ID and originator external ID") + @ApiResponse(responseCode = "200", description = "OK") + @ApiResponse(responseCode = "403", description = "Loan not in correct status, originator not ACTIVE, duplicate mapping") + @ApiResponse(responseCode = "404", description = "Loan or originator not found") + public LoanOriginatorMappingResponse attachOriginatorToLoanByOriginatorExternalId( + @PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + @PathParam("originatorExternalId") @Parameter(description = "originatorExternalId") final String originatorExternalId) { + + final Long originatorId = this.loanOriginatorReadPlatformService.resolveIdByExternalId(originatorExternalId); + + final CommandWrapper commandRequest = new CommandWrapperBuilder().attachLoanOriginator(loanId, originatorId).build(); + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return buildMappingResponse(result); + } + + @POST + @Path("external-id/{loanExternalId}/originators/{originatorId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Attach originator to loan by loan external ID and originator ID") + @ApiResponse(responseCode = "200", description = "OK") + @ApiResponse(responseCode = "403", description = "Loan not in correct status, originator not ACTIVE, duplicate mapping") + @ApiResponse(responseCode = "404", description = "Loan or originator not found") + public LoanOriginatorMappingResponse attachOriginatorToLoanByLoanExternalId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId, + @PathParam("originatorId") @Parameter(description = "originatorId") final Long originatorId) { + + final ExternalId externalId = ExternalIdFactory.produce(loanExternalId); + final Long loanId = this.loanReadPlatformService.retrieveLoanIdByExternalId(externalId); + if (loanId == null) { + throw new LoanNotFoundException(externalId); + } + + final CommandWrapper commandRequest = new CommandWrapperBuilder().attachLoanOriginator(loanId, originatorId).build(); + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return buildMappingResponse(result); + } + + @POST + @Path("external-id/{loanExternalId}/originators/external-id/{originatorExternalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Attach originator to loan by external IDs") + @ApiResponse(responseCode = "200", description = "OK") + @ApiResponse(responseCode = "403", description = "Loan not in correct status, originator not ACTIVE, duplicate mapping") + @ApiResponse(responseCode = "404", description = "Loan or originator not found") + public LoanOriginatorMappingResponse attachOriginatorToLoanByExternalIds( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId, + @PathParam("originatorExternalId") @Parameter(description = "originatorExternalId") final String originatorExternalId) { + + final ExternalId loanExtId = ExternalIdFactory.produce(loanExternalId); + final Long loanId = this.loanReadPlatformService.retrieveLoanIdByExternalId(loanExtId); + if (loanId == null) { + throw new LoanNotFoundException(loanExtId); + } + + final Long originatorId = this.loanOriginatorReadPlatformService.resolveIdByExternalId(originatorExternalId); + + final CommandWrapper commandRequest = new CommandWrapperBuilder().attachLoanOriginator(loanId, originatorId).build(); + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return buildMappingResponse(result); + } + + @DELETE + @Path("{loanId}/originators/{originatorId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Detach originator from loan by IDs", description = "Detaches an originator from a loan. Loan must be in 'Submitted and Pending Approval' status. Requires DETACH_LOAN_ORIGINATOR permission.") + @ApiResponse(responseCode = "200", description = "OK - Originator detached") + @ApiResponse(responseCode = "403", description = "Loan not in correct status or insufficient permissions") + @ApiResponse(responseCode = "404", description = "Loan, originator, or mapping not found") + public LoanOriginatorMappingResponse detachOriginatorFromLoan(@PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + @PathParam("originatorId") @Parameter(description = "originatorId") final Long originatorId) { + + final CommandWrapper commandRequest = new CommandWrapperBuilder().detachLoanOriginator(loanId, originatorId).build(); + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return buildMappingResponse(result); + } + + @DELETE + @Path("{loanId}/originators/external-id/{originatorExternalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Detach originator from loan by loan ID and originator external ID") + @ApiResponse(responseCode = "200", description = "OK") + @ApiResponse(responseCode = "403", description = "Loan not in correct status") + @ApiResponse(responseCode = "404", description = "Loan, originator, or mapping not found") + public LoanOriginatorMappingResponse detachOriginatorFromLoanByOriginatorExternalId( + @PathParam("loanId") @Parameter(description = "loanId") final Long loanId, + @PathParam("originatorExternalId") @Parameter(description = "originatorExternalId") final String originatorExternalId) { + + final Long originatorId = this.loanOriginatorReadPlatformService.resolveIdByExternalId(originatorExternalId); + + final CommandWrapper commandRequest = new CommandWrapperBuilder().detachLoanOriginator(loanId, originatorId).build(); + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return buildMappingResponse(result); + } + + @DELETE + @Path("external-id/{loanExternalId}/originators/{originatorId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Detach originator from loan by loan external ID and originator ID") + @ApiResponse(responseCode = "200", description = "OK") + @ApiResponse(responseCode = "403", description = "Loan not in correct status") + @ApiResponse(responseCode = "404", description = "Loan, originator, or mapping not found") + public LoanOriginatorMappingResponse detachOriginatorFromLoanByLoanExternalId( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId, + @PathParam("originatorId") @Parameter(description = "originatorId") final Long originatorId) { + + final ExternalId externalId = ExternalIdFactory.produce(loanExternalId); + final Long loanId = this.loanReadPlatformService.retrieveLoanIdByExternalId(externalId); + if (loanId == null) { + throw new LoanNotFoundException(externalId); + } + + final CommandWrapper commandRequest = new CommandWrapperBuilder().detachLoanOriginator(loanId, originatorId).build(); + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return buildMappingResponse(result); + } + + @DELETE + @Path("external-id/{loanExternalId}/originators/external-id/{originatorExternalId}") + @Consumes({ MediaType.APPLICATION_JSON }) + @Produces({ MediaType.APPLICATION_JSON }) + @Operation(summary = "Detach originator from loan by external IDs") + @ApiResponse(responseCode = "200", description = "OK") + @ApiResponse(responseCode = "403", description = "Loan not in correct status") + @ApiResponse(responseCode = "404", description = "Loan, originator, or mapping not found") + public LoanOriginatorMappingResponse detachOriginatorFromLoanByExternalIds( + @PathParam("loanExternalId") @Parameter(description = "loanExternalId") final String loanExternalId, + @PathParam("originatorExternalId") @Parameter(description = "originatorExternalId") final String originatorExternalId) { + + final ExternalId loanExtId = ExternalIdFactory.produce(loanExternalId); + final Long loanId = this.loanReadPlatformService.retrieveLoanIdByExternalId(loanExtId); + if (loanId == null) { + throw new LoanNotFoundException(loanExtId); + } + + final Long originatorId = this.loanOriginatorReadPlatformService.resolveIdByExternalId(originatorExternalId); + + final CommandWrapper commandRequest = new CommandWrapperBuilder().detachLoanOriginator(loanId, originatorId).build(); + final CommandProcessingResult result = this.commandsSourceWritePlatformService.logCommandSource(commandRequest); + + return buildMappingResponse(result); + } + + private LoanOriginatorMappingResponse buildMappingResponse(final CommandProcessingResult result) { + return LoanOriginatorMappingResponse.of(result.getResourceId(), + result.getResourceExternalId() != null ? result.getResourceExternalId().getValue() : null, result.getSubResourceId(), + result.getSubResourceExternalId() != null ? result.getSubResourceExternalId().getValue() : null); + } } diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/data/LoanOriginatorMappingResponse.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/data/LoanOriginatorMappingResponse.java new file mode 100644 index 000000000..db5d2ffab --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/data/LoanOriginatorMappingResponse.java @@ -0,0 +1,46 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanorigination.data; + +import java.io.Serial; +import java.io.Serializable; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class LoanOriginatorMappingResponse implements Serializable { + + @Serial + private static final long serialVersionUID = 1L; + + private Long loanId; + private String loanExternalId; + private Long originatorId; + private String originatorExternalId; + + public static LoanOriginatorMappingResponse of(Long loanId, String loanExternalId, Long originatorId, String originatorExternalId) { + return LoanOriginatorMappingResponse.builder().loanId(loanId).loanExternalId(loanExternalId).originatorId(originatorId) + .originatorExternalId(originatorExternalId).build(); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanNotInSubmittedStatusException.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanNotInSubmittedStatusException.java new file mode 100644 index 000000000..bf7501b22 --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanNotInSubmittedStatusException.java @@ -0,0 +1,31 @@ +/** + * 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 LoanNotInSubmittedStatusException extends AbstractPlatformDomainRuleException { + + public LoanNotInSubmittedStatusException(Long loanId, String currentStatus) { + super("error.msg.loan.not.in.submitted.status", + "Loan with id " + loanId + " has status " + currentStatus + + ". Originator can only be attached/detached while loan is in 'Submitted and Pending Approval' status.", + loanId, currentStatus); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorMappingAlreadyExistsException.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorMappingAlreadyExistsException.java new file mode 100644 index 000000000..6195b34f8 --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorMappingAlreadyExistsException.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 LoanOriginatorMappingAlreadyExistsException extends AbstractPlatformDomainRuleException { + + public LoanOriginatorMappingAlreadyExistsException(Long loanId, Long originatorId) { + super("error.msg.loan.originator.mapping.already.exists", + "Originator with id " + originatorId + " is already attached to loan with id " + loanId, loanId, originatorId); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorMappingNotFoundException.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorMappingNotFoundException.java new file mode 100644 index 000000000..a6c930461 --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorMappingNotFoundException.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.AbstractPlatformResourceNotFoundException; + +public class LoanOriginatorMappingNotFoundException extends AbstractPlatformResourceNotFoundException { + + public LoanOriginatorMappingNotFoundException(Long loanId, Long originatorId) { + super("error.msg.loan.originator.mapping.not.found", + "Originator with id " + originatorId + " is not attached to loan with id " + loanId, loanId, originatorId); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorNotActiveException.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorNotActiveException.java new file mode 100644 index 000000000..261ec8049 --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/exception/LoanOriginatorNotActiveException.java @@ -0,0 +1,30 @@ +/** + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you under the Apache License, Version 2.0 (the + * "License"); you may not use this file except in compliance + * with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, + * software distributed under the License is distributed on an + * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY + * KIND, either express or implied. See the License for the + * specific language governing permissions and limitations + * under the License. + */ +package org.apache.fineract.portfolio.loanorigination.exception; + +import org.apache.fineract.infrastructure.core.exception.AbstractPlatformDomainRuleException; + +public class LoanOriginatorNotActiveException extends AbstractPlatformDomainRuleException { + + public LoanOriginatorNotActiveException(Long originatorId, String status) { + super("error.msg.loan.originator.not.active", + "Originator with id " + originatorId + " has status " + status + ". Only ACTIVE originators can be attached to loans.", + originatorId, status); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/AttachLoanOriginatorCommandHandler.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/AttachLoanOriginatorCommandHandler.java new file mode 100644 index 000000000..f21a2290f --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/AttachLoanOriginatorCommandHandler.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 = "ATTACH") +@RequiredArgsConstructor +@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", havingValue = "true") +public class AttachLoanOriginatorCommandHandler implements NewCommandSourceHandler { + + private final LoanOriginatorWritePlatformService writePlatformService; + + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.attachOriginatorToLoan(command.getLoanId(), command.subentityId()); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/DetachLoanOriginatorCommandHandler.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/DetachLoanOriginatorCommandHandler.java new file mode 100644 index 000000000..2ec7157d6 --- /dev/null +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/handler/DetachLoanOriginatorCommandHandler.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 = "DETACH") +@RequiredArgsConstructor +@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", havingValue = "true") +public class DetachLoanOriginatorCommandHandler implements NewCommandSourceHandler { + + private final LoanOriginatorWritePlatformService writePlatformService; + + @Override + public CommandProcessingResult processCommand(final JsonCommand command) { + return this.writePlatformService.detachOriginatorFromLoan(command.getLoanId(), command.subentityId()); + } +} diff --git a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorWritePlatformService.java b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorWritePlatformService.java index 7bd3f182f..4b4171647 100644 --- a/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorWritePlatformService.java +++ b/fineract-loan-origination/src/main/java/org/apache/fineract/portfolio/loanorigination/service/LoanOriginatorWritePlatformService.java @@ -28,4 +28,8 @@ public interface LoanOriginatorWritePlatformService { CommandProcessingResult update(Long id, JsonCommand command); CommandProcessingResult delete(Long id); + + CommandProcessingResult attachOriginatorToLoan(Long loanId, Long originatorId); + + CommandProcessingResult detachOriginatorFromLoan(Long loanId, Long originatorId); } 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 index 04801ea59..de8faebe9 100644 --- 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 @@ -35,12 +35,19 @@ 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.loanaccount.domain.Loan; +import org.apache.fineract.portfolio.loanaccount.domain.LoanRepositoryWrapper; import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginator; +import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMapping; import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorMappingRepository; import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorRepository; import org.apache.fineract.portfolio.loanorigination.domain.LoanOriginatorStatus; +import org.apache.fineract.portfolio.loanorigination.exception.LoanNotInSubmittedStatusException; import org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorCannotBeDeletedException; import org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorDuplicateExternalIdException; +import org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorMappingAlreadyExistsException; +import org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorMappingNotFoundException; +import org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorNotActiveException; import org.apache.fineract.portfolio.loanorigination.exception.LoanOriginatorNotFoundException; import org.apache.fineract.portfolio.loanorigination.serialization.LoanOriginatorDataValidator; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; @@ -57,6 +64,7 @@ public class LoanOriginatorWritePlatformServiceImpl implements LoanOriginatorWri private final LoanOriginatorMappingRepository loanOriginatorMappingRepository; private final LoanOriginatorDataValidator loanOriginatorDataValidator; private final CodeValueRepositoryWrapper codeValueRepositoryWrapper; + private final LoanRepositoryWrapper loanRepositoryWrapper; @Override public CommandProcessingResult create(final JsonCommand command) { @@ -144,6 +152,52 @@ public class LoanOriginatorWritePlatformServiceImpl implements LoanOriginatorWri return new CommandProcessingResultBuilder().withEntityId(id).withEntityExternalId(externalId).build(); } + @Override + public CommandProcessingResult attachOriginatorToLoan(final Long loanId, final Long originatorId) { + final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); + + if (!loan.isSubmittedAndPendingApproval()) { + throw new LoanNotInSubmittedStatusException(loanId, loan.getStatus().getCode()); + } + + final LoanOriginator originator = this.loanOriginatorRepository.findById(originatorId) + .orElseThrow(() -> new LoanOriginatorNotFoundException(originatorId)); + + if (originator.getStatus() != LoanOriginatorStatus.ACTIVE) { + throw new LoanOriginatorNotActiveException(originatorId, originator.getStatus().getValue()); + } + + if (this.loanOriginatorMappingRepository.existsByLoanIdAndOriginatorId(loanId, originatorId)) { + throw new LoanOriginatorMappingAlreadyExistsException(loanId, originatorId); + } + + final LoanOriginatorMapping mapping = LoanOriginatorMapping.create(loanId, originator); + this.loanOriginatorMappingRepository.saveAndFlush(mapping); + + return new CommandProcessingResultBuilder().withEntityId(loanId).withEntityExternalId(loan.getExternalId()) + .withSubEntityId(originatorId).withSubEntityExternalId(originator.getExternalId()).build(); + } + + @Override + public CommandProcessingResult detachOriginatorFromLoan(final Long loanId, final Long originatorId) { + final Loan loan = this.loanRepositoryWrapper.findOneWithNotFoundDetection(loanId); + + if (!loan.isSubmittedAndPendingApproval()) { + throw new LoanNotInSubmittedStatusException(loanId, loan.getStatus().getCode()); + } + + final LoanOriginator originator = this.loanOriginatorRepository.findById(originatorId) + .orElseThrow(() -> new LoanOriginatorNotFoundException(originatorId)); + + final LoanOriginatorMapping mapping = this.loanOriginatorMappingRepository.findByLoanIdAndOriginatorId(loanId, originatorId) + .orElseThrow(() -> new LoanOriginatorMappingNotFoundException(loanId, originatorId)); + + this.loanOriginatorMappingRepository.delete(mapping); + + return new CommandProcessingResultBuilder().withEntityId(loanId).withEntityExternalId(loan.getExternalId()) + .withSubEntityId(originatorId).withSubEntityExternalId(originator.getExternalId()).build(); + } + private CodeValue resolveCodeValue(final JsonCommand command, final String paramName, final String codeName) { final Long codeValueId = command.longValueOfParameterNamed(paramName); if (codeValueId == null) { 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 9c8f305f0..0aa875a3f 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 @@ -24,4 +24,5 @@ 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/0003_mapping_permissions.xml b/fineract-loan-origination/src/main/resources/db/changelog/tenant/module/loanorigination/parts/0003_mapping_permissions.xml new file mode 100644 index 000000000..3b750c200 --- /dev/null +++ b/fineract-loan-origination/src/main/resources/db/changelog/tenant/module/loanorigination/parts/0003_mapping_permissions.xml @@ -0,0 +1,50 @@ + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'ATTACH_LOAN_ORIGINATOR' + + + + + + + + + + + + + SELECT COUNT(1) FROM m_permission WHERE code = 'DETACH_LOAN_ORIGINATOR' + + + + + + + + + + + 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 deleted file mode 100644 index 576936b9a..000000000 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/LoanOriginatorApiTest.java +++ /dev/null @@ -1,280 +0,0 @@ -/** - * 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/FeignLoanHelper.java b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignLoanHelper.java index 01ad0ecc7..80a147e13 100644 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignLoanHelper.java +++ b/integration-tests/src/test/java/org/apache/fineract/integrationtests/client/feign/helpers/FeignLoanHelper.java @@ -39,6 +39,32 @@ public class FeignLoanHelper { this.fineractClient = fineractClient; } + public Long createSimpleLoanProduct() { + PostLoanProductsRequest request = new PostLoanProductsRequest()// + .name("Simple Loan Product " + System.currentTimeMillis())// + .shortName(java.util.UUID.randomUUID().toString().substring(0, 4).toUpperCase())// + .currencyCode("USD")// + .digitsAfterDecimal(2)// + .inMultiplesOf(1)// + .principal(10000.0)// + .numberOfRepayments(12)// + .repaymentEvery(1)// + .repaymentFrequencyType(2L)// + .interestRatePerPeriod(1.0)// + .interestRateFrequencyType(2)// + .amortizationType(1)// + .interestType(0)// + .interestCalculationPeriodType(1)// + .transactionProcessingStrategyCode("mifos-standard-strategy")// + .daysInYearType(365)// + .daysInMonthType(30)// + .isInterestRecalculationEnabled(false)// + .accountingRule(1)// + .locale("en")// + .dateFormat("dd MMMM yyyy"); + return createLoanProduct(request); + } + public Long createLoanProduct(PostLoanProductsRequest request) { PostLoanProductsResponse response = ok(() -> fineractClient.loanProducts().createLoanProduct(request)); return response.getResourceId(); @@ -106,4 +132,34 @@ public class FeignLoanHelper { approveLoan(loanId, approveRequest); return loanId; } + + public Long createSubmittedLoan(Long clientId, Long productId, String submittedOnDate, Double principal, Integer numberOfRepayments) { + PostLoansRequest applyRequest = new PostLoansRequest()// + .clientId(clientId)// + .productId(productId)// + .loanType("individual")// + .submittedOnDate(submittedOnDate)// + .expectedDisbursementDate(submittedOnDate)// + .principal(BigDecimal.valueOf(principal))// + .loanTermFrequency(numberOfRepayments)// + .loanTermFrequencyType(2)// + .numberOfRepayments(numberOfRepayments)// + .repaymentEvery(1)// + .repaymentFrequencyType(2)// + .interestRatePerPeriod(BigDecimal.ZERO)// + .amortizationType(1)// + .interestType(0)// + .interestCalculationPeriodType(1)// + .transactionProcessingStrategyCode("mifos-standard-strategy")// + .locale("en")// + .dateFormat("dd MMMM yyyy"); + return applyForLoan(applyRequest); + } + + public Long createSubmittedLoan(Long clientId) { + Long productId = createSimpleLoanProduct(); + String todayDate = org.apache.fineract.integrationtests.common.Utils.dateFormatter + .format(org.apache.fineract.integrationtests.common.Utils.getLocalDateOfTenant()); + return createSubmittedLoan(clientId, productId, todayDate, 10000.0, 12); + } } 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 index 020f3e7a4..6283e2f66 100644 --- 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 @@ -105,4 +105,32 @@ public class FeignLoanOriginatorHelper { public static String generateUniqueExternalId() { return "EXT-" + UUID.randomUUID().toString().substring(0, 8); } + + public void attachOriginatorToLoan(Long loanId, Long originatorId) { + ok(() -> { + fineractClient.loanOriginators().attachOriginatorToLoan(loanId, originatorId); + return null; + }); + } + + public CallFailedRuntimeException attachOriginatorToLoanExpectingError(Long loanId, Long originatorId) { + return fail(() -> { + fineractClient.loanOriginators().attachOriginatorToLoan(loanId, originatorId); + return null; + }); + } + + public void detachOriginatorFromLoan(Long loanId, Long originatorId) { + ok(() -> { + fineractClient.loanOriginators().detachOriginatorFromLoan(loanId, originatorId); + return null; + }); + } + + public CallFailedRuntimeException detachOriginatorFromLoanExpectingError(Long loanId, Long originatorId) { + return fail(() -> { + fineractClient.loanOriginators().detachOriginatorFromLoan(loanId, originatorId); + return null; + }); + } } 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 index e15013727..dcba22dcb 100644 --- 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 @@ -19,12 +19,15 @@ package org.apache.fineract.integrationtests.client.feign.tests; import java.util.List; +import org.apache.fineract.client.feign.FineractFeignClient; import org.apache.fineract.client.feign.util.CallFailedRuntimeException; import org.apache.fineract.client.models.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.FeignClientHelper; +import org.apache.fineract.integrationtests.client.feign.helpers.FeignLoanHelper; import org.apache.fineract.integrationtests.client.feign.helpers.FeignLoanOriginatorHelper; import org.apache.fineract.integrationtests.common.FineractFeignClientHelper; import org.junit.jupiter.api.BeforeAll; @@ -35,10 +38,15 @@ import org.junit.jupiter.api.Test; public class FeignLoanOriginatorApiTest extends FeignIntegrationTest { private static FeignLoanOriginatorHelper originatorHelper; + private static FeignClientHelper clientHelper; + private static FeignLoanHelper loanHelper; @BeforeAll public static void setup() { - originatorHelper = new FeignLoanOriginatorHelper(FineractFeignClientHelper.getFineractFeignClient()); + FineractFeignClient fineractClient = FineractFeignClientHelper.getFineractFeignClient(); + originatorHelper = new FeignLoanOriginatorHelper(fineractClient); + clientHelper = new FeignClientHelper(fineractClient); + loanHelper = new FeignLoanHelper(fineractClient); } @Test @@ -245,4 +253,94 @@ public class FeignLoanOriginatorApiTest extends FeignIntegrationTest { final CallFailedRuntimeException exception = originatorHelper.createOriginatorExpectingError(request); assertThat(exception.getStatus()).isEqualTo(404); } + + @Test + public void testAttachOriginatorToSubmittedLoan() { + final String originatorExternalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final Long originatorId = originatorHelper.createOriginator(originatorExternalId); + + final Long clientId = clientHelper.createClient("Test", "Client"); + final Long loanId = loanHelper.createSubmittedLoan(clientId); + + originatorHelper.attachOriginatorToLoan(loanId, originatorId); + + originatorHelper.detachOriginatorFromLoan(loanId, originatorId); + originatorHelper.deleteOriginator(originatorId); + } + + @Test + public void testDetachOriginatorFromLoan() { + final String originatorExternalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final Long originatorId = originatorHelper.createOriginator(originatorExternalId); + + final Long clientId = clientHelper.createClient("Test", "Client"); + final Long loanId = loanHelper.createSubmittedLoan(clientId); + + originatorHelper.attachOriginatorToLoan(loanId, originatorId); + originatorHelper.detachOriginatorFromLoan(loanId, originatorId); + + originatorHelper.deleteOriginator(originatorId); + } + + @Test + public void testAttachInactiveOriginatorReturns403() { + final String originatorExternalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final Long originatorId = originatorHelper.createOriginator(originatorExternalId, "Inactive Test", "INACTIVE"); + + final Long clientId = clientHelper.createClient("Test", "Client"); + final Long loanId = loanHelper.createSubmittedLoan(clientId); + + final CallFailedRuntimeException exception = originatorHelper.attachOriginatorToLoanExpectingError(loanId, originatorId); + assertThat(exception.getStatus()).isEqualTo(403); + + originatorHelper.deleteOriginator(originatorId); + } + + @Test + public void testAttachSameOriginatorTwiceReturns403() { + final String originatorExternalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final Long originatorId = originatorHelper.createOriginator(originatorExternalId); + + final Long clientId = clientHelper.createClient("Test", "Client"); + final Long loanId = loanHelper.createSubmittedLoan(clientId); + + originatorHelper.attachOriginatorToLoan(loanId, originatorId); + + final CallFailedRuntimeException exception = originatorHelper.attachOriginatorToLoanExpectingError(loanId, originatorId); + assertThat(exception.getStatus()).isEqualTo(403); + + originatorHelper.detachOriginatorFromLoan(loanId, originatorId); + originatorHelper.deleteOriginator(originatorId); + } + + @Test + public void testDetachNonAttachedOriginatorReturns404() { + final String originatorExternalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final Long originatorId = originatorHelper.createOriginator(originatorExternalId); + + final Long clientId = clientHelper.createClient("Test", "Client"); + final Long loanId = loanHelper.createSubmittedLoan(clientId); + + final CallFailedRuntimeException exception = originatorHelper.detachOriginatorFromLoanExpectingError(loanId, originatorId); + assertThat(exception.getStatus()).isEqualTo(404); + + originatorHelper.deleteOriginator(originatorId); + } + + @Test + public void testAttachOriginatorToApprovedLoanReturns403() { + final String originatorExternalId = FeignLoanOriginatorHelper.generateUniqueExternalId(); + final Long originatorId = originatorHelper.createOriginator(originatorExternalId); + + final Long clientId = clientHelper.createClient("Test", "Client"); + final Long loanProductId = loanHelper.createSimpleLoanProduct(); + final String todayDate = org.apache.fineract.integrationtests.common.Utils.dateFormatter + .format(org.apache.fineract.integrationtests.common.Utils.getLocalDateOfTenant()); + final Long loanId = loanHelper.applyAndApproveLoan(clientId, loanProductId, todayDate, 10000.0, 12); + + final CallFailedRuntimeException exception = originatorHelper.attachOriginatorToLoanExpectingError(loanId, originatorId); + assertThat(exception.getStatus()).isEqualTo(403); + + originatorHelper.deleteOriginator(originatorId); + } } 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 deleted file mode 100644 index 716fc7225..000000000 --- a/integration-tests/src/test/java/org/apache/fineract/integrationtests/common/loans/LoanOriginatorHelper.java +++ /dev/null @@ -1,166 +0,0 @@ -/** - * 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); - } -}