This is an automated email from the ASF dual-hosted git repository.
adamsaghy pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/fineract.git
The following commit(s) were added to refs/heads/develop by this push:
new 87e328e9e7 FINERACT-2418: added e2e test covering loan origination
feature
87e328e9e7 is described below
commit 87e328e9e738d2d4d82abaa72c042215799f7bc1
Author: Rustam Zeinalov <[email protected]>
AuthorDate: Sat Feb 14 22:24:48 2026 +0100
FINERACT-2418: added e2e test covering loan origination feature
---
.../test/stepdef/loan/LoanOriginationStepDef.java | 662 +++++++++++++++++++++
.../fineract/test/support/TestContextKey.java | 6 +
.../resources/features/LoanOrigination.feature | 271 +++++++++
3 files changed, 939 insertions(+)
diff --git
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanOriginationStepDef.java
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanOriginationStepDef.java
new file mode 100644
index 0000000000..1ade03c99b
--- /dev/null
+++
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/stepdef/loan/LoanOriginationStepDef.java
@@ -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();
+ }
+}
diff --git
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
index 1b18697f58..8a3fb94a64 100644
---
a/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
+++
b/fineract-e2e-tests-core/src/test/java/org/apache/fineract/test/support/TestContextKey.java
@@ -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";
diff --git
a/fineract-e2e-tests-runner/src/test/resources/features/LoanOrigination.feature
b/fineract-e2e-tests-runner/src/test/resources/features/LoanOrigination.feature
new file mode 100644
index 0000000000..9355329d75
--- /dev/null
+++
b/fineract-e2e-tests-runner/src/test/resources/features/LoanOrigination.feature
@@ -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