diff --git a/bpdm-system-tester/src/main/kotlin/org/eclipse/tractusx/bpdm/test/system/stepdefinations/UltimateOwnerDistributionStepDefs.kt b/bpdm-system-tester/src/main/kotlin/org/eclipse/tractusx/bpdm/test/system/stepdefinations/UltimateOwnerDistributionStepDefs.kt new file mode 100644 index 000000000..ffade47ff --- /dev/null +++ b/bpdm-system-tester/src/main/kotlin/org/eclipse/tractusx/bpdm/test/system/stepdefinations/UltimateOwnerDistributionStepDefs.kt @@ -0,0 +1,173 @@ +/******************************************************************************* + * Copyright (c) 2021 Contributors to the Eclipse Foundation + * + * See the NOTICE file(s) distributed with this work for additional + * information regarding copyright ownership. + * + * This program and the accompanying materials are made available under the + * terms of the Apache License, Version 2.0 which is available at + * https://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. + * + * SPDX-License-Identifier: Apache-2.0 + ******************************************************************************/ + +package org.eclipse.tractusx.bpdm.test.system.stepdefinations + +import io.cucumber.java.en.And +import io.cucumber.java.en.Then +import io.cucumber.java.en.When +import mu.KotlinLogging +import org.assertj.core.api.Assertions.assertThat +import org.eclipse.tractusx.bpdm.gate.api.client.GateClient +import org.eclipse.tractusx.bpdm.gate.api.model.response.LegalEntityRepresentationInputDto +import org.eclipse.tractusx.bpdm.pool.api.client.PoolApiClient +import org.eclipse.tractusx.bpdm.pool.api.model.LegalEntityDto +import org.eclipse.tractusx.bpdm.pool.api.model.request.LegalEntityPartnerUpdateRequest +import org.eclipse.tractusx.bpdm.test.system.utils.ScenarioContext +import tools.jackson.databind.json.JsonMapper + +/** + * Steps for the "Ultimate Owner Distribution" feature. + * + * These steps handle marking legal entities as ultimate owners and verifying that the + * ultimateOwnerBpnl is correctly reflected in the output of owned entities. + */ +class UltimateOwnerDistributionStepDefs( + private val gateClient: GateClient, + private val poolClient: PoolApiClient, + private val jsonMapper: JsonMapper +) : SpringTestRunConfiguration() { + + companion object { + private val logger = KotlinLogging.logger {} + } + + /** + * Sharing member marks an entity as the ultimate owner via the Gate. + * This uploads the ultimate owner flag to the Gate input. + */ + @When("the sharing member marks {string} as the ultimate owner") + fun sharingMemberMarksAsUltimateOwner(entityId: String) { + logger.info { "Sharing member marks '$entityId' as the ultimate owner" } + + val context = ScenarioContext.current() ?: error("No active scenario context") + + // Get the legal entity from context + val legalEntityWithAddress = context.legalEntities[entityId] + ?: error("Legal entity '$entityId' not found in scenario context") + + // Create the Gate input representation with ultimate owner flag + val legalEntityInput = LegalEntityRepresentationInputDto( + legalEntityBpn = legalEntityWithAddress.header.bpnl, + legalName = legalEntityWithAddress.header.legalName, + shortName = legalEntityWithAddress.header.legalShortName, + legalForm = legalEntityWithAddress.header.legalFormVerbose?.technicalKey, + ownershipUltimate = true, + states = emptyList() + ) + + // Upload to Gate via upsertBusinessPartnersInput + val inputRequest = context.inputData[entityId]?.copy( + externalId = context.runId(entityId), + legalEntity = legalEntityInput + ) ?: error("Input data for '$entityId' not found in scenario context") + + gateClient.businessParters.upsertBusinessPartnersInput(listOf(inputRequest)) + logger.info { "Successfully uploaded '$entityId' as ultimate owner to Gate" } + } + + /** + * Golden record process confirms an entity as the ultimate owner. + * This updates the Pool with the ultimate owner flag from the golden record. + */ + @And("the golden record process confirms {string} as the ultimate owner") + fun goldenRecordConfirmsAsUltimateOwner(entityId: String) { + logger.info { "Golden record process confirms '$entityId' as the ultimate owner" } + + val context = ScenarioContext.current() ?: error("No active scenario context") + + // Get the legal entity from context + val legalEntityWithAddress = context.legalEntities[entityId] + ?: error("Legal entity '$entityId' not found in scenario context") + + // Convert verbose DTO to non-verbose DTO using JSON serialization + val verboseJson = jsonMapper.writeValueAsString(legalEntityWithAddress) + val legalEntity = jsonMapper.readValue(verboseJson, LegalEntityDto::class.java) + + // Update the legal entity to mark it as ultimate owner + val updatedLegalEntity = legalEntity.copy( + header = legalEntity.header.copy( + ownershipUltimate = true + ) + ) + + // Update the legal entity in the Pool to mark it as ultimate owner + val updateRequest = LegalEntityPartnerUpdateRequest( + bpnl = legalEntityWithAddress.header.bpnl, + legalEntity = updatedLegalEntity + ) + + poolClient.legalEntities.updateBusinessPartners(listOf(updateRequest)) + logger.info { "Successfully confirmed '$entityId' as ultimate owner in Pool" } + } + + /** + * Asserts that a record's output reflects a specific entity as the ultimate owner. + */ + @Then("{string} output reflects {string} as the ultimate owner") + fun assertUltimateOwner(recordId: String, expectedOwnerRecordId: String) { + logger.info { "Asserting that '$recordId' output reflects '$expectedOwnerRecordId' as the ultimate owner" } + + val context = ScenarioContext.current() ?: error("No active scenario context") + + // Get the output from the Gate + val runId = context.runId(recordId) + val outputPage = gateClient.businessParters.getBusinessPartnersOutput(listOf(runId)) + val output = outputPage.content.firstOrNull() + ?: error("No output found for record '$recordId'") + + // Get the expected owner's BPNL + val expectedOwnerRunId = context.runId(expectedOwnerRecordId) + val ownerOutputPage = gateClient.businessParters.getBusinessPartnersOutput(listOf(expectedOwnerRunId)) + val ownerOutput = ownerOutputPage.content.firstOrNull() + ?: error("No output found for owner record '$expectedOwnerRecordId'") + + val expectedBpnl = ownerOutput.legalEntity.legalEntityBpn + + // Assert the ultimateOwnerBpnl matches + assertThat(output.legalEntity.ultimateOwnerBpnl) + .withFailMessage("Expected '$recordId' to have ultimate owner '$expectedBpnl' but was '${output.legalEntity.ultimateOwnerBpnl}'") + .isEqualTo(expectedBpnl) + + logger.info { "Successfully asserted '$recordId' has ultimate owner '$expectedOwnerRecordId'" } + } + + /** + * Asserts that a record's output has no ultimate owner (null). + */ + @Then("{string} output has no ultimate owner") + fun assertNoUltimateOwner(recordId: String) { + logger.info { "Asserting that '$recordId' output has no ultimate owner" } + + val context = ScenarioContext.current() ?: error("No active scenario context") + + // Get the output from the Gate + val runId = context.runId(recordId) + val outputPage = gateClient.businessParters.getBusinessPartnersOutput(listOf(runId)) + val output = outputPage.content.firstOrNull() + ?: error("No output found for record '$recordId'") + + // Assert the ultimateOwnerBpnl is null + assertThat(output.legalEntity.ultimateOwnerBpnl) + .withFailMessage("Expected '$recordId' to have no ultimate owner but was '${output.legalEntity.ultimateOwnerBpnl}'") + .isNull() + + logger.info { "Successfully asserted '$recordId' has no ultimate owner" } + } +} diff --git a/bpdm-system-tester/src/main/resources/cucumber/ultimate_owner_distribution.feature b/bpdm-system-tester/src/main/resources/cucumber/ultimate_owner_distribution.feature new file mode 100644 index 000000000..8ce503cd6 --- /dev/null +++ b/bpdm-system-tester/src/main/resources/cucumber/ultimate_owner_distribution.feature @@ -0,0 +1,131 @@ +# This feature covers how the ultimate owner flag is distributed through the business partner +# ownership hierarchy. When a legal entity is marked as the ultimate owner, this flag should be +# reflected in the output of all entities that are owned by it (directly or indirectly). +# +# The ultimate owner resolution follows these rules: +# - An entity marked with ownershipUltimate = true is its own ultimate owner +# - An entity owned by an ultimate owner inherits that ultimate owner's BPNL +# - When no ultimate owner exists in the chain, entities report null +@CXTPM-1743 +Feature: Ultimate Owner Distribution + + #h3. Test Objective: + # + #* Verify that when a legal entity is marked as ultimate owner, owned entities report this ultimate owner. + # + #h3. Preconditions: + # + ## A legal entity hierarchy exists with an owner and owned entity. + # + #h3. Description: + # + ## The owner entity is marked as ownershipUltimate = true in the golden record. + ## An IsOwnedBy relation is established from the child to the parent. + ## The golden record process processes the updates. + ## The child entity reports the parent's BPNL as its ultimate owner. + @TEST_CXTPM-1743-001 @BPDM + Scenario: Owned entity reports ultimate owner from ownership chain + Given record "parent-record" reflects legal entity "parent" + And record "child-record" reflects legal entity "child" + When the sharing member shares relation "ownership" of type "IsOwnedBy" from "child-record" to "parent-record" + And the golden record process establishes relation "ownership" + And the sharing member marks "parent" as the ultimate owner + And the golden record process confirms "parent" as the ultimate owner + Then "child-record" output reflects "parent" as the ultimate owner + + #h3. Test Objective: + # + #* Verify that ultimate owner flag propagates through multi-level ownership hierarchy. + # + #h3. Preconditions: + # + ## A multi-level legal entity hierarchy exists (grandparent -> parent -> child). + # + #h3. Description: + # + ## The grandparent entity is marked as ownershipUltimate = true. + ## IsOwnedBy relations connect child -> parent -> grandparent. + ## The golden record process processes the updates. + ## Both parent and child report the grandparent's BPNL as ultimate owner. + @TEST_CXTPM-1743-002 @BPDM + Scenario: Ultimate owner propagates through multi-level hierarchy + Given record "grandparent-record" reflects legal entity "grandparent" + And record "parent-record" reflects legal entity "parent" + And record "child-record" reflects legal entity "child" + When the sharing member shares relation "parent-ownership" of type "IsOwnedBy" from "parent-record" to "grandparent-record" + And the golden record process establishes relation "parent-ownership" + And the sharing member shares relation "child-ownership" of type "IsOwnedBy" from "child-record" to "parent-record" + And the golden record process establishes relation "child-ownership" + And the sharing member marks "grandparent" as the ultimate owner + And the golden record process confirms "grandparent" as the ultimate owner + Then "parent-record" output reflects "grandparent" as the ultimate owner + And "child-record" output reflects "grandparent" as the ultimate owner + + #h3. Test Objective: + # + #* Verify that when no ultimate owner exists in the ownership chain, entities report null. + # + #h3. Preconditions: + # + ## A legal entity hierarchy exists with no entity marked as ultimate owner. + # + #h3. Description: + # + ## No entity in the hierarchy is marked as ownershipUltimate = true. + ## IsOwnedBy relations are established between the entities. + ## The golden record process processes the hierarchy. + ## All entities report null as their ultimate owner. + @TEST_CXTPM-1743-003 @BPDM + Scenario: No ultimate owner reported when flag holder does not exist in chain + Given record "parent-record" reflects legal entity "parent" + And record "child-record" reflects legal entity "child" + When the sharing member shares relation "ownership" of type "IsOwnedBy" from "child-record" to "parent-record" + And the golden record process establishes relation "ownership" + Then "child-record" output has no ultimate owner + And "parent-record" output has no ultimate owner + + #h3. Test Objective: + # + #* Verify that an entity marked as ultimate owner reports itself as the ultimate owner. + # + #h3. Preconditions: + # + ## A legal entity is marked as ownershipUltimate = true in the golden record. + # + #h3. Description: + # + ## The entity is marked as ownershipUltimate = true in the golden record. + ## The golden record process processes the update. + ## The entity reports its own BPNL as the ultimate owner. + @TEST_CXTPM-1743-004 @BPDM + Scenario: Entity marked as ultimate owner reports itself + Given record "self-owner-record" reflects legal entity "self-owner" + When the sharing member marks "self-owner" as the ultimate owner + And the golden record process confirms "self-owner" as the ultimate owner + Then "self-owner-record" output reflects "self-owner" as the ultimate owner + + #h3. Test Objective: + # + #* Verify that ultimate owner flag change propagates to already-owned entities. + # + #h3. Preconditions: + # + ## A legal entity hierarchy exists with no entity marked as ultimate owner initially. + # + #h3. Description: + # + ## Initially, no entity is marked as ultimate owner. + ## IsOwnedBy relations are established. + ## Later, the parent entity is marked as ownershipUltimate = true. + ## The golden record process processes the update. + ## The child entity now reports the parent's BPNL as ultimate owner. + @TEST_CXTPM-1743-005 @BPDM + Scenario: Ultimate owner flag change propagates to existing owned entities + Given record "parent-record" reflects legal entity "parent" + And record "child-record" reflects legal entity "child" + When the sharing member shares relation "ownership" of type "IsOwnedBy" from "child-record" to "parent-record" + And the golden record process establishes relation "ownership" + And the sharing member marks "parent" as the ultimate owner + And the golden record process confirms "parent" as the ultimate owner + Then "child-record" output reflects "parent" as the ultimate owner + And "parent-record" output reflects "parent" as the ultimate owner