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);
- }
-}