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 000000000..1ade03c99 --- /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 originatorTypes = template.getOriginatorTypeOptions(); + List 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 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 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 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 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 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 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 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 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 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 1b18697f5..8a3fb94a6 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 000000000..9355329d7 --- /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