Repository: eclipse-tractusx/industry-core-hub
Pull request #158: feat: added services for the APIs
Author: login: sm29105
Created: 2025-04-23T12:32:45Z | Merged: 2025-05-05T06:47:07Z

<!-- 
Thanks for your contribution! 
Please follow the instructions on your PRs title and description.
aligned title description: '(feat|fix|chore|doc): _description of introduced change_'
Important: Contributing Guidelines can be found here: https://eclipse-tractusx.github.io/docs/oss/how-to-contribute
Info: <!- text comments ->  will be hidden from the rendered preview of your PR.
-->

## Description
This includes a services layer defining the later API in a technology independent way.

It depends on PR #156 and #157.

<!-- Please tag the related issue `Fixes or Updates #issue_number`, if applicable. -->

## Pre-review checks

Please ensure to do as many of the following checks as possible, before asking for committer review:

- [x] DEPENDENCIES are up-to-date. [Dash license tool](https://github.com/eclipse/dash-licenses). Committers can open IP issues for restricted libs.
- [x] [Copyright and license header](https://eclipse-tractusx.github.io/docs/release/trg-7/trg-7-02) are present on all affected files



Diff:
diff --git a/ichub-backend/controllers/__init__.py b/ichub-backend/controllers/__init__.py
new file mode 100644
index 000000000..d47335233
--- /dev/null
+++ b/ichub-backend/controllers/__init__.py
@@ -0,0 +1,25 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+# Package-level variables
+__author__ = 'Eclipse Tractus-X Contributors'
+__license__ = "Apache License, Version 2.0"
diff --git a/ichub-backend/controllers/fastapi/__init__.py b/ichub-backend/controllers/fastapi/__init__.py
new file mode 100644
index 000000000..1359e6172
--- /dev/null
+++ b/ichub-backend/controllers/fastapi/__init__.py
@@ -0,0 +1,27 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+# Package-level variables
+__author__ = 'Eclipse Tractus-X Contributors'
+__license__ = "Apache License, Version 2.0"
+
+from .app import app
diff --git a/ichub-backend/controllers/fastapi/app.py b/ichub-backend/controllers/fastapi/app.py
new file mode 100644
index 000000000..6425499ae
--- /dev/null
+++ b/ichub-backend/controllers/fastapi/app.py
@@ -0,0 +1,68 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 DRÄXLMAIER Group
+# (represented by Lisa Dräxlmaier GmbH)
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+from typing import List, Optional
+
+from fastapi import FastAPI
+
+from services.part_management_service import PartManagementService
+from services.partner_management_service import PartnerManagementService
+from models.services.part_management import CatalogPartRead, PartnerCatalogPartBase
+from models.services.partner_management import BusinessPartnerRead, BusinessPartnerCreate, BusinessPartnerCreateInput, DataExchangeAgreementRead
+
+app = FastAPI(title="Industry Core Hub Backend API", version="0.0.1")
+
+part_management_service = PartManagementService()
+partner_management_service = PartnerManagementService()
+
+@app.get("/part-management/catalog-part/{manufacturer_id}/{manufacturer_part_id}", response_model=CatalogPartRead)
+async def part_management_get_catalog_part(manufacturer_id: str, manufacturer_part_id: str) -> Optional[CatalogPartRead]:
+    return part_management_service.get_catalog_part(manufacturer_id, manufacturer_part_id)
+
+@app.get("/part-management/catalog-part", response_model=List[CatalogPartRead])
+async def part_management_get_catalog_parts() -> List[CatalogPartRead]:
+    return part_management_service.get_catalog_parts()
+
+@app.post("/part-management/catalog-part/{manufacturer_id}/{manufacturer_part_id}", response_model=CatalogPartRead)
+async def part_management_create_catalog_part(manufacturer_id: str, manufacturer_part_id: str, customer_parts: Optional[List[PartnerCatalogPartBase]]) -> CatalogPartRead:
+    return part_management_service.create_catalog_part_by_ids(manufacturer_id, manufacturer_part_id, customer_parts)
+
+@app.get("/partner-management/business-partner", response_model=List[BusinessPartnerRead])
+async def partner_management_get_business_partners() -> List[BusinessPartnerRead]:
+    return partner_management_service.list_business_partners()
+
+@app.get("/partner-management/business-partner/{business_partner_name}", response_model=Optional[BusinessPartnerRead])
+async def partner_management_get_business_partner(business_partner_name: str) -> Optional[BusinessPartnerRead]:
+    return partner_management_service.get_business_partner(business_partner_name)
+
+@app.post("/partner-management/business-partner/{business_partner_name}", response_model=BusinessPartnerRead)
+async def partner_management_create_business_partner(business_partner_name: str, create_input: BusinessPartnerCreateInput) -> BusinessPartnerRead:
+    return partner_management_service.create_business_partner(BusinessPartnerCreate(
+        name=business_partner_name,
+        bpnl=create_input.bpnl
+    ))
+
+@app.get("/partner-management/business-partner/{business_partner_name}/data-exchange-agreement", response_model=List[DataExchangeAgreementRead])
+async def partner_management_get_data_exchange_agreements(business_partner_name: str) -> List[DataExchangeAgreementRead]:
+    return partner_management_service.get_data_exchange_agreements(business_partner_name)
diff --git a/ichub-backend/managers/metadata_database/repositories.py b/ichub-backend/managers/metadata_database/repositories.py
index 35dc87295..3d90166d4 100644
--- a/ichub-backend/managers/metadata_database/repositories.py
+++ b/ichub-backend/managers/metadata_database/repositories.py
@@ -110,17 +110,18 @@ def get_by_legal_entity_id_manufacturer_part_id(self, legal_entity_id: int, manu
         return self._session.scalars(stmt).first()
 
     def find_by_manufacturer_id_manufacturer_part_id(self, manufacturer_id: Optional[str], manufacturer_part_id: Optional[str], join_partner_catalog_parts : bool = False) -> List[CatalogPart]:
-        stmt = select(CatalogPart)
-
-        if join_partner_catalog_parts:
-            stmt = stmt.join(PartnerCatalogPart, CatalogPart.id == PartnerCatalogPart.catalog_part_id).join(BusinessPartner, BusinessPartner.id == PartnerCatalogPart.business_partner_id)
+        stmt = select(CatalogPart).distinct()
 
         if manufacturer_id:
             stmt = stmt.join(LegalEntity, LegalEntity.id == CatalogPart.legal_entity_id).where(LegalEntity.bpnl == manufacturer_id)
 
         if manufacturer_part_id:
             stmt = stmt.where(CatalogPart.manufacturer_part_id == manufacturer_part_id)
-        
+
+        if join_partner_catalog_parts:
+            subquery = select(PartnerCatalogPart).join(BusinessPartner, BusinessPartner.id == PartnerCatalogPart.business_partner_id).where(PartnerCatalogPart.catalog_part_id == CatalogPart.id).subquery()
+            stmt = stmt.join(subquery, subquery.c.catalog_part_id == CatalogPart.id)
+
         return self._session.exec(stmt).all()
 
 class DataExchangeAgreementRepository(BaseRepository[DataExchangeAgreement]):
diff --git a/ichub-backend/models/services/__init__.py b/ichub-backend/models/services/__init__.py
index 781a0126e..d8ec47f02 100644
--- a/ichub-backend/models/services/__init__.py
+++ b/ichub-backend/models/services/__init__.py
@@ -44,7 +44,7 @@
 )
 
 from .partner_management import (
-    BusinessPartner,
+    BusinessPartnerRead,
     DataExchangeContractRead,
     DataExchangeContractCreate,
     DataExchangeAgreementCreate,
diff --git a/ichub-backend/models/services/part_management.py b/ichub-backend/models/services/part_management.py
index ee7b85aad..48f37ae0c 100644
--- a/ichub-backend/models/services/part_management.py
+++ b/ichub-backend/models/services/part_management.py
@@ -27,7 +27,7 @@
 
 from pydantic import BaseModel, Field
 
-from models.services.partner_management import BusinessPartner
+from models.services.partner_management import BusinessPartnerRead
 
 class CatalogPartBase(BaseModel):
     manufacturer_id: str = Field(alias="manufacturerId", description="The BPNL (manufactuer ID) of the part to register.")
@@ -38,7 +38,7 @@ class PartnerCatalogPartBase(BaseModel):
     business_partner_name: str = Field(alias="businessPartnerName", description="The unique name of the business partner to map the catalog part to.")
 
 class CatalogPartRead(CatalogPartBase):
-    customer_part_ids: Optional[Dict[str, BusinessPartner]] = Field(alias="customerPartIds", description="The list of customer part IDs mapped to the respective Business Partners.", default={})
+    customer_part_ids: Optional[Dict[str, BusinessPartnerRead]] = Field(alias="customerPartIds", description="The list of customer part IDs mapped to the respective Business Partners.", default={})
 
 class CatalogPartCreate(CatalogPartBase):
     customer_part_ids: Optional[List[PartnerCatalogPartBase]] = Field(alias="customerPartIds", description="An optional list of customer part IDs to business partner name mappings.", default=[])
@@ -77,7 +77,7 @@ class SerializedPartBase(CatalogPartBase):
 
 class SerializedPartRead(SerializedPartBase):
     van: Optional[str] = Field(description="The optional VAN (Vehicle Assembly Number) of the serialized part.", default=None)
-    business_partner: BusinessPartner = Field(alias="businessPartner", description="The business partner to whom the part is being offered.")
+    business_partner: BusinessPartnerRead = Field(alias="businessPartner", description="The business partner to whom the part is being offered.")
 
 class SerializedPartCreate(SerializedPartBase):
     van: Optional[str] = Field(description="The optional VAN (Vehicle Assembly Number) of the serialized part.", default=None)
@@ -97,7 +97,7 @@ class JISPartBase(CatalogPartBase):
 class JISPartRead(JISPartBase):
     parent_order_number: Optional[str] = Field(alias="parentOrderNumber", description="The parent order number of the JIS part.", default=None)
     jis_call_date: Optional[datetime] = Field(alias="jisCallDate", description="The JIS call date of the JIS part.", default=None)
-    business_partner: BusinessPartner = Field(alias="businessPartner", description="The business partner to whom the part is being offered.")
+    business_partner: BusinessPartnerRead = Field(alias="businessPartner", description="The business partner to whom the part is being offered.")
 
 class JISPartCreate(JISPartBase):
     parent_order_number: Optional[str] = Field(description="The parent order number of the JIS part.", default=None)
diff --git a/ichub-backend/models/services/partner_management.py b/ichub-backend/models/services/partner_management.py
index b7e484002..7f26ca2b1 100644
--- a/ichub-backend/models/services/partner_management.py
+++ b/ichub-backend/models/services/partner_management.py
@@ -29,23 +29,30 @@ def validate_bpnl(bpnl: str, field_name: str) -> str:
     """Validates the BPNL format."""
     if len(bpnl) != 16 or not bpnl.isalnum():
         raise ValueError(f"{field_name} must be exactly 16 alphanumeric characters.")
-    if bpnl[0:3] != "BPNL":
+    if bpnl[0:4] != "BPNL":
         raise ValueError(f"{field_name} must start with 'BPNL'.")
     
     return bpnl
 
-class BusinessPartner(BaseModel):
+class BusinessPartnerRead(BaseModel):
     """Represents a partner with a validated name field."""
     
     name: str = Field(description="The unique name of the business partner.")
     bpnl: str = Field(description="The Catena-X Business Partner Number (BPNL) of the business partner.")
 
+class BusinessPartnerCreateInput(BaseModel):
+    bpnl: str = Field(description="The Catena-X Business Partner Number (BPNL) of the business partner.")
+
     @field_validator("bpnl")
     @staticmethod
     def validate_bpnl(bpnl: str) -> str:
         """Validates the BPNL format."""
         return validate_bpnl(bpnl, "bpnl")
-    
+
+
+class BusinessPartnerCreate(BusinessPartnerRead, BusinessPartnerCreateInput):
+    pass
+
 class DataExchangeContractRead(BaseModel):
     """Represents document type specific contract terms belonging to a data exchange agreement."""
 
@@ -55,9 +62,9 @@ class DataExchangeContractRead(BaseModel):
 class DataExchangeAgreementRead(BaseModel):
     """Represents a data exchange agreement between two business partners."""
 
-    business_partner: BusinessPartner = Field(alias="businessPartner", description="The business partner to whom the data exchange agreement applies.")
+    business_partner: BusinessPartnerRead = Field(alias="businessPartner", description="The business partner to whom the data exchange agreement applies.")
     name: str = Field(description="The unique name of the data exchange agreement with the given business partner.")
-    contracts: List[DataExchangeContractRead] = Field(description="The list of data exchange contracts that are part of the agreement.")
+    contracts: List[DataExchangeContractRead] = Field(description="The list of data exchange contracts that are part of the agreement.", default=[])
 
 class DataExchangeAgreementCreate(BaseModel):
     business_partner_name: str = Field(alias="businessPartnerName", description="The unique name of the business partner to whom the data exchange contract applies.")
diff --git a/ichub-backend/runtimes/__init__.py b/ichub-backend/runtimes/__init__.py
new file mode 100644
index 000000000..d47335233
--- /dev/null
+++ b/ichub-backend/runtimes/__init__.py
@@ -0,0 +1,25 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+# Package-level variables
+__author__ = 'Eclipse Tractus-X Contributors'
+__license__ = "Apache License, Version 2.0"
diff --git a/ichub-backend/runtimes/fastapi/__init__.py b/ichub-backend/runtimes/fastapi/__init__.py
new file mode 100644
index 000000000..d47335233
--- /dev/null
+++ b/ichub-backend/runtimes/fastapi/__init__.py
@@ -0,0 +1,25 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+# Package-level variables
+__author__ = 'Eclipse Tractus-X Contributors'
+__license__ = "Apache License, Version 2.0"
diff --git a/ichub-backend/runtimes/fastapi/main.py b/ichub-backend/runtimes/fastapi/main.py
new file mode 100644
index 000000000..3eb26b509
--- /dev/null
+++ b/ichub-backend/runtimes/fastapi/main.py
@@ -0,0 +1,28 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+import uvicorn
+from controllers.fastapi import app as api
+
+app = api
+
+if __name__ == "__main__":
+    uvicorn.run("runtimes.fastapi.main:api", reload=True)
\ No newline at end of file
diff --git a/ichub-backend/services/part_management_service.py b/ichub-backend/services/part_management_service.py
new file mode 100644
index 000000000..a64ac945f
--- /dev/null
+++ b/ichub-backend/services/part_management_service.py
@@ -0,0 +1,272 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 DRÄXLMAIER Group
+# (represented by Lisa Dräxlmaier GmbH)
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+from typing import Dict, List, Optional
+from models.services.part_management import BatchCreate, BatchRead, CatalogPartCreate, CatalogPartDelete, CatalogPartRead, JISPartCreate, JISPartDelete, JISPartRead, PartnerCatalogPartBase, PartnerCatalogPartCreate, PartnerCatalogPartDelete, SerializedPartCreate, SerializedPartDelete, SerializedPartRead
+from models.services.partner_management import BusinessPartnerRead
+from managers.metadata_database.repositories import CatalogPartRepository, BusinessPartnerRepository, LegalEntityRepository, PartnerCatalogPartRepository
+from managers.metadata_database.manager import RepositoryManager, RepositoryManagerFactory
+from models.metadata_database.models import CatalogPart, Batch, SerializedPart, JISPart, PartnerCatalogPart
+
+class PartManagementService():
+    """
+    Service class for managing parts and their relationships in the system.
+    """
+
+    def __init__(self, ):
+        self.repositories = RepositoryManagerFactory.create()
+
+    def create_catalog_part(self, catalog_part_create: CatalogPartCreate) -> CatalogPartRead:
+        """
+        Create a new catalog part in the system.
+        Optionally also create attached partner catalog parts - i.e. partner specific mappings of the catalog part.
+        """
+        with self.repositories as repos:
+            
+            # First check if the legal entity exists for the given manufacturer ID
+            db_legal_entity = repos.legal_entity_repository.get_by_bpnl(catalog_part_create.manufacturer_id)
+            if not db_legal_entity:
+                raise ValueError(f"Legal Entity with manufacturer BPNL '{catalog_part_create.manufacturer_id}' does not exist. Please create it first.")
+
+            # Check if the catalog part already exists
+            db_catalog_part = repos.catalog_part_repository.get_by_legal_entity_id_manufacturer_part_id(
+                db_legal_entity.id, catalog_part_create.manufacturer_part_id
+            )
+            if db_catalog_part:
+                raise ValueError("Catalog part already exists.")
+            else:
+                # Create the catalog part in the metadata database
+                db_catalog_part = CatalogPart(
+                    manufacturer_part_id=catalog_part_create.manufacturer_part_id,
+                    legal_entity=db_legal_entity
+                )
+                repos.catalog_part_repository.create(db_catalog_part)
+
+            # Prepare the result object
+            result = CatalogPartRead(
+                manufacturerId=catalog_part_create.manufacturer_id,
+                manufacturerPartId=catalog_part_create.manufacturer_part_id)
+
+            # Check if we already should create some customer part IDs for the given catalog part
+            if catalog_part_create.customer_part_ids:
+                for partner_catalog_part_create in catalog_part_create.customer_part_ids:
+                    
+                    # We need both the customer part ID and the name of the business partner
+                    if not partner_catalog_part_create.customer_part_id:
+                        raise ValueError("Customer part ID is required for a customer part mapping.")
+                    
+                    if not partner_catalog_part_create.business_partner_name:
+                        raise ValueError("Business partner name is required for a customer part mapping.")
+                    
+                    # Resolve the business partner by name from the metadata database
+                    db_business_partner = repos.business_partner_repository.get_by_name(partner_catalog_part_create.business_partner_name)
+                    if not db_business_partner:
+                        raise ValueError(f"Business partner '{partner_catalog_part_create.business_partner_name}' does not exist. Please create it first.")
+
+                    # Create the partner catalog part entry in the metadata database
+                    repos.partner_catalog_part_repository.create(PartnerCatalogPart(
+                        business_partner_id=db_business_partner.id,
+                        customer_part_id=partner_catalog_part_create.customer_part_id,
+                        catalog_part_id=db_catalog_part.id
+                    ))
+                    # TODO: error handling (issue: if one customer part ID fails, all should fail???)
+
+                    result.customer_part_ids[partner_catalog_part_create.customer_part_id] = BusinessPartnerRead(name = db_business_partner.name, bpnl = db_business_partner.bpnl)  
+
+            return result
+
+    def create_catalog_part_by_ids(self, manufacturer_id: str, manufacturer_part_id: str, customer_parts: Optional[List[PartnerCatalogPartBase]]) -> CatalogPartRead:
+        """Convenience method to create a catalog part by its IDs."""
+
+        partner_catalog_parts = []
+        for partner_catalog_part in customer_parts:
+            partner_catalog_part = PartnerCatalogPartBase(
+                customerPartId=partner_catalog_part.customer_part_id,
+                businessPartnerName=partner_catalog_part.business_partner_name
+            )
+            partner_catalog_parts.append(partner_catalog_part)
+
+        catalog_part_create = CatalogPartCreate(
+            manufacturerId=manufacturer_id,
+            manufacturerPartId=manufacturer_part_id,
+            customerPartIds=partner_catalog_parts
+        )
+
+        return self.create_catalog_part(catalog_part_create)
+
+
+    def delete_catalog_part(self, catlog_part: CatalogPartDelete) -> None:
+        """
+        Delete a catalog part from the system.
+        """
+        # Logic to delete a catalog part
+        pass
+
+    def get_catalog_parts(self, manufacturer_id: Optional[str] = None, manufacturer_part_id: Optional[str] = None) -> List[CatalogPartRead]:
+        with self.repositories as repos:
+            result = []
+            
+            db_catalog_parts: List[CatalogPart] = repos.catalog_part_repository.find_by_manufacturer_id_manufacturer_part_id(
+                manufacturer_id, manufacturer_part_id, join_partner_catalog_parts=True
+            )
+            
+            if db_catalog_parts:
+                for db_catalog_part in db_catalog_parts:
+                    customer_parts: Dict[str, BusinessPartnerRead] = {}
+
+                    if db_catalog_part.partner_catalog_parts:
+                        for db_partner_catalog_part in db_catalog_part.partner_catalog_parts:
+                            customer_parts[db_partner_catalog_part.customer_part_id] = BusinessPartnerRead(
+                                name=db_partner_catalog_part.business_partner.name,
+                                bpnl=db_partner_catalog_part.business_partner.bpnl
+                            )
+
+                    result.append(
+                        CatalogPartRead(
+                            manufacturerId=db_catalog_part.legal_entity.bpnl,
+                            manufacturerPartId=db_catalog_part.manufacturer_part_id,
+                            customerPartIds=customer_parts
+                        )
+                    )
+            
+            return result
+
+    def get_catalog_part(self, manufacturer_id: str, manufacturer_part_id: str) -> Optional[CatalogPartRead]:
+        """
+        Retrieve a catalog part from the system.
+        """
+
+        part_list = self.get_catalog_parts(manufacturer_id, manufacturer_part_id)
+        return part_list[0] if part_list else None
+
+    def create_batch(self, batch_create: BatchCreate) -> BatchRead:
+        """
+        Create a new batch in the system.
+        """
+        
+        # Logic to create a batch
+        pass
+
+    def delete_batch(self, batch: BatchRead) -> None:
+        """
+        Delete a batch from the system.
+        """
+        
+        # Logic to delete a batch
+        pass
+
+    def get_batch(self, manufacturer_id: str, manufacturer_part_id: str, batch_id: str) -> BatchRead:
+        """
+        Retrieve a batch from the system.
+        """
+        
+        # Logic to retrieve a batch
+        pass
+
+    def get_batches(self, manufacturer_id: str = None, manufacturer_part_id = None, batch_id: str = None) -> List[BatchRead]:
+        """
+        Retrieves batches from the system according to given parameters.
+        """
+
+        pass
+
+    def create_serialized_part(self, serialized_part_create: SerializedPartCreate) -> SerializedPartRead:
+        """
+        Create a new serialized part in the system.
+        """
+        
+        # Logic to create a serialized part
+        pass
+
+    def delete_serialized_part(self, serialized_part: SerializedPartDelete) -> None:
+        """
+        Delete a serialized part from the system.
+        """
+        
+        # Logic to delete a serialized part
+        pass
+
+    def get_serialized_part(self, manufacturer_id: str, manufacturer_part_id: str, part_instance_id: str) -> SerializedPartRead:
+        """
+        Retrieve a serialized part from the system.
+        """
+        
+        # Logic to retrieve a serialized part
+        pass
+
+    def get_serialized_parts(self, manufacturer_id: str = None, manufacturer_part_id: str = None, part_instance_id: str = None) -> List[SerializedPartRead]:
+        """
+        Retrieves serialized parts from the system according to given parameters.
+        """
+        
+        # Logic to retrieve all serialized parts
+        pass
+
+    def create_jis_part(self, jis_part_create: JISPartCreate) -> JISPartRead:
+        """
+        Create a new JIS part in the system.
+        """
+        
+        # Logic to create a JIS part
+        pass
+
+    def delete_jis_part(self, jis_part: JISPartDelete) -> None:
+        """
+        Delete a JIS part from the system.
+        """
+        
+        # Logic to delete a JIS part
+        pass
+
+    def get_jis_part(self, manufacturer_id: str, manufacturer_part_id: str, jis_number: str) -> JISPartRead:
+        """
+        Retrieve a JIS part from the system.
+        """
+        
+        # Logic to retrieve a JIS part
+        pass
+
+    def get_jis_parts(self, manufacturer_id: str = None, manufacturer_part_id: str = None, jis_number: str = None) -> List[JISPartRead]:
+        """
+        Retrieves JIS parts from the system according to given parameters.
+        """
+        
+        # Logic to retrieve all JIS parts
+        pass
+
+    def create_partner_catalog_part_mapping(self, partner_catalog_part_create: PartnerCatalogPartCreate) -> CatalogPartRead:
+        """
+        Create a new partner catalog part in the system.
+        """
+        
+        # Logic to create a partner catalog part
+        pass
+
+    def delete_partner_catalog_part_mapping(self, partner_catalog_part: PartnerCatalogPartDelete) -> CatalogPartRead:
+        """
+        Delete a partner catalog part from the system.
+        """
+        
+        # Logic to delete a partner catalog part
+        pass
\ No newline at end of file
diff --git a/ichub-backend/services/partner_management_service.py b/ichub-backend/services/partner_management_service.py
new file mode 100644
index 000000000..d13c76863
--- /dev/null
+++ b/ichub-backend/services/partner_management_service.py
@@ -0,0 +1,102 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 DRÄXLMAIER Group
+# (represented by Lisa Dräxlmaier GmbH)
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+from typing import List, Optional
+
+from models.services.partner_management import BusinessPartnerCreate, BusinessPartnerRead, DataExchangeAgreementRead
+from models.metadata_database.models import BusinessPartner, DataExchangeAgreement
+from managers.metadata_database.manager import RepositoryManagerFactory
+
+class PartnerManagementService():
+    """
+    Service class for managing partners and exchange agreements.
+    """
+
+    def __init__(self, ):
+        self.repositories = RepositoryManagerFactory.create()
+
+    def create_business_partner(self, partner_create: BusinessPartnerCreate) -> BusinessPartnerRead:
+        """
+        Create a new partner in the system.
+        """
+        with self.repositories as repo:
+            
+            # First create the business partner entity
+            db_partner = repo.business_partner_repository.create(BusinessPartner(
+                name=partner_create.name,
+                bpnl=partner_create.bpnl
+            ))
+
+            # Needed to get the generated ID from the database into the entity
+            repo.commit()
+            repo.refresh(db_partner)
+
+            # Always create a default data exchange agreement for the partner
+            # (TODO: to be replaced by an explicit API call in a later version)
+            repo.data_exchange_agreement_repository.create(
+                DataExchangeAgreement(
+                    business_partner_id=db_partner.id,
+                    name='Default'
+                ))
+            
+            return BusinessPartnerRead(name=db_partner.name, bpnl=db_partner.bpnl)
+
+    def get_business_partner(self, partner_name: str) -> Optional[BusinessPartnerRead]:
+        """
+        Retrieve a partner by its ID.
+        """
+        
+        with self.repositories as repo:
+            db_partner = repo.business_partner_repository.get_by_name(partner_name)
+            return BusinessPartnerRead(name=db_partner.name, bpnl=db_partner.bpnl) if db_partner else None
+
+
+    def delete_business_partner(self, partner_name: str) -> bool:
+        """
+        Delete a partner from the system.
+        """
+        # Logic to delete a partner
+        pass
+
+    def list_business_partners(self) -> List[BusinessPartnerRead]:
+        """
+        List all partners in the system.
+        """
+        with self.repositories as repo:
+            db_partners = repo.business_partner_repository.find_all()
+            return [BusinessPartnerRead(name=bp.name, bpnl=bp.bpnl) for bp in db_partners]
+        
+    def get_data_exchange_agreements(self, partner_name: str) -> List[DataExchangeAgreementRead]:
+        """
+        List all data exchange agreements for a given partner.
+        """
+        with self.repositories as repo:
+            db_partner = repo.business_partner_repository.get_by_name(partner_name)
+            if not db_partner:
+                return []
+            
+            db_agreements = repo.data_exchange_agreement_repository.get_by_business_partner_id(db_partner.id)
+            return [DataExchangeAgreementRead(
+                businessPartner=BusinessPartnerRead(name=db_partner.name, bpnl=db_partner.bpnl),
+                name=agreement.name) for agreement in db_agreements]
diff --git a/ichub-backend/services/twin_management_service.py b/ichub-backend/services/twin_management_service.py
new file mode 100644
index 000000000..81fa12f77
--- /dev/null
+++ b/ichub-backend/services/twin_management_service.py
@@ -0,0 +1,127 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 DRÄXLMAIER Group
+# (represented by Lisa Dräxlmaier GmbH)
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+from uuid import UUID
+
+from managers.metadata_database.manager import RepositoryManagerFactory
+from models.services.twin_management import CatalogPartTwinCreate, TwinRead, TwinDetailsRead, TwinAspectCreate, TwinAspectRead
+
+
+class TwinManagementService:
+    """
+    Service class for managing twin-related operations (CRUD and Twin sharing).
+    """
+
+    def __init__(self, ):
+        self.repositories = RepositoryManagerFactory.create()
+
+    def create_catalog_part_twin(self, twin_catalog_part_create: CatalogPartTwinCreate) -> TwinRead:
+        # Step 1: Retrieve the catalog part entity according to the catalog part data (manufacturer_id, manufacturer_part_id)
+        # (if not there => raise error)
+
+        # Step 2 (future): Retrieve the enablement service stack entity from the DB according to the given name
+        # (for the moment: we have a singleton entity for that - e.g. with id 1)
+        # (if not there => raise error)
+
+        # Step 3: Check the twin_id field of that entity if a twin is already registered
+
+        # Step 4: If no twin was there, create it now in the DB (generating on demand a new global_id and dtr_aas_id)
+
+        # Step 5: Try to find the twin registration for the twin id and enablement service stack id
+        # (if not there => create it now, setting the dtr_registered flag to False)
+    
+        # Step 6: Check the dtr_registered flag on the twin registration entity
+        # (if True => we can skip the operation from here on => nothing to do)
+        # (if False => we need to register the twin in the DTR using the industry core SDK, then
+        #  update the twin registration entity with the dtr_registered flag to True)
+
+        pass
+
+    def create_catalog_part_twin_share(self, global_id: UUID, business_partner_name: str) -> TwinRead:
+        # Step 1: Retrieve the twin entity according to the global_id
+        # (if not there => raise error)
+        # (as an alternative we could also call create_catalog_part_twin() here to create a twin accordingly;
+        #  remark: this will not return the primary key of the twin entity; maybe we need to move the logic
+        #  to an internal helper function called by both this function and create_catalog_part_twin())
+
+        # Step 2: Retrieve the catalog part entity according to the id of the twin entity
+        # (if not there => raise error)
+
+        # Step 3: Retrieve the business partner entity according to the business_partner_name
+        # (if not there => raise error)
+
+        # Step 4: Retrieve the data exchange agreement entity for the business partner (with name 'default')
+        # (the 'default' agreement will later be replaced with an explicit mechanism)
+        # (if not there => raise error)
+
+        # Step 5: Check if there is already a twin exchange entity for the twin and data exchange agreement
+        # (if there => we can skip the operation from here on => nothing to do)
+
+        # Step 6: Find the partner catalog part entity for the given catalog part and business partner
+        # (if there => fine)
+        # (if not there => we normally now would need a customer part ID - either we take it as optional arg
+        #  and raise now an error when not there or we create an artificial one; but both need to be reworked
+        #  in a later release; finally create the partner catalog part entity)
+
+        # Step 7: Create the twin exchange entity for the twin and data exchange agreement
+
+        # Step 8: Create the shell descriptor in the DTR via the industry core SDK
+        # Step 8a: if twin was existing before => add access to the twin for the new business partner
+        # (this inculudes: register all specified specific asset IDs visible for partners - e.g. customer part ID - not sure about
+        #  manufacturer)
+        pass
+
+    def create_twin_aspect(self, twin_aspect_create: TwinAspectCreate) -> TwinAspectRead:
+        """
+        Create a new twin aspect for a give twin.
+        """
+        # Step 1: Retrieve the twin entity according to the global_id
+        # (if not there => raise error)
+
+        # Step 2: Retrieve a potentially existing twin aspect entity for the given twin_id and semantic_id
+        # (if there => for the moment we could raise an error; but in the future I recommend
+        #  the API to be "repeatable" - e.g. to update the payload in the submodel service)
+
+        # Step 3: Create the twin aspect entity in the database
+        # (generate a new submodel_id for it if not given)
+        # Step 3a: Create a twin aspect registration entry pointing to the singleton enablement service stack
+        # (set the status to PLANNED and the mode to SINGLE)
+
+        # Step 4: Upload the payload to the submodel service
+        # (use industry core SDK or it's wrapper for that)
+        # Step 4a: Update the twin aspect registration entry with the status to STORED
+
+        # Step 5: (later to be implemented): Register the aspect as asset in the EDC (if necessary)
+        # (use industry core SDK or it's wrapper for that)
+        # Step 5a: Update the twin aspect registration entry with the status to EDC_REGISTERED
+
+        # Step 6: Attach a submodel descriptor to the shell descriptor in the DTR
+        # (use industry core SDK or it's wrapper for that)
+        # Step 6a: Update the twin aspect registration entry with the status to DTR_REGISTERED
+        # (tricky: use the right asset ID for the EDC and the right URLs for data plane and control plane)
+
+        pass
+
+    def get_twin_details(self, global_id: UUID) -> TwinDetailsRead:
+        pass
\ No newline at end of file


Reviews:
user: login: CDiezRodriguez
body: Amazing work @sm29105!

I've suggested changes to some methods that I believe need adjustments.

Also, besides the unimplemented methods, I think the exception handler is still missing in the implemented ones. Is that correct?

Could you also sync with the main branch?
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T10:15:23Z
user: login: sm29105
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T13:10:00Z
user: login: sm29105
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T13:10:08Z
user: login: CDiezRodriguez
body: I am testing locally with the following command:
```
python3.12 -m runtimes.fastapi.main --host 0.0.0.0 --port 8000
```
To make it work correctly, I needed to change the following things.

If you agree, I can go ahead and merge it afterward.
state: CHANGES_REQUESTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T13:22:44Z
user: login: CDiezRodriguez
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T13:45:31Z
user: login: gomezbc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T14:32:35Z
user: login: gomezbc
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-05-02T14:33:37Z
user: login: matbmoser
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-05-05T06:45:24Z

Review comments:
diff_hunk: @@ -0,0 +1,86 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 DRÄXLMAIER Group
+# (represented by Lisa Dräxlmaier GmbH)
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+from typing import List, Optional
+
+from models.services.partner_management import BusinessPartnerCreate, BusinessPartnerRead
+from models.metadata_database.models import BusinessPartner, DataExchangeAgreement
+from managers.metadata_database.manager import RepositoryManagerFactory
+
+class PartnerManagementService():
+    """
+    Service class for managing partners and exchange agreements.
+    """
+
+    def __init__(self, ):
+        self.repositories = RepositoryManagerFactory.create()
+
+    def create_partner(self, partner_create: BusinessPartnerCreate) -> BusinessPartnerRead:
+        """
+        Create a new partner in the system.
+        """
+        with self.repositories as repo:
+            
+            # First create the business partner entity
+            db_partner = repo.business_partner_repository.create(BusinessPartner(
+                name=partner_create,
+                bpnl=partner_create.bpnl
+            ))
+
+            # Needed to get the generated ID from the database into the entity
+            repo.commit()
+            repo.refresh(db_partner)
+
+            # Always create a default data exchange agreement for the partner
+            # (TODO: to be replaced by an explicit API call in a later version)
+            repo.data_exchange_agreement_repository.create(
+                DataExchangeAgreement(
+                    business_partner=db_partner,
+                    name='Default'
+                ))
path: ichub-backend/services/partner_management_service.py
user: login: CDiezRodriguez
body: ```suggestion
                ))
            
            return BusinessPartnerRead(db_partner.name, db_partner.bpnl)
```
created_at: 2025-05-02T09:27:43Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,86 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 DRÄXLMAIER Group
+# (represented by Lisa Dräxlmaier GmbH)
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+from typing import List, Optional
+
+from models.services.partner_management import BusinessPartnerCreate, BusinessPartnerRead
+from models.metadata_database.models import BusinessPartner, DataExchangeAgreement
+from managers.metadata_database.manager import RepositoryManagerFactory
+
+class PartnerManagementService():
+    """
+    Service class for managing partners and exchange agreements.
+    """
+
+    def __init__(self, ):
+        self.repositories = RepositoryManagerFactory.create()
+
+    def create_partner(self, partner_create: BusinessPartnerCreate) -> BusinessPartnerRead:
+        """
+        Create a new partner in the system.
+        """
+        with self.repositories as repo:
+            
+            # First create the business partner entity
+            db_partner = repo.business_partner_repository.create(BusinessPartner(
+                name=partner_create,
+                bpnl=partner_create.bpnl
+            ))
+
+            # Needed to get the generated ID from the database into the entity
+            repo.commit()
+            repo.refresh(db_partner)
+
+            # Always create a default data exchange agreement for the partner
+            # (TODO: to be replaced by an explicit API call in a later version)
+            repo.data_exchange_agreement_repository.create(
+                DataExchangeAgreement(
+                    business_partner=db_partner,
+                    name='Default'
+                ))
+
+    def get_partner(self, partner_name: str) -> BusinessPartnerRead:
path: ichub-backend/services/partner_management_service.py
user: login: CDiezRodriguez
body: ```suggestion
    def get_partner(self, partner_name: str) -> Optional[BusinessPartnerRead]:
```
created_at: 2025-05-02T09:34:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+# Package-level variables
+__author__ = 'Eclipse Tractus-X Contributors'
+__license__ = "Apache License, Version 2.0"
path: ichub-backend/controllers/__init__.py
user: login: CDiezRodriguez
body: ```suggestion
__license__ = "Apache License, Version 2.0"

from .app import app

```
created_at: 2025-05-02T13:06:12Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,28 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+import uvicorn
+from controllers.fastapi import app as api
+
+app = api.app
path: ichub-backend/runtimes/fastapi/main.py
user: login: CDiezRodriguez
body: ```suggestion
app = api
```
It works on my local machine if I remove the .app attribute.
created_at: 2025-05-02T13:08:54Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,86 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 DRÄXLMAIER Group
+# (represented by Lisa Dräxlmaier GmbH)
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+from typing import List, Optional
+
+from models.services.partner_management import BusinessPartnerCreate, BusinessPartnerRead
+from models.metadata_database.models import BusinessPartner, DataExchangeAgreement
+from managers.metadata_database.manager import RepositoryManagerFactory
+
+class PartnerManagementService():
+    """
+    Service class for managing partners and exchange agreements.
+    """
+
+    def __init__(self, ):
+        self.repositories = RepositoryManagerFactory.create()
+
+    def create_partner(self, partner_create: BusinessPartnerCreate) -> BusinessPartnerRead:
+        """
+        Create a new partner in the system.
+        """
+        with self.repositories as repo:
+            
+            # First create the business partner entity
+            db_partner = repo.business_partner_repository.create(BusinessPartner(
+                name=partner_create,
+                bpnl=partner_create.bpnl
+            ))
+
+            # Needed to get the generated ID from the database into the entity
+            repo.commit()
+            repo.refresh(db_partner)
+
+            # Always create a default data exchange agreement for the partner
+            # (TODO: to be replaced by an explicit API call in a later version)
+            repo.data_exchange_agreement_repository.create(
+                DataExchangeAgreement(
+                    business_partner=db_partner,
+                    name='Default'
+                ))
path: ichub-backend/services/partner_management_service.py
user: login: sm29105
body: Fixed now
created_at: 2025-05-02T13:10:00Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,86 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 DRÄXLMAIER Group
+# (represented by Lisa Dräxlmaier GmbH)
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+from typing import List, Optional
+
+from models.services.partner_management import BusinessPartnerCreate, BusinessPartnerRead
+from models.metadata_database.models import BusinessPartner, DataExchangeAgreement
+from managers.metadata_database.manager import RepositoryManagerFactory
+
+class PartnerManagementService():
+    """
+    Service class for managing partners and exchange agreements.
+    """
+
+    def __init__(self, ):
+        self.repositories = RepositoryManagerFactory.create()
+
+    def create_partner(self, partner_create: BusinessPartnerCreate) -> BusinessPartnerRead:
+        """
+        Create a new partner in the system.
+        """
+        with self.repositories as repo:
+            
+            # First create the business partner entity
+            db_partner = repo.business_partner_repository.create(BusinessPartner(
+                name=partner_create,
+                bpnl=partner_create.bpnl
+            ))
+
+            # Needed to get the generated ID from the database into the entity
+            repo.commit()
+            repo.refresh(db_partner)
+
+            # Always create a default data exchange agreement for the partner
+            # (TODO: to be replaced by an explicit API call in a later version)
+            repo.data_exchange_agreement_repository.create(
+                DataExchangeAgreement(
+                    business_partner=db_partner,
+                    name='Default'
+                ))
+
+    def get_partner(self, partner_name: str) -> BusinessPartnerRead:
path: ichub-backend/services/partner_management_service.py
user: login: sm29105
body: Fixed now
created_at: 2025-05-02T13:10:08Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,25 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+# Package-level variables
+__author__ = 'Eclipse Tractus-X Contributors'
+__license__ = "Apache License, Version 2.0"
path: ichub-backend/controllers/__init__.py
user: login: CDiezRodriguez
body: Sorry @sm29105, my mistake. I confused the level, and this request needs to be inside the `fastapi` folder, in the `__init__.py` file.
Alternatively, you can change it to `from .fastapi.app import app` for it to work correctly.
created_at: 2025-05-02T13:45:31Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 DRÄXLMAIER Group
+# (represented by Lisa Dräxlmaier GmbH)
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+from typing import List, Optional
+
+from fastapi import FastAPI
+
+from services.part_management_service import PartManagementService
+from services.partner_management_service import PartnerManagementService
+from models.services.part_management import CatalogPartRead, PartnerCatalogPartBase
+from models.services.partner_management import BusinessPartnerRead, BusinessPartnerCreate, BusinessPartnerCreateInput, DataExchangeAgreementRead
+
+app = FastAPI(title="Industry Core Hub Backend API", version="0.0.1")
+
+part_management_service = PartManagementService()
+partner_management_service = PartnerManagementService()
+
+@app.get("/part-management/catalog-part/{manufacturer_id}/{manufacturer_part_id}", response_model=CatalogPartRead)
+async def part_management_get_catalog_part(manufacturer_id: str, manufacturer_part_id: str) -> CatalogPartRead:
+    return part_management_service.get_catalog_part(manufacturer_id, manufacturer_part_id)
+
+@app.get("/part-management/catalog-part", response_model=List[CatalogPartRead])
+async def part_management_get_catalog_parts() -> List[CatalogPartRead]:
+    return part_management_service.get_catalog_parts()
+
+@app.post("/part-management/catalog-part/{manufacturer_id}/{manufacturer_part_id}", response_model=CatalogPartRead)
+async def part_management_create_catalog_part(manufacturer_id: str, manufacturer_part_id: str, customer_parts: Optional[List[PartnerCatalogPartBase]]) -> CatalogPartRead:
+    return part_management_service.create_catalog_part_by_ids(manufacturer_id, manufacturer_part_id, customer_parts)
+
+@app.get("/partner-management/business-partner", response_model=List[BusinessPartnerRead])
+async def partner_management_get_business_partners() -> List[BusinessPartnerRead]:
+    return partner_management_service.list_business_partners()
+
+@app.get("/partner-management/business-partner/{business_partner_name}", response_model=Optional[BusinessPartnerRead])
+async def partner_management_get_business_partners(business_partner_name: str) -> Optional[BusinessPartnerRead]:
path: ichub-backend/controllers/fastapi/app.py
user: login: gomezbc
body: ```suggestion
async def partner_management_get_business_partner(business_partner_name: str) -> Optional[BusinessPartnerRead]:
```
we need to rename it otherwise it will overwrite the method above
created_at: 2025-05-02T14:32:34Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,68 @@
+#################################################################################
+# Eclipse Tractus-X - Industry Core Hub Backend
+#
+# Copyright (c) 2025 DRÄXLMAIER Group
+# (represented by Lisa Dräxlmaier GmbH)
+# Copyright (c) 2025 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 govern in permissions and limitations
+# under the License.
+#
+# SPDX-License-Identifier: Apache-2.0
+#################################################################################
+
+from typing import List, Optional
+
+from fastapi import FastAPI
+
+from services.part_management_service import PartManagementService
+from services.partner_management_service import PartnerManagementService
+from models.services.part_management import CatalogPartRead, PartnerCatalogPartBase
+from models.services.partner_management import BusinessPartnerRead, BusinessPartnerCreate, BusinessPartnerCreateInput, DataExchangeAgreementRead
+
+app = FastAPI(title="Industry Core Hub Backend API", version="0.0.1")
+
+part_management_service = PartManagementService()
+partner_management_service = PartnerManagementService()
+
+@app.get("/part-management/catalog-part/{manufacturer_id}/{manufacturer_part_id}", response_model=CatalogPartRead)
+async def part_management_get_catalog_part(manufacturer_id: str, manufacturer_part_id: str) -> CatalogPartRead:
path: ichub-backend/controllers/fastapi/app.py
user: login: gomezbc
body: 
```suggestion
async def part_management_get_catalog_part(manufacturer_id: str, manufacturer_part_id: str) -> Optional[CatalogPartRead]:
```
created_at: 2025-05-02T14:33:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: sm29105
created_at: 2025-05-02T13:10:49Z
body: Main branch was merged - about the exception handler - no idea how to do this - can you do it by yourself after the merge?
author_association: CONTRIBUTOR
user: login: sonarqubecloud[bot]
created_at: 2025-05-05T05:50:32Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=eclipse-tractusx_industry-core-hub&pullRequest=158) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [2 New issues](https://sonarcloud.io/project/issues?id=eclipse-tractusx_industry-core-hub&pullRequest=158&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=eclipse-tractusx_industry-core-hub&pullRequest=158&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=eclipse-tractusx_industry-core-hub&pullRequest=158&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=eclipse-tractusx_industry-core-hub&pullRequest=158&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=eclipse-tractusx_industry-core-hub&pullRequest=158&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=eclipse-tractusx_industry-core-hub&pullRequest=158)


author_association: NONE