Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions fineract-avro-schemas/src/main/avro/loan/v1/LoanAccountDataV1.avsc
Original file line number Diff line number Diff line change
Expand Up @@ -842,6 +842,17 @@
"type": "map"
}
]
},
{
"default": null,
"name": "originators",
"type": [
"null",
{
"type": "array",
"items": "org.apache.fineract.avro.loan.v1.OriginatorDetailsV1"
}
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,18 @@
"null",
"org.apache.fineract.avro.generic.v1.CodeValueDataV1"
]
},
{
"default": null,
"name": "originators",
"doc": "List of originators attached to the parent loan for revenue sharing",
"type": [
"null",
{
"type": "array",
"items": "org.apache.fineract.avro.loan.v1.OriginatorDetailsV1"
}
]
}
]
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"name": "OriginatorDetailsV1",
"namespace": "org.apache.fineract.avro.loan.v1",
"doc": "Loan originator details for revenue sharing and reporting",
"type": "record",
"fields": [
{
"default": null,
"name": "id",
"doc": "Internal originator ID",
"type": [
"null",
"long"
]
},
{
"default": null,
"name": "externalId",
"doc": "Unique external identifier (Revenue Share ID)",
"type": [
"null",
"string"
]
},
{
"default": null,
"name": "name",
"doc": "Originator name",
"type": [
"null",
"string"
]
},
{
"default": null,
"name": "status",
"doc": "Originator status: ACTIVE, PENDING, or INACTIVE",
"type": [
"null",
"string"
]
},
{
"default": null,
"name": "originatorType",
"doc": "Code value for originator type (MERCHANT, BROKER, AFFILIATE, PLATFORM)",
"type": [
"null",
"org.apache.fineract.avro.generic.v1.CodeValueDataV1"
]
},
{
"default": null,
"name": "channelType",
"doc": "Code value for channel type (ONLINE, IN_STORE, API, AGGREGATOR)",
"type": [
"null",
"org.apache.fineract.avro.generic.v1.CodeValueDataV1"
]
}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -3929,4 +3929,27 @@ public CommandWrapperBuilder updateLoanAvailableDisbursementAmount(final Long lo
this.href = "/loans/" + loanId;
return this;
}

public CommandWrapperBuilder createLoanOriginator() {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maintaining constants of CREATE, UPDATE, DELETE, LOAN_ORIGINATOR and /loan-originators/ may be more maintainable

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Based on the current pattern of the file, I am not sure we should change it.

this.actionName = "CREATE";
this.entityName = "LOAN_ORIGINATOR";
this.href = "/loan-originators";
return this;
}

public CommandWrapperBuilder updateLoanOriginator(final Long originatorId) {
this.actionName = "UPDATE";
this.entityName = "LOAN_ORIGINATOR";
this.entityId = originatorId;
this.href = "/loan-originators/" + originatorId;
return this;
}

public CommandWrapperBuilder deleteLoanOriginator(final Long originatorId) {
this.actionName = "DELETE";
this.entityName = "LOAN_ORIGINATOR";
this.entityId = originatorId;
this.href = "/loan-originators/" + originatorId;
return this;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -552,7 +552,7 @@ public void runBatchApiCreateAndApproveLoanRescheduleWithGivenUserLockedByCobErr
// Create new user which cannot bypass loan COB execution
PostUsersResponse createUserResponse = testContext().get(TestContextKey.CREATED_SIMPLE_USER_RESPONSE);
Long createdUserId = createUserResponse.getResourceId();
GetUsersUserIdResponse user = fineractFeignClient.users().retrieveOne31(createdUserId);
GetUsersUserIdResponse user = fineractFeignClient.users().retrieveOne32(createdUserId);
String authorizationString = user.getUsername() + ":" + PWD_USER_WITH_ROLE;
Base64 base64 = new Base64();
headerMap.put("Authorization",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ public void createUserWithUsernameAndRoles(String username, String roleName, Lis
.repeatPassword(PWD_USER_WITH_ROLE) //
.roles(List.of(roleId));

PostUsersResponse createUserResponse = ok(() -> fineractClient.users().create15(postUsersRequest));
PostUsersResponse createUserResponse = ok(() -> fineractClient.users().create16(postUsersRequest));
testContext().set(TestContextKey.CREATED_SIMPLE_USER_RESPONSE, createUserResponse);
testContext().set(TestContextKey.CREATED_SIMPLE_USER_USERNAME, generatedUsername);
testContext().set(TestContextKey.CREATED_SIMPLE_USER_PASSWORD, PWD_USER_WITH_ROLE);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ public void makeRepaymentWithGivenUser(String repaymentType, String transactionD

PostUsersResponse createUserResponse = testContext().get(TestContextKey.CREATED_SIMPLE_USER_RESPONSE);
Long createdUserId = createUserResponse.getResourceId();
GetUsersUserIdResponse user = ok(() -> fineractClient.users().retrieveOne31(createdUserId));
GetUsersUserIdResponse user = ok(() -> fineractClient.users().retrieveOne32(createdUserId));

String apiBaseUrl = apiProperties.getBaseUrl() + "/fineract-provider/api/";
FineractFeignClient userClient = FineractFeignClient.builder().baseUrl(apiBaseUrl)
Expand Down Expand Up @@ -200,7 +200,7 @@ public void makeRepaymentWithGivenUserByExternalId(String repaymentType, String

PostUsersResponse createUserResponse = testContext().get(TestContextKey.CREATED_SIMPLE_USER_RESPONSE);
Long createdUserId = createUserResponse.getResourceId();
GetUsersUserIdResponse user = ok(() -> fineractClient.users().retrieveOne31(createdUserId));
GetUsersUserIdResponse user = ok(() -> fineractClient.users().retrieveOne32(createdUserId));

String apiBaseUrl = apiProperties.getBaseUrl() + "/fineract-provider/api/";
FineractFeignClient userClient = FineractFeignClient.builder().baseUrl(apiBaseUrl)
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanorigination.api;

import java.util.Set;

public final class LoanOriginatorApiConstants {

private LoanOriginatorApiConstants() {}

public static final String RESOURCE_NAME = "LOAN_ORIGINATOR";
public static final String RESOURCE_PATH = "/loan-originators";

public static final String ACTION_CREATE = "CREATE";
public static final String ACTION_UPDATE = "UPDATE";
public static final String ACTION_DELETE = "DELETE";

public static final String ORIGINATOR_TYPE_CODE_NAME = "LoanOriginatorType";
public static final String CHANNEL_TYPE_CODE_NAME = "LoanOriginationChannelType";

public static final String EXTERNAL_ID_PARAM = "externalId";
public static final String NAME_PARAM = "name";
public static final String STATUS_PARAM = "status";
public static final String ORIGINATOR_TYPE_ID_PARAM = "originatorTypeId";
public static final String CHANNEL_TYPE_ID_PARAM = "channelTypeId";

public static final Set<String> CREATE_REQUEST_PARAMS = Set.of(EXTERNAL_ID_PARAM, NAME_PARAM, STATUS_PARAM, ORIGINATOR_TYPE_ID_PARAM,
CHANNEL_TYPE_ID_PARAM);

public static final Set<String> UPDATE_REQUEST_PARAMS = Set.of(NAME_PARAM, STATUS_PARAM, ORIGINATOR_TYPE_ID_PARAM,
CHANNEL_TYPE_ID_PARAM);

public static final Set<String> RESPONSE_PARAMS = Set.of("id", EXTERNAL_ID_PARAM, NAME_PARAM, STATUS_PARAM, ORIGINATOR_TYPE_ID_PARAM,
CHANNEL_TYPE_ID_PARAM);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
/**
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing,
* software distributed under the License is distributed on an
* "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
* KIND, either express or implied. See the License for the
* specific language governing permissions and limitations
* under the License.
*/
package org.apache.fineract.portfolio.loanorigination.api;

import io.swagger.v3.oas.annotations.Operation;
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.PUT;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.PathParam;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.core.MediaType;
import java.util.List;
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.security.service.PlatformSecurityContext;
import org.apache.fineract.portfolio.loanorigination.data.LoanOriginatorData;
import org.apache.fineract.portfolio.loanorigination.service.LoanOriginatorReadPlatformService;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.stereotype.Component;

@Path("/v1/loan-originators")
@Component
@ConditionalOnProperty(value = "fineract.module.loan-origination.enabled", havingValue = "true")
@Tag(name = "Loan Originators", description = "Manage loan originator details for revenue sharing and reporting")
@RequiredArgsConstructor
public class LoanOriginatorApiResource {

private final PlatformSecurityContext context;
private final LoanOriginatorReadPlatformService loanOriginatorReadPlatformService;
private final PortfolioCommandSourceWritePlatformService commandsSourceWritePlatformService;

@POST
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Create a new loan originator", description = "Creates a new loan originator record. Requires CREATE_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "400", description = "Required parameter is missing or incorrect format")
@ApiResponse(responseCode = "403", description = "Duplicate external ID or insufficient permissions")
public CommandProcessingResult create(@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().createLoanOriginator().withJson(apiRequestBodyAsJson).build();
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}

@GET
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "List all loan originators", description = "Retrieves all loan originator records. Requires READ_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "403", description = "Insufficient permissions")
public List<LoanOriginatorData> retrieveAll() {
this.context.authenticatedUser().validateHasReadPermission(LoanOriginatorApiConstants.RESOURCE_NAME);

return this.loanOriginatorReadPlatformService.retrieveAll();
}

@GET
@Path("{originatorId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a loan originator by ID", description = "Retrieves a loan originator by its internal ID. Requires READ_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "403", description = "Insufficient permissions")
@ApiResponse(responseCode = "404", description = "Originator not found")
public LoanOriginatorData retrieveOne(@PathParam("originatorId") @Parameter(description = "originatorId") final Long originatorId) {
this.context.authenticatedUser().validateHasReadPermission(LoanOriginatorApiConstants.RESOURCE_NAME);

return this.loanOriginatorReadPlatformService.retrieveById(originatorId);
}

@GET
@Path("external-id/{externalId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Retrieve a loan originator by external ID", description = "Retrieves a loan originator by its external ID. Requires READ_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "403", description = "Insufficient permissions")
@ApiResponse(responseCode = "404", description = "Originator not found")
public LoanOriginatorData retrieveByExternalId(
@PathParam("externalId") @Parameter(description = "externalId") final String externalId) {
this.context.authenticatedUser().validateHasReadPermission(LoanOriginatorApiConstants.RESOURCE_NAME);

return this.loanOriginatorReadPlatformService.retrieveByExternalId(externalId);
}

@PUT
@Path("{originatorId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update a loan originator by ID", description = "Updates a loan originator by its internal ID. Requires UPDATE_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "400", description = "Incorrect format")
@ApiResponse(responseCode = "403", description = "Insufficient permissions")
@ApiResponse(responseCode = "404", description = "Originator not found")
public CommandProcessingResult update(@PathParam("originatorId") @Parameter(description = "originatorId") final Long originatorId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().updateLoanOriginator(originatorId).withJson(apiRequestBodyAsJson)
.build();
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}

@PUT
@Path("external-id/{externalId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Update a loan originator by external ID", description = "Updates a loan originator by its external ID. Requires UPDATE_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "400", description = "Incorrect format")
@ApiResponse(responseCode = "403", description = "Insufficient permissions")
@ApiResponse(responseCode = "404", description = "Originator not found")
public CommandProcessingResult updateByExternalId(
@PathParam("externalId") @Parameter(description = "externalId") final String externalId,
@Parameter(hidden = true) final String apiRequestBodyAsJson) {
final Long originatorId = this.loanOriginatorReadPlatformService.resolveIdByExternalId(externalId);

final CommandWrapper commandRequest = new CommandWrapperBuilder().updateLoanOriginator(originatorId).withJson(apiRequestBodyAsJson)
.build();
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}

@DELETE
@Path("{originatorId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a loan originator by ID", description = "Deletes a loan originator by its internal ID. Requires DELETE_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "403", description = "Originator is mapped to loans and cannot be deleted, or insufficient permissions")
@ApiResponse(responseCode = "404", description = "Originator not found")
public CommandProcessingResult delete(@PathParam("originatorId") @Parameter(description = "originatorId") final Long originatorId) {
final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteLoanOriginator(originatorId).build();
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}

@DELETE
@Path("external-id/{externalId}")
@Consumes({ MediaType.APPLICATION_JSON })
@Produces({ MediaType.APPLICATION_JSON })
@Operation(summary = "Delete a loan originator by external ID", description = "Deletes a loan originator by its external ID. Requires DELETE_LOAN_ORIGINATOR permission.")
@ApiResponse(responseCode = "200", description = "OK")
@ApiResponse(responseCode = "403", description = "Originator is mapped to loans and cannot be deleted, or insufficient permissions")
@ApiResponse(responseCode = "404", description = "Originator not found")
public CommandProcessingResult deleteByExternalId(
@PathParam("externalId") @Parameter(description = "externalId") final String externalId) {
final Long originatorId = this.loanOriginatorReadPlatformService.resolveIdByExternalId(externalId);

final CommandWrapper commandRequest = new CommandWrapperBuilder().deleteLoanOriginator(originatorId).build();
return this.commandsSourceWritePlatformService.logCommandSource(commandRequest);
}
}
Loading
Loading