FINERACT-2418: added e2e test covering loan origination feature

This commit is contained in:
Rustam Zeinalov
2026-02-17 11:12:52 +00:00
committed by Adam Saghy
parent 588b831f8f
commit 87e328e9e7
3 changed files with 939 additions and 0 deletions
@@ -0,0 +1,662 @@
/**
* 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.test.stepdef.loan;
import static org.apache.fineract.client.feign.util.FeignCalls.executeVoid;
import static org.apache.fineract.client.feign.util.FeignCalls.fail;
import static org.apache.fineract.client.feign.util.FeignCalls.failVoid;
import static org.apache.fineract.client.feign.util.FeignCalls.ok;
import static org.assertj.core.api.Assertions.assertThat;
import io.cucumber.java.en.Then;
import io.cucumber.java.en.When;
import java.math.BigDecimal;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;
import lombok.extern.slf4j.Slf4j;
import org.apache.fineract.avro.loan.v1.OriginatorDetailsV1;
import org.apache.fineract.client.feign.FineractFeignClient;
import org.apache.fineract.client.feign.util.CallFailedRuntimeException;
import org.apache.fineract.client.models.DeleteLoanOriginatorsResponse;
import org.apache.fineract.client.models.GetCodeValuesDataResponse;
import org.apache.fineract.client.models.GetLoanOriginatorTemplateResponse;
import org.apache.fineract.client.models.GetLoanOriginatorsResponse;
import org.apache.fineract.client.models.GetLoansLoanIdOriginatorData;
import org.apache.fineract.client.models.GetLoansLoanIdResponse;
import org.apache.fineract.client.models.PostClientsResponse;
import org.apache.fineract.client.models.PostLoanOriginatorsRequest;
import org.apache.fineract.client.models.PostLoanOriginatorsResponse;
import org.apache.fineract.client.models.PostLoansLoanIdRequest;
import org.apache.fineract.client.models.PostLoansLoanIdResponse;
import org.apache.fineract.client.models.PostLoansOriginatorData;
import org.apache.fineract.client.models.PostLoansRequest;
import org.apache.fineract.client.models.PostLoansResponse;
import org.apache.fineract.client.models.PutLoanOriginatorsRequest;
import org.apache.fineract.client.models.PutLoanOriginatorsResponse;
import org.apache.fineract.test.api.ApiProperties;
import org.apache.fineract.test.factory.LoanRequestFactory;
import org.apache.fineract.test.helper.ErrorMessageHelper;
import org.apache.fineract.test.messaging.EventAssertion;
import org.apache.fineract.test.messaging.event.loan.LoanApprovedEvent;
import org.apache.fineract.test.messaging.store.EventStore;
import org.apache.fineract.test.stepdef.AbstractStepDef;
import org.apache.fineract.test.support.TestContextKey;
import org.springframework.beans.factory.annotation.Autowired;
@Slf4j
public class LoanOriginationStepDef extends AbstractStepDef {
private static final long NON_EXISTENT_ID = Long.MAX_VALUE;
@Autowired
private FineractFeignClient fineractClient;
@Autowired
private LoanRequestFactory loanRequestFactory;
@Autowired
private EventAssertion eventAssertion;
@Autowired
private EventStore eventStore;
@Autowired
private ApiProperties apiProperties;
// --- Originator CRUD steps ---
@When("Admin creates a new loan originator with external ID and name {string}")
public void createOriginatorWithName(String name) {
createOriginatorAndStore(name, null, TestContextKey.ORIGINATOR_CREATE_RESPONSE, TestContextKey.ORIGINATOR_EXTERNAL_ID);
}
@When("Admin creates a new loan originator with external ID, name {string} and status {string}")
public void createOriginatorWithNameAndStatus(String name, String status) {
createOriginatorAndStore(name, status, TestContextKey.ORIGINATOR_CREATE_RESPONSE, TestContextKey.ORIGINATOR_EXTERNAL_ID);
}
@When("Admin creates a new loan originator with all fields and name {string}")
public void createOriginatorWithAllFields(String name) {
GetLoanOriginatorTemplateResponse template = ok(() -> fineractClient.loanOriginators().retrieveLoanOriginatorTemplate());
List<GetCodeValuesDataResponse> originatorTypes = template.getOriginatorTypeOptions();
List<GetCodeValuesDataResponse> channelTypes = template.getChannelTypeOptions();
assertThat(originatorTypes).as("Originator type options should not be empty").isNotNull().isNotEmpty();
assertThat(channelTypes).as("Channel type options should not be empty").isNotNull().isNotEmpty();
GetCodeValuesDataResponse originatorType = originatorTypes.get(0);
GetCodeValuesDataResponse channelType = channelTypes.get(0);
String externalId = UUID.randomUUID().toString();
PostLoanOriginatorsRequest request = new PostLoanOriginatorsRequest().externalId(externalId).name(name)
.originatorTypeId(originatorType.getId()).channelTypeId(channelType.getId());
PostLoanOriginatorsResponse response = ok(() -> fineractClient.loanOriginators().create11(request));
assertThat(response.getResourceId()).isNotNull();
testContext().set(TestContextKey.ORIGINATOR_CREATE_RESPONSE, response);
testContext().set(TestContextKey.ORIGINATOR_EXTERNAL_ID, externalId);
testContext().set(TestContextKey.ORIGINATOR_TYPE_NAME, originatorType.getName());
testContext().set(TestContextKey.ORIGINATOR_CHANNEL_TYPE_NAME, channelType.getName());
log.info("Created originator with ID {}, externalId {}, originatorType {}, channelType {}", response.getResourceId(), externalId,
originatorType.getName(), channelType.getName());
}
@When("Admin creates a second loan originator with external ID and name {string}")
public void createSecondOriginatorWithName(String name) {
createOriginatorAndStore(name, null, TestContextKey.ORIGINATOR_SECOND_CREATE_RESPONSE,
TestContextKey.ORIGINATOR_SECOND_EXTERNAL_ID);
}
@Then("Loan originator is created successfully with status {string}")
public void verifyOriginatorStatus(String expectedStatus) {
PostLoanOriginatorsResponse createResponse = testContext().get(TestContextKey.ORIGINATOR_CREATE_RESPONSE);
Long originatorId = createResponse.getResourceId();
GetLoanOriginatorsResponse originator = ok(() -> fineractClient.loanOriginators().retrieveOne18(originatorId));
assertThat(originator.getStatus()).isEqualTo(expectedStatus);
assertThat(originator.getExternalId()).isEqualTo(testContext().get(TestContextKey.ORIGINATOR_EXTERNAL_ID));
}
@Then("Loan originator is created successfully with all fields populated")
public void verifyOriginatorAllFields() {
PostLoanOriginatorsResponse createResponse = testContext().get(TestContextKey.ORIGINATOR_CREATE_RESPONSE);
Long originatorId = createResponse.getResourceId();
String expectedExternalId = testContext().get(TestContextKey.ORIGINATOR_EXTERNAL_ID);
String expectedOriginatorTypeName = testContext().get(TestContextKey.ORIGINATOR_TYPE_NAME);
String expectedChannelTypeName = testContext().get(TestContextKey.ORIGINATOR_CHANNEL_TYPE_NAME);
GetLoanOriginatorsResponse originator = ok(() -> fineractClient.loanOriginators().retrieveOne18(originatorId));
assertThat(originator.getId()).as("Originator ID").isNotNull();
assertThat(originator.getExternalId()).as("Originator externalId").isEqualTo(expectedExternalId);
assertThat(originator.getStatus()).as("Originator status").isEqualTo("ACTIVE");
assertThat(originator.getOriginatorType()).as("Originator type").isNotNull();
assertThat(originator.getOriginatorType().getName()).as("Originator type name").isEqualTo(expectedOriginatorTypeName);
assertThat(originator.getChannelType()).as("Channel type").isNotNull();
assertThat(originator.getChannelType().getName()).as("Channel type name").isEqualTo(expectedChannelTypeName);
log.info("Verified originator {} with all fields: type={}, channel={}", originatorId, expectedOriginatorTypeName,
expectedChannelTypeName);
}
// --- Attach / Detach steps ---
@When("Admin attaches the originator to the loan")
public void attachOriginatorToLoan() {
attachOriginatorInternal(TestContextKey.ORIGINATOR_CREATE_RESPONSE);
}
@When("Admin attaches the second originator to the loan")
public void attachSecondOriginatorToLoan() {
attachOriginatorInternal(TestContextKey.ORIGINATOR_SECOND_CREATE_RESPONSE);
}
@When("Admin detaches the originator from the loan")
public void detachOriginatorFromLoan() {
PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
PostLoanOriginatorsResponse originatorResponse = testContext().get(TestContextKey.ORIGINATOR_CREATE_RESPONSE);
long loanId = loanResponse.getLoanId();
long originatorId = originatorResponse.getResourceId();
executeVoid(() -> fineractClient.loanOriginators().detachOriginatorFromLoan(loanId, originatorId));
log.info("Detached originator {} from loan {}", originatorId, loanId);
}
// --- Loan details verification steps ---
@Then("Loan details with association {string} has the originator attached")
public void verifyOriginatorInLoanDetails(String association) {
long loanId = getLoanId();
String expectedExternalId = testContext().get(TestContextKey.ORIGINATOR_EXTERNAL_ID);
List<GetLoansLoanIdOriginatorData> originators = retrieveLoanOriginators(loanId, association);
assertThat(originators).as("Originators should not be null or empty").isNotNull().isNotEmpty();
boolean found = originators.stream().anyMatch(o -> expectedExternalId.equals(o.getExternalId()));
assertThat(found).as("Expected originator with externalId %s in loan details", expectedExternalId).isTrue();
}
@Then("Loan details with association {string} has the originator with all fields attached")
public void verifyOriginatorWithAllFieldsInLoanDetails(String association) {
PostLoanOriginatorsResponse createResponse = testContext().get(TestContextKey.ORIGINATOR_CREATE_RESPONSE);
long loanId = getLoanId();
long originatorId = createResponse.getResourceId();
String expectedExternalId = testContext().get(TestContextKey.ORIGINATOR_EXTERNAL_ID);
String expectedOriginatorTypeName = testContext().get(TestContextKey.ORIGINATOR_TYPE_NAME);
String expectedChannelTypeName = testContext().get(TestContextKey.ORIGINATOR_CHANNEL_TYPE_NAME);
List<GetLoansLoanIdOriginatorData> originators = retrieveLoanOriginators(loanId, association);
assertThat(originators).as("Originators should not be null or empty").isNotNull().isNotEmpty();
GetLoansLoanIdOriginatorData originator = originators.stream().filter(o -> expectedExternalId.equals(o.getExternalId())).findFirst()
.orElseThrow(() -> new AssertionError("Originator with externalId " + expectedExternalId + " not found in loan details"));
assertThat(originator.getId()).as("Originator ID in loan details").isNotNull();
assertThat(originator.getName()).as("Originator name in loan details").isNotNull();
assertThat(originator.getStatus()).as("Originator status in loan details").isEqualTo("ACTIVE");
// Verify type fields via direct originator GET (loan details serializes CodeValueData as nested objects
// which don't map to the flat fields in the generated client model)
GetLoanOriginatorsResponse originatorDetails = ok(() -> fineractClient.loanOriginators().retrieveOne18(originatorId));
assertThat(originatorDetails.getOriginatorType()).as("Originator type").isNotNull();
assertThat(originatorDetails.getOriginatorType().getName()).as("Originator type name").isEqualTo(expectedOriginatorTypeName);
assertThat(originatorDetails.getChannelType()).as("Channel type").isNotNull();
assertThat(originatorDetails.getChannelType().getName()).as("Channel type name").isEqualTo(expectedChannelTypeName);
log.info("Verified originator with all fields in loan {} details", loanId);
}
@Then("Loan details with association {string} has {int} originator(s) attached")
public void verifyOriginatorCountInLoanDetails(String association, int expectedCount) {
long loanId = getLoanId();
List<GetLoansLoanIdOriginatorData> originators = retrieveLoanOriginators(loanId, association);
int actualCount = (originators == null) ? 0 : originators.size();
assertThat(actualCount).as("Number of originators in loan details").isEqualTo(expectedCount);
log.info("Verified loan {} has {} originators attached", loanId, actualCount);
}
@Then("Loan details with association {string} has originator with name {string}")
public void verifyOriginatorNameInLoanDetails(String association, String expectedName) {
long loanId = getLoanId();
List<GetLoansLoanIdOriginatorData> originators = retrieveLoanOriginators(loanId, association);
assertThat(originators).as("Originators should not be null or empty").isNotNull().isNotEmpty();
boolean found = originators.stream().anyMatch(o -> expectedName.equals(o.getName()));
assertThat(found).as("Expected originator with name '%s' in loan details", expectedName).isTrue();
log.info("Verified loan {} has originator with name '{}'", loanId, expectedName);
}
@Then("Loan details with association {string} has the second originator attached")
public void verifySecondOriginatorInLoanDetails(String association) {
long loanId = getLoanId();
String expectedExternalId = testContext().get(TestContextKey.ORIGINATOR_SECOND_EXTERNAL_ID);
List<GetLoansLoanIdOriginatorData> originators = retrieveLoanOriginators(loanId, association);
assertThat(originators).as("Originators should not be null or empty").isNotNull().isNotEmpty();
boolean found = originators.stream().anyMatch(o -> expectedExternalId.equals(o.getExternalId()));
assertThat(found).as("Expected second originator with externalId %s in loan details", expectedExternalId).isTrue();
}
@Then("Loan details with association {string} has no originator attached")
public void verifyNoOriginatorInLoanDetails(String association) {
long loanId = getLoanId();
List<GetLoansLoanIdOriginatorData> originators = retrieveLoanOriginators(loanId, association);
assertThat(originators).as("Originators list should be empty after detach").isNullOrEmpty();
}
// --- Failure / validation steps ---
@Then("Attaching the originator to the loan should fail")
public void attachOriginatorShouldFail() {
long loanId = getLoanId();
long originatorId = getOriginatorId();
failVoid(() -> fineractClient.loanOriginators().attachOriginatorToLoan(loanId, originatorId));
log.info("Attach originator {} to loan {} failed as expected", originatorId, loanId);
}
@Then("Attaching the originator to the loan should fail with status {int}")
public void attachOriginatorShouldFailWithStatus(int expectedStatus) {
long loanId = getLoanId();
long originatorId = getOriginatorId();
CallFailedRuntimeException exception = failVoid(
() -> fineractClient.loanOriginators().attachOriginatorToLoan(loanId, originatorId));
assertExpectedStatus(exception, expectedStatus);
log.info("Attach originator {} to loan {} failed with expected status {}", originatorId, loanId, expectedStatus);
}
@Then("Detaching the originator from the loan should fail with status {int}")
public void detachOriginatorShouldFailWithStatus(int expectedStatus) {
long loanId = getLoanId();
long originatorId = getOriginatorId();
CallFailedRuntimeException exception = failVoid(
() -> fineractClient.loanOriginators().detachOriginatorFromLoan(loanId, originatorId));
assertExpectedStatus(exception, expectedStatus);
log.info("Detach originator {} from loan {} failed with expected status {}", originatorId, loanId, expectedStatus);
}
@Then("Attaching the second originator to the loan should fail with status {int}")
public void attachSecondOriginatorShouldFailWithStatus(int expectedStatus) {
long loanId = getLoanId();
PostLoanOriginatorsResponse originatorResponse = testContext().get(TestContextKey.ORIGINATOR_SECOND_CREATE_RESPONSE);
long originatorId = originatorResponse.getResourceId();
CallFailedRuntimeException exception = failVoid(
() -> fineractClient.loanOriginators().attachOriginatorToLoan(loanId, originatorId));
assertExpectedStatus(exception, expectedStatus);
log.info("Attach second originator {} to loan {} failed with expected status {}", originatorId, loanId, expectedStatus);
}
@Then("Attaching non-existent originator to the loan should fail with status {int}")
public void attachNonExistentOriginatorShouldFail(int expectedStatus) {
long loanId = getLoanId();
CallFailedRuntimeException exception = failVoid(
() -> fineractClient.loanOriginators().attachOriginatorToLoan(loanId, NON_EXISTENT_ID));
assertExpectedStatus(exception, expectedStatus);
log.info("Attach non-existent originator to loan {} failed with expected status {}", loanId, expectedStatus);
}
@Then("Attaching the originator to non-existent loan should fail with status {int}")
public void attachOriginatorToNonExistentLoanShouldFail(int expectedStatus) {
long originatorId = getOriginatorId();
CallFailedRuntimeException exception = failVoid(
() -> fineractClient.loanOriginators().attachOriginatorToLoan(NON_EXISTENT_ID, originatorId));
assertExpectedStatus(exception, expectedStatus);
log.info("Attach originator {} to non-existent loan failed with expected status {}", originatorId, expectedStatus);
}
@Then("Creating a loan originator without name should fail with status {int}")
public void createOriginatorWithoutNameShouldFail(int expectedStatus) {
PostLoanOriginatorsRequest request = new PostLoanOriginatorsRequest().externalId(UUID.randomUUID().toString());
CallFailedRuntimeException exception = fail(() -> fineractClient.loanOriginators().create11(request));
assertExpectedStatus(exception, expectedStatus);
log.info("Create originator without name failed with expected status {}", expectedStatus);
}
@Then("Creating a loan originator without name succeeds")
public void createOriginatorWithoutNameSucceeds() {
PostLoanOriginatorsRequest request = new PostLoanOriginatorsRequest().externalId(UUID.randomUUID().toString());
PostLoanOriginatorsResponse response = ok(() -> fineractClient.loanOriginators().create11(request));
assertThat(response.getResourceId()).as("Originator created without name").isNotNull();
log.info("Created originator without name, resourceId {}", response.getResourceId());
}
// --- Permission steps ---
@Then("Created user without ATTACH_LOAN_ORIGINATOR permission fails to attach originator to the loan")
public void userWithoutAttachPermissionFails() {
long loanId = getLoanId();
long originatorId = getOriginatorId();
FineractFeignClient userClient = createClientForUser();
CallFailedRuntimeException exception = failVoid(() -> userClient.loanOriginators().attachOriginatorToLoan(loanId, originatorId));
assertExpectedStatus(exception, 403);
log.info("User without ATTACH_LOAN_ORIGINATOR permission failed to attach originator {} to loan {} as expected", originatorId,
loanId);
}
@Then("Created user without DETACH_LOAN_ORIGINATOR permission fails to detach originator from the loan")
public void userWithoutDetachPermissionFails() {
long loanId = getLoanId();
long originatorId = getOriginatorId();
FineractFeignClient userClient = createClientForUser();
CallFailedRuntimeException exception = failVoid(() -> userClient.loanOriginators().detachOriginatorFromLoan(loanId, originatorId));
assertExpectedStatus(exception, 403);
log.info("User without DETACH_LOAN_ORIGINATOR permission failed to detach originator {} from loan {} as expected", originatorId,
loanId);
}
@Then("Created user without CREATE_LOAN_ORIGINATOR permission fails to create an originator")
public void userWithoutCreatePermissionFails() {
FineractFeignClient userClient = createClientForUser();
PostLoanOriginatorsRequest request = new PostLoanOriginatorsRequest().externalId(UUID.randomUUID().toString())
.name("Should Fail Originator");
CallFailedRuntimeException exception = fail(() -> userClient.loanOriginators().create11(request));
assertExpectedStatus(exception, 403);
log.info("User without CREATE_LOAN_ORIGINATOR permission failed to create originator as expected");
}
@Then("Created user without UPDATE_LOAN_ORIGINATOR permission fails to update the originator")
public void userWithoutUpdatePermissionFails() {
long originatorId = getOriginatorId();
FineractFeignClient userClient = createClientForUser();
PutLoanOriginatorsRequest updateRequest = new PutLoanOriginatorsRequest().name("Should Fail Update");
CallFailedRuntimeException exception = fail(() -> userClient.loanOriginators().update16(originatorId, updateRequest));
assertExpectedStatus(exception, 403);
log.info("User without UPDATE_LOAN_ORIGINATOR permission failed to update originator {} as expected", originatorId);
}
@Then("Created user without DELETE_LOAN_ORIGINATOR permission fails to delete the originator")
public void userWithoutDeletePermissionFails() {
long originatorId = getOriginatorId();
FineractFeignClient userClient = createClientForUser();
CallFailedRuntimeException exception = fail(() -> userClient.loanOriginators().delete14(originatorId));
assertExpectedStatus(exception, 403);
log.info("User without DELETE_LOAN_ORIGINATOR permission failed to delete originator {} as expected", originatorId);
}
// --- Inline loan creation ---
@When("Admin creates a new Loan with originator inline submitted on date: {string}")
public void createLoanWithOriginatorInline(String submitDate) {
PostClientsResponse clientResponse = testContext().get(TestContextKey.CLIENT_CREATE_RESPONSE);
Long clientId = clientResponse.getClientId();
String originatorExternalId = UUID.randomUUID().toString();
PostLoansOriginatorData originatorData = new PostLoansOriginatorData().externalId(originatorExternalId)
.name("Inline Created Originator");
PostLoansRequest loansRequest = loanRequestFactory.defaultLoansRequest(clientId).submittedOnDate(submitDate)
.expectedDisbursementDate(submitDate).addOriginatorsItem(originatorData);
PostLoansResponse response = ok(() -> fineractClient.loans().calculateLoanScheduleOrSubmitLoanApplication(loansRequest, Map.of()));
assertThat(response.getLoanId()).isNotNull();
testContext().set(TestContextKey.LOAN_CREATE_RESPONSE, response);
testContext().set(TestContextKey.ORIGINATOR_EXTERNAL_ID, originatorExternalId);
log.info("Created loan {} with inline originator externalId {}", response.getLoanId(), originatorExternalId);
}
// --- Approve without event check + event verification ---
@When("Admin approves the loan on {string} with {string} amount and expected disbursement date on {string}")
public void approveLoanWithoutEventCheck(String approveDate, String approvedAmount, String expectedDisbursementDate) {
long loanId = getLoanId();
eventStore.reset();
PostLoansLoanIdRequest approveRequest = LoanRequestFactory.defaultLoanApproveRequest().approvedOnDate(approveDate)
.approvedLoanAmount(new BigDecimal(approvedAmount)).expectedDisbursementDate(expectedDisbursementDate);
PostLoansLoanIdResponse loanApproveResponse = ok(
() -> fineractClient.loans().stateTransitions(loanId, approveRequest, Map.of("command", "approve")));
testContext().set(TestContextKey.LOAN_APPROVAL_RESPONSE, loanApproveResponse);
log.info("Loan {} approved (event check skipped for separate verification)", loanId);
}
@Then("LoanApprovedBusinessEvent is created with originator details")
public void verifyOriginatorInApprovalEvent() {
long loanId = getLoanId();
String expectedExternalId = testContext().get(TestContextKey.ORIGINATOR_EXTERNAL_ID);
eventAssertion.assertEvent(LoanApprovedEvent.class, loanId).extractingData(loanAccountDataV1 -> {
List<OriginatorDetailsV1> originators = loanAccountDataV1.getOriginators();
assertThat(originators).as("Originators in LoanApprovedEvent should not be null or empty").isNotNull().isNotEmpty();
assertThat(originators.get(0).getExternalId()).as("Originator externalId in LoanApprovedEvent").isEqualTo(expectedExternalId);
assertThat(originators.get(0).getStatus()).as("Originator status in LoanApprovedEvent").isEqualTo("ACTIVE");
return loanAccountDataV1.getId();
}).isEqualTo(loanId);
log.info("Verified originator {} in LoanApprovedEvent for loan {}", expectedExternalId, loanId);
}
// --- Organization-level CRUD steps ---
@Then("Loan originator is retrieved successfully by external ID with all fields")
public void retrieveOriginatorByExternalIdWithAllFields() {
String expectedExternalId = testContext().get(TestContextKey.ORIGINATOR_EXTERNAL_ID);
String expectedOriginatorTypeName = testContext().get(TestContextKey.ORIGINATOR_TYPE_NAME);
String expectedChannelTypeName = testContext().get(TestContextKey.ORIGINATOR_CHANNEL_TYPE_NAME);
GetLoanOriginatorsResponse originator = ok(() -> fineractClient.loanOriginators().retrieveByExternalId(expectedExternalId));
assertThat(originator.getId()).as("Originator ID").isNotNull();
assertThat(originator.getExternalId()).as("Originator externalId").isEqualTo(expectedExternalId);
assertThat(originator.getName()).as("Originator name").isNotNull();
assertThat(originator.getStatus()).as("Originator status").isEqualTo("ACTIVE");
assertThat(originator.getOriginatorType()).as("Originator type").isNotNull();
assertThat(originator.getOriginatorType().getName()).as("Originator type name").isEqualTo(expectedOriginatorTypeName);
assertThat(originator.getChannelType()).as("Channel type").isNotNull();
assertThat(originator.getChannelType().getName()).as("Channel type name").isEqualTo(expectedChannelTypeName);
log.info("Retrieved originator by externalId {} with all fields", expectedExternalId);
}
@Then("Loan originator list contains the created originator")
public void verifyOriginatorInList() {
long expectedId = getOriginatorId();
List<GetLoanOriginatorsResponse> allOriginators = ok(() -> fineractClient.loanOriginators().retrieveAll28());
assertThat(allOriginators).as("Originator list should not be null or empty").isNotNull().isNotEmpty();
boolean found = allOriginators.stream().anyMatch(o -> o.getId() != null && o.getId() == expectedId);
assertThat(found).as("Expected originator with ID %d in list", expectedId).isTrue();
log.info("Verified originator {} is present in the list of {} originators", expectedId, allOriginators.size());
}
@Then("Loan originator template contains status options, originator type options and channel type options")
public void verifyOriginatorTemplate() {
GetLoanOriginatorTemplateResponse template = ok(() -> fineractClient.loanOriginators().retrieveLoanOriginatorTemplate());
assertThat(template.getStatusOptions()).as("Status options").isNotNull().isNotEmpty();
assertThat(template.getOriginatorTypeOptions()).as("Originator type options").isNotNull().isNotEmpty();
assertThat(template.getChannelTypeOptions()).as("Channel type options").isNotNull().isNotEmpty();
assertThat(template.getExternalId()).as("Template generated externalId").isNotNull().isNotEmpty();
log.info("Verified template: {} status options, {} originator types, {} channel types", template.getStatusOptions().size(),
template.getOriginatorTypeOptions().size(), template.getChannelTypeOptions().size());
}
@When("Admin updates the originator name to {string} and status to {string}")
public void updateOriginatorById(String newName, String newStatus) {
long originatorId = getOriginatorId();
PutLoanOriginatorsRequest updateRequest = new PutLoanOriginatorsRequest().name(newName).status(newStatus);
PutLoanOriginatorsResponse updateResponse = ok(() -> fineractClient.loanOriginators().update16(originatorId, updateRequest));
assertThat(updateResponse.getResourceId()).as("Updated originator resource ID").isEqualTo(originatorId);
log.info("Updated originator {} with name={}, status={}", originatorId, newName, newStatus);
}
@When("Admin updates the originator by external ID with name {string}")
public void updateOriginatorByExternalId(String newName) {
String externalId = testContext().get(TestContextKey.ORIGINATOR_EXTERNAL_ID);
PutLoanOriginatorsRequest updateRequest = new PutLoanOriginatorsRequest().name(newName);
PutLoanOriginatorsResponse updateResponse = ok(
() -> fineractClient.loanOriginators().updateByExternalId(externalId, updateRequest));
assertThat(updateResponse.getResourceId()).as("Updated originator resource ID").isNotNull();
log.info("Updated originator by externalId {} with name={}", externalId, newName);
}
@Then("Loan originator has name {string} and status {string}")
public void verifyOriginatorNameAndStatus(String expectedName, String expectedStatus) {
long originatorId = getOriginatorId();
GetLoanOriginatorsResponse originator = ok(() -> fineractClient.loanOriginators().retrieveOne18(originatorId));
assertThat(originator.getName()).as("Originator name").isEqualTo(expectedName);
assertThat(originator.getStatus()).as("Originator status").isEqualTo(expectedStatus);
log.info("Verified originator {} has name={}, status={}", originatorId, expectedName, expectedStatus);
}
@Then("Loan originator retrieved by external ID has name {string}")
public void verifyOriginatorByExternalIdHasName(String expectedName) {
String externalId = testContext().get(TestContextKey.ORIGINATOR_EXTERNAL_ID);
GetLoanOriginatorsResponse originator = ok(() -> fineractClient.loanOriginators().retrieveByExternalId(externalId));
assertThat(originator.getName()).as("Originator name").isEqualTo(expectedName);
log.info("Verified originator with externalId {} has name={}", externalId, expectedName);
}
@When("Admin deletes the originator by ID")
public void deleteOriginatorById() {
long originatorId = getOriginatorId();
DeleteLoanOriginatorsResponse deleteResponse = ok(() -> fineractClient.loanOriginators().delete14(originatorId));
assertThat(deleteResponse.getResourceId()).as("Deleted originator resource ID").isEqualTo(originatorId);
log.info("Deleted originator by ID {}", originatorId);
}
@When("Admin deletes the originator by external ID")
public void deleteOriginatorByExternalId() {
String externalId = testContext().get(TestContextKey.ORIGINATOR_EXTERNAL_ID);
DeleteLoanOriginatorsResponse deleteResponse = ok(() -> fineractClient.loanOriginators().deleteByExternalId(externalId));
assertThat(deleteResponse.getResourceId()).as("Deleted originator resource ID").isNotNull();
log.info("Deleted originator by externalId {}", externalId);
}
@Then("Retrieving the deleted originator by ID should fail with status {int}")
public void retrieveDeletedOriginatorByIdShouldFail(int expectedStatus) {
long originatorId = getOriginatorId();
CallFailedRuntimeException exception = fail(() -> fineractClient.loanOriginators().retrieveOne18(originatorId));
assertExpectedStatus(exception, expectedStatus);
log.info("Retrieving deleted originator {} failed with expected status {}", originatorId, expectedStatus);
}
@Then("Retrieving the deleted originator by external ID should fail with status {int}")
public void retrieveDeletedOriginatorByExternalIdShouldFail(int expectedStatus) {
String externalId = testContext().get(TestContextKey.ORIGINATOR_EXTERNAL_ID);
CallFailedRuntimeException exception = fail(() -> fineractClient.loanOriginators().retrieveByExternalId(externalId));
assertExpectedStatus(exception, expectedStatus);
log.info("Retrieving deleted originator by externalId {} failed with expected status {}", externalId, expectedStatus);
}
@Then("Deleting the originator should fail with status {int}")
public void deleteOriginatorShouldFailWithStatus(int expectedStatus) {
long originatorId = getOriginatorId();
CallFailedRuntimeException exception = fail(() -> fineractClient.loanOriginators().delete14(originatorId));
assertExpectedStatus(exception, expectedStatus);
log.info("Deleting originator {} failed with expected status {}", originatorId, expectedStatus);
}
// --- Helper methods ---
private long getLoanId() {
PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
return loanResponse.getLoanId();
}
private long getOriginatorId() {
PostLoanOriginatorsResponse response = testContext().get(TestContextKey.ORIGINATOR_CREATE_RESPONSE);
return response.getResourceId();
}
private void createOriginatorAndStore(String name, String status, String responseKey, String externalIdKey) {
String externalId = UUID.randomUUID().toString();
PostLoanOriginatorsRequest request = new PostLoanOriginatorsRequest().externalId(externalId).name(name);
if (status != null) {
request.status(status);
}
PostLoanOriginatorsResponse response = ok(() -> fineractClient.loanOriginators().create11(request));
assertThat(response.getResourceId()).isNotNull();
testContext().set(responseKey, response);
testContext().set(externalIdKey, externalId);
log.info("Created originator with ID {} and externalId {}", response.getResourceId(), externalId);
}
private void attachOriginatorInternal(String originatorContextKey) {
PostLoansResponse loanResponse = testContext().get(TestContextKey.LOAN_CREATE_RESPONSE);
PostLoanOriginatorsResponse originatorResponse = testContext().get(originatorContextKey);
long loanId = loanResponse.getLoanId();
long originatorId = originatorResponse.getResourceId();
executeVoid(() -> fineractClient.loanOriginators().attachOriginatorToLoan(loanId, originatorId));
log.info("Attached originator {} to loan {}", originatorId, loanId);
}
private List<GetLoansLoanIdOriginatorData> retrieveLoanOriginators(long loanId, String association) {
GetLoansLoanIdResponse loanDetails = ok(() -> fineractClient.loans().retrieveLoan(loanId,
Map.of("staffInSelectedOfficeOnly", false, "associations", association, "exclude", "", "fields", "")));
return loanDetails.getOriginators();
}
private void assertExpectedStatus(CallFailedRuntimeException exception, int expectedStatus) {
assertThat(exception.getStatus()).as(ErrorMessageHelper.wrongErrorCode(exception.getStatus(), expectedStatus))
.isEqualTo(expectedStatus);
}
private FineractFeignClient createClientForUser() {
String username = testContext().get(TestContextKey.CREATED_SIMPLE_USER_USERNAME);
String password = testContext().get(TestContextKey.CREATED_SIMPLE_USER_PASSWORD);
String apiBaseUrl = apiProperties.getBaseUrl() + "/fineract-provider/api/";
return FineractFeignClient.builder().baseUrl(apiBaseUrl).credentials(username, password).tenantId(apiProperties.getTenantId())
.disableSslVerification(true).connectTimeout(60, TimeUnit.SECONDS)
.readTimeout((int) apiProperties.getReadTimeout(), TimeUnit.SECONDS).build();
}
}
@@ -298,6 +298,12 @@ public abstract class TestContextKey {
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADVANCED_CUSTOM_PAYMENT_ALLOCATION_PROGRESSIVE_LOAN_SCHEDULE_PRINCIPAL_FIRST = "loanProductCreateResponseLP2AdvancedPaymentHorizontalPrincipalFirst";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INTEREST_DAILY_EMI_360_30_INTEREST_RECALC_DAILY_MULTIDISBURSE_FULL_TERM_TRANCHE = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyMultidisburseFullTermTranche";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL_360_30_USD = "loanProductCreateResponseLP2AdvancedPaymentHorizontal36030Usd";
public static final String ORIGINATOR_CREATE_RESPONSE = "originatorCreateResponse";
public static final String ORIGINATOR_EXTERNAL_ID = "originatorExternalId";
public static final String ORIGINATOR_TYPE_NAME = "originatorTypeName";
public static final String ORIGINATOR_CHANNEL_TYPE_NAME = "originatorChannelTypeName";
public static final String ORIGINATOR_SECOND_CREATE_RESPONSE = "originatorSecondCreateResponse";
public static final String ORIGINATOR_SECOND_EXTERNAL_ID = "originatorSecondExternalId";
public static final String VERIFIED_LOAN_ACCRUALS = "VERIFIED_LOAN_ACCRUALS";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INT_DAILY_EMI_360_30_INT_RECALC_DAILY_MULTIDISB_FULL_TERM_TRANCHE_DOWNPAYMENT = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyMultidisburseFullTermTrancheDownPayment";
public static final String DEFAULT_LOAN_PRODUCT_CREATE_RESPONSE_LP2_ADV_PYMNT_INT_DAILY_EMI_360_30_INT_RECALC_DAILY_MULTIDISB_FULL_TERM_TRANCHE_DOWNPAYMENT_AUTO = "loanProductCreateResponseLP2AdvancedPaymentInterestDailyEmi36030InterestRecalculationDailyMultidisburseFullTermTrancheDownPaymentAuto";
@@ -0,0 +1,271 @@
@LoanOriginationFeature
Feature: Loan Origination
@TestRailId:C4649
Scenario: Verify loan originator registration, attachment to loan, and persistence through approval and disbursal
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID and name "Merchant Partner Alpha"
Then Loan originator is created successfully with status "ACTIVE"
When Admin creates a new default Loan with date: "1 January 2025"
When Admin attaches the originator to the loan
Then Loan details with association "originators" has the originator attached
And Admin successfully approves the loan on "1 January 2025" with "1000" amount and expected disbursement date on "1 January 2025"
When Admin successfully disburse the loan on "1 January 2025" with "1000" EUR transaction amount
Then Loan details with association "originators" has the originator attached
@TestRailId:C4650
Scenario: Verify loan originator inline attachment with auto-creation during loan application
Given Global configuration "enable-originator-creation-during-loan-application" is enabled
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new Loan with originator inline submitted on date: "1 January 2025"
Then Loan details with association "originators" has the originator attached
And Admin successfully approves the loan on "1 January 2025" with "1000" amount and expected disbursement date on "1 January 2025"
When Admin successfully disburse the loan on "1 January 2025" with "1000" EUR transaction amount
Then Loan details with association "originators" has the originator attached
Given Global configuration "enable-originator-creation-during-loan-application" is disabled
@TestRailId:C4651
Scenario: Verify loan originator details in external business event after loan approval
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID and name "Event Test Originator"
When Admin creates a new default Loan with date: "1 January 2025"
When Admin attaches the originator to the loan
When Admin approves the loan on "1 January 2025" with "1000" amount and expected disbursement date on "1 January 2025"
Then LoanApprovedBusinessEvent is created with originator details
@TestRailId:C4652
Scenario: Verify loan originator detachment from loan before approval
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID and name "Detach Test Originator"
When Admin creates a new default Loan with date: "1 January 2025"
When Admin attaches the originator to the loan
Then Loan details with association "originators" has the originator attached
When Admin detaches the originator from the loan
Then Loan details with association "originators" has no originator attached
@TestRailId:C4653
Scenario: Verify that inactive originator cannot be attached to loan
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID, name "Inactive Originator" and status "PENDING"
When Admin creates a new default Loan with date: "1 January 2025"
Then Attaching the originator to the loan should fail
@TestRailId:C4654
Scenario: Verify loan originator creation with all fields and persistence in loan details
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with all fields and name "Full Fields Originator"
Then Loan originator is created successfully with all fields populated
When Admin creates a new default Loan with date: "1 January 2025"
When Admin attaches the originator to the loan
Then Loan details with association "originators" has the originator with all fields attached
@TestRailId:C4655
Scenario: Verify that originator cannot be attached to approved loan
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID and name "Post Approval Originator"
When Admin creates a new default Loan with date: "1 January 2025"
And Admin successfully approves the loan on "1 January 2025" with "1000" amount and expected disbursement date on "1 January 2025"
Then Attaching the originator to the loan should fail with status 403
@TestRailId:C4656
Scenario: Verify that originator cannot be detached from approved loan
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID and name "Pre Approval Detach Originator"
When Admin creates a new default Loan with date: "1 January 2025"
When Admin attaches the originator to the loan
And Admin successfully approves the loan on "1 January 2025" with "1000" amount and expected disbursement date on "1 January 2025"
Then Detaching the originator from the loan should fail with status 403
@TestRailId:C4657
Scenario: Verify that same originator cannot be attached to loan twice
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID and name "Duplicate Originator"
When Admin creates a new default Loan with date: "1 January 2025"
When Admin attaches the originator to the loan
Then Attaching the originator to the loan should fail with status 403
@TestRailId:C4658
Scenario: Verify that non-attached originator cannot be detached from loan
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID and name "Not Attached Originator"
When Admin creates a new default Loan with date: "1 January 2025"
Then Detaching the originator from the loan should fail with status 404
@TestRailId:C4659
Scenario: Verify that originator cannot be attached to non-existent loan
When Admin creates a new loan originator with external ID and name "Orphan Originator"
Then Attaching the originator to non-existent loan should fail with status 404
@TestRailId:C4660
Scenario: Verify that non-existent originator cannot be attached to loan
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new default Loan with date: "1 January 2025"
Then Attaching non-existent originator to the loan should fail with status 404
@TestRailId:C4661
Scenario: Verify loan originator creation without name succeeds with default handling
Then Creating a loan originator without name succeeds
@TestRailId:C4662
Scenario: Verify that user without ATTACH_LOAN_ORIGINATOR permission cannot attach originator
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID and name "Permission Test Originator"
When Admin creates a new default Loan with date: "1 January 2025"
When Admin creates new user with "ORIGINATOR_NO_ATTACH" username, "ORIGINATOR_NO_ATTACH_ROLE" role name and given permissions:
| READ_LOAN |
Then Created user without ATTACH_LOAN_ORIGINATOR permission fails to attach originator to the loan
@TestRailId:C4663
Scenario: Verify that user without DETACH_LOAN_ORIGINATOR permission cannot detach originator
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID and name "Permission Detach Originator"
When Admin creates a new default Loan with date: "1 January 2025"
When Admin attaches the originator to the loan
When Admin creates new user with "ORIGINATOR_NO_DETACH" username, "ORIGINATOR_NO_DETACH_ROLE" role name and given permissions:
| READ_LOAN |
Then Created user without DETACH_LOAN_ORIGINATOR permission fails to detach originator from the loan
@TestRailId:C4664
Scenario: Verify loan originator persistence through full loan lifecycle with repayments
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with all fields and name "Lifecycle Originator"
When Admin creates a fully customized loan with the following data:
| LoanProduct | submitted on date | with Principal | ANNUAL interest rate % | interest type | interest calculation period | amortization type | loanTermFrequency | loanTermFrequencyType | repaymentEvery | repaymentFrequencyType | numberOfRepayments | graceOnPrincipalPayment | graceOnInterestPayment | interest free period | Payment strategy |
| LP2_ADV_CUSTOM_PMT_ALLOC_PROGRESSIVE_LOAN_SCHEDULE_HORIZONTAL | 01 January 2025 | 1000 | 12 | DECLINING_BALANCE | DAILY | EQUAL_INSTALLMENTS | 3 | MONTHS | 1 | MONTHS | 3 | 0 | 0 | 0 | ADVANCED_PAYMENT_ALLOCATION |
When Admin attaches the originator to the loan
Then Loan details with association "originators" has the originator with all fields attached
And Admin successfully approves the loan on "1 January 2025" with "1000" amount and expected disbursement date on "1 January 2025"
When Admin successfully disburse the loan on "1 January 2025" with "1000" EUR transaction amount
Then Loan details with association "originators" has the originator with all fields attached
When Admin sets the business date to "1 February 2025"
And Customer makes "AUTOPAY" repayment on "1 February 2025" with 340 EUR transaction amount
Then Loan details with association "originators" has the originator with all fields attached
When Admin sets the business date to "1 March 2025"
And Customer makes "AUTOPAY" repayment on "1 March 2025" with 340 EUR transaction amount
Then Loan details with association "originators" has the originator with all fields attached
@TestRailId:C4665
Scenario: Verify multiple originators on a loan with add, update, and detach operations
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID and name "First Originator"
When Admin creates a second loan originator with external ID and name "Second Originator"
When Admin creates a new default Loan with date: "1 January 2025"
When Admin attaches the originator to the loan
When Admin attaches the second originator to the loan
Then Loan details with association "originators" has 2 originators attached
When Admin updates the originator name to "First Originator Updated" and status to "ACTIVE"
Then Loan details with association "originators" has originator with name "First Originator Updated"
When Admin detaches the originator from the loan
Then Loan details with association "originators" has 1 originator attached
And Loan details with association "originators" has the second originator attached
@TestRailId:C4666
Scenario: Verify loan originator persistence through undo approval
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID and name "Undo Approval Originator"
When Admin creates a new default Loan with date: "1 January 2025"
When Admin attaches the originator to the loan
Then Loan details with association "originators" has the originator attached
And Admin successfully approves the loan on "1 January 2025" with "1000" amount and expected disbursement date on "1 January 2025"
Then Loan details with association "originators" has the originator attached
Then Admin can successfully undone the loan approval
Then Loan details with association "originators" has the originator attached
@TestRailId:C4667
Scenario: Verify loan originator persistence through undo disbursal
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID and name "Undo Disbursal Originator"
When Admin creates a new default Loan with date: "1 January 2025"
When Admin attaches the originator to the loan
And Admin successfully approves the loan on "1 January 2025" with "1000" amount and expected disbursement date on "1 January 2025"
When Admin successfully disburse the loan on "1 January 2025" with "1000" EUR transaction amount
Then Loan details with association "originators" has the originator attached
When Admin successfully undo disbursal
Then Loan details with association "originators" has the originator attached
@TestRailId:C4668
Scenario: Verify loan originator persistence through loan charge-off
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID and name "Charge Off Originator"
When Admin creates a new default Loan with date: "1 January 2025"
When Admin attaches the originator to the loan
And Admin successfully approves the loan on "1 January 2025" with "1000" amount and expected disbursement date on "1 January 2025"
When Admin successfully disburse the loan on "1 January 2025" with "1000" EUR transaction amount
Then Loan details with association "originators" has the originator attached
And Admin does charge-off the loan on "1 January 2025"
Then Loan details with association "originators" has the originator attached
@TestRailId:C4669
Scenario: Verify that originator cannot be attached or detached from disbursed loan
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID and name "Disbursed Loan Originator"
When Admin creates a second loan originator with external ID and name "Disbursed Loan Extra Originator"
When Admin creates a new default Loan with date: "1 January 2025"
When Admin attaches the originator to the loan
And Admin successfully approves the loan on "1 January 2025" with "1000" amount and expected disbursement date on "1 January 2025"
When Admin successfully disburse the loan on "1 January 2025" with "1000" EUR transaction amount
Then Attaching the second originator to the loan should fail with status 403
And Detaching the originator from the loan should fail with status 403
@TestRailId:C4670
Scenario: Verify loan originator read operations with retrieve by external ID, list all, and template
When Admin creates a new loan originator with all fields and name "CRUD Read Originator"
Then Loan originator is retrieved successfully by external ID with all fields
And Loan originator list contains the created originator
And Loan originator template contains status options, originator type options and channel type options
@TestRailId:C4671
Scenario: Verify loan originator update operations by ID and by external ID
When Admin creates a new loan originator with all fields and name "CRUD Update Originator"
Then Loan originator is created successfully with status "ACTIVE"
When Admin updates the originator name to "Updated Name" and status to "INACTIVE"
Then Loan originator has name "Updated Name" and status "INACTIVE"
When Admin updates the originator by external ID with name "ExtId Updated Name"
Then Loan originator retrieved by external ID has name "ExtId Updated Name"
@TestRailId:C4672
Scenario: Verify loan originator delete operations by ID, by external ID, and deletion prevention when mapped to loan
When Admin creates a new loan originator with external ID and name "Delete By ID Originator"
When Admin deletes the originator by ID
Then Retrieving the deleted originator by ID should fail with status 404
When Admin creates a new loan originator with external ID and name "Delete By ExtId Originator"
When Admin deletes the originator by external ID
Then Retrieving the deleted originator by external ID should fail with status 404
When Admin sets the business date to "1 January 2025"
When Admin creates a client with random data
When Admin creates a new loan originator with external ID and name "Mapped Originator"
When Admin creates a new default Loan with date: "1 January 2025"
When Admin attaches the originator to the loan
Then Deleting the originator should fail with status 403
@TestRailId:C4673
Scenario: Verify loan originator CRUD permission checks with create, update, and delete denied without permissions
When Admin creates a new loan originator with external ID and name "Permission CRUD Originator"
When Admin creates new user with "ORIGINATOR_NO_CREATE" username, "ORIGINATOR_NO_CREATE_ROLE" role name and given permissions:
| READ_LOAN |
Then Created user without CREATE_LOAN_ORIGINATOR permission fails to create an originator
When Admin creates new user with "ORIGINATOR_NO_UPDATE" username, "ORIGINATOR_NO_UPDATE_ROLE" role name and given permissions:
| READ_LOAN_ORIGINATOR |
Then Created user without UPDATE_LOAN_ORIGINATOR permission fails to update the originator
When Admin creates new user with "ORIGINATOR_NO_DELETE" username, "ORIGINATOR_NO_DELETE_ROLE" role name and given permissions:
| READ_LOAN_ORIGINATOR |
Then Created user without DELETE_LOAN_ORIGINATOR permission fails to delete the originator