FINERACT-2418: add originatior loan mapping

This commit is contained in:
Attila Budai
2026-02-03 14:58:28 +01:00
committed by Adam Saghy
parent 3dbada34f7
commit 3b5ea96f5e
18 changed files with 749 additions and 447 deletions
@@ -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;
}
}
@@ -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);
}
}
@@ -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();
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -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());
}
}
@@ -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());
}
}
@@ -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);
}
@@ -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) {
@@ -24,4 +24,5 @@
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<include relativeToChangelogFile="true" file="parts/0001_initial_schema.xml"/>
<include relativeToChangelogFile="true" file="parts/0002_permissions.xml"/>
<include relativeToChangelogFile="true" file="parts/0003_mapping_permissions.xml"/>
</databaseChangeLog>
@@ -0,0 +1,50 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Licensed to the Apache Software Foundation (ASF) under one
or more contributor license agreements. See the NOTICE file
distributed with this work for additional information
regarding copyright ownership. The ASF licenses this file
to you under the Apache License, Version 2.0 (the
"License"); you may not use this file except in compliance
with the License. You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing,
software distributed under the License is distributed on an
"AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
KIND, either express or implied. See the License for the
specific language governing permissions and limitations
under the License.
-->
<databaseChangeLog xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.1.xsd">
<changeSet id="loan-origination-mapping-perm-001" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(1) FROM m_permission WHERE code = 'ATTACH_LOAN_ORIGINATOR'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="ATTACH_LOAN_ORIGINATOR"/>
<column name="entity_name" value="LOAN_ORIGINATOR"/>
<column name="action_name" value="ATTACH"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
<changeSet id="loan-origination-mapping-perm-002" author="fineract">
<preConditions onFail="MARK_RAN">
<sqlCheck expectedResult="0">SELECT COUNT(1) FROM m_permission WHERE code = 'DETACH_LOAN_ORIGINATOR'</sqlCheck>
</preConditions>
<insert tableName="m_permission">
<column name="grouping" value="portfolio"/>
<column name="code" value="DETACH_LOAN_ORIGINATOR"/>
<column name="entity_name" value="LOAN_ORIGINATOR"/>
<column name="action_name" value="DETACH"/>
<column name="can_maker_checker" valueBoolean="false"/>
</insert>
</changeSet>
</databaseChangeLog>
@@ -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<String, Object> 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<String, Object> 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<String, Object> 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<String, Object> 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<HashMap<String, Object>> 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<String, Object> 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<String, Object> 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<String, Object> updateResult = LoanOriginatorHelper.updateOriginatorByExternalId(requestSpec, responseSpec,
externalId, "Updated via ExternalId", null, null, null);
assertNotNull(updateResult);
// Verify update
final HashMap<String, Object> 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);
}
}
@@ -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);
}
}
@@ -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;
});
}
}
@@ -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);
}
}
@@ -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<String, Object> 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<HashMap<String, Object>> 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<String, Object> 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<String, Object> 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<HashMap<String, Object>> getAllOriginators(final RequestSpecification requestSpec,
final ResponseSpecification responseSpec) {
return Utils.performServerGet(requestSpec, responseSpec, LOAN_ORIGINATOR_URL + "?" + Utils.TENANT_IDENTIFIER, "");
}
// ========== UPDATE ==========
public static HashMap<String, Object> 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<String, Object> 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);
}
}