diff --git a/README.md b/README.md
index 2056188cc7ff88eb1e9cb0a4cbfe05a65130af32..cb0cac396dac06de905b67f05d408286af9365c1 100644
--- a/README.md
+++ b/README.md
@@ -2,4 +2,4 @@
 The GÉANT interpretation of [`orchestrator-core`](https://github.com/workfloworchestrator/orchestrator-core).
 
 ## Documentation
-You can build the documentation locally using either [build-docs.sh](build-docs.sh) or [test-docs.sh](test-docs.sh).
+You can build the documentation locally using [build-docs.sh](build-docs.sh).
diff --git a/docs/source/glossary.rst b/docs/source/glossary.rst
index bb7a2a69ca2c76900be5d138f6f3db610528538b..b98edde4222ab7f001fba8b36dfcc750d1821a8e 100644
--- a/docs/source/glossary.rst
+++ b/docs/source/glossary.rst
@@ -3,6 +3,9 @@ Glossary of terms
 
 .. glossary::
 
+  API
+    Application Programming Interface
+
   BGP
     Border Gateway Protocol: a path vector routing protocol described in
     `RFC 4271 <https://datatracker.ietf.org/doc/html/rfc4271>`_.
diff --git a/docs/source/module/products/index.rst b/docs/source/module/products/index.rst
index c68072cc071e570f268408b67e656b63a1ed4bf6..415b1c48357cd59d9a59fbd372be22cf11ac7b2c 100644
--- a/docs/source/module/products/index.rst
+++ b/docs/source/module/products/index.rst
@@ -1,6 +1,10 @@
 ``gso.products``
 ================
 
+.. automodule:: gso.products
+   :members:
+   :show-inheritance:
+
 Subpackages
 -----------
 
diff --git a/gso/api/v1/imports.py b/gso/api/v1/imports.py
index bd8c0d5f2cb41b654e198d84f093d8d204714215..51f58c5d62705969f054a2d680aab9e806ff2171 100644
--- a/gso/api/v1/imports.py
+++ b/gso/api/v1/imports.py
@@ -1,17 +1,126 @@
+import ipaddress
 from typing import Any
 from uuid import UUID
 
 from fastapi import Depends, HTTPException, status
 from fastapi.routing import APIRouter
 from orchestrator.security import opa_security_default
-from orchestrator.services import processes, subscriptions
+from orchestrator.services import processes
+from orchestrator.services import subscriptions as wfo_subscriptions
+from pydantic import BaseModel, root_validator, validator
 from sqlalchemy.exc import MultipleResultsFound
 
-from gso.schemas.imports import ImportResponseModel, IptrunkImportModel, RouterImportModel, SiteImportModel
+from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity
+from gso.products.product_blocks.router import RouterRole, RouterVendor
+from gso.products.product_blocks.site import SiteTier
+from gso.services import subscriptions
+from gso.services.crm import CustomerNotFoundError, get_customer_by_name
+from gso.utils.helpers import LAGMember
 
 router = APIRouter(prefix="/imports", tags=["Imports"], dependencies=[Depends(opa_security_default)])
 
 
+class ImportResponseModel(BaseModel):
+    pid: UUID
+    detail: str
+
+
+class SiteImportModel(BaseModel):
+    site_name: str
+    site_city: str
+    site_country: str
+    site_country_code: str
+    site_latitude: float
+    site_longitude: float
+    site_bgp_community_id: int
+    site_internal_id: int
+    site_tier: SiteTier
+    site_ts_address: str
+    customer: str
+
+
+class RouterImportModel(BaseModel):
+    customer: str
+    router_site: str
+    hostname: str
+    ts_port: int
+    router_vendor: RouterVendor
+    router_role: RouterRole
+    is_ias_connected: bool
+    router_lo_ipv4_address: ipaddress.IPv4Address
+    router_lo_ipv6_address: ipaddress.IPv6Address
+    router_lo_iso_address: str
+    router_si_ipv4_network: ipaddress.IPv4Network | None = None
+    router_ias_lt_ipv4_network: ipaddress.IPv4Network | None = None
+    router_ias_lt_ipv6_network: ipaddress.IPv6Network | None = None
+
+
+class IptrunkImportModel(BaseModel):
+    customer: str
+    geant_s_sid: str
+    iptrunk_type: IptrunkType
+    iptrunk_description: str
+    iptrunk_speed: PhyPortCapacity
+    iptrunk_minimum_links: int
+    side_a_node_id: str
+    side_a_ae_iface: str
+    side_a_ae_geant_a_sid: str
+    side_a_ae_members: list[LAGMember]
+    side_b_node_id: str
+    side_b_ae_iface: str
+    side_b_ae_geant_a_sid: str
+    side_b_ae_members: list[LAGMember]
+
+    iptrunk_ipv4_network: ipaddress.IPv4Network
+    iptrunk_ipv6_network: ipaddress.IPv6Network
+
+    @classmethod
+    def _get_active_routers(cls) -> set[str]:
+        return {
+            str(router_id) for router_id in subscriptions.get_active_router_subscriptions(fields=["subscription_id"])
+        }
+
+    @validator("customer")
+    def check_if_customer_exists(cls, value: str) -> str:
+        try:
+            get_customer_by_name(value)
+        except CustomerNotFoundError:
+            raise ValueError(f"Customer {value} not found")
+
+        return value
+
+    @validator("side_a_node_id", "side_b_node_id")
+    def check_if_router_side_is_available(cls, value: str) -> str:
+        if value not in cls._get_active_routers():
+            raise ValueError(f"Router {value} not found")
+
+        return value
+
+    @validator("side_a_ae_members", "side_b_ae_members")
+    def check_side_uniqueness(cls, value: list[str]) -> list[str]:
+        if len(value) != len(set(value)):
+            raise ValueError("Items must be unique")
+
+        return value
+
+    @root_validator
+    def check_members(cls, values: dict[str, Any]) -> dict[str, Any]:
+        min_links = values["iptrunk_minimum_links"]
+        side_a_members = values.get("side_a_ae_members", [])
+        side_b_members = values.get("side_b_ae_members", [])
+
+        len_a = len(side_a_members)
+        len_b = len(side_b_members)
+
+        if len_a < min_links:
+            raise ValueError(f"Side A members should be at least {min_links} (iptrunk_minimum_links)")
+
+        if len_a != len_b:
+            raise ValueError("Mismatch between Side A and B members")
+
+        return values
+
+
 def _start_process(process_name: str, data: dict) -> UUID:
     """Start a process and handle common exceptions."""
 
@@ -42,7 +151,7 @@ def import_site(site: SiteImportModel) -> dict[str, Any]:
     :raises HTTPException: If the site already exists or if there's an error in the process.
     """
     try:
-        subscription = subscriptions.retrieve_subscription_by_subscription_instance_value(
+        subscription = wfo_subscriptions.retrieve_subscription_by_subscription_instance_value(
             resource_type="site_name", value=site.site_name, sub_status=("provisioning", "active")
         )
         if subscription:
diff --git a/gso/migrations/env.py b/gso/migrations/env.py
index 2f41e241668abfc8989a2ff01d91cd5099fb0bc4..a1f9b9fcdbb9088dc846a4273a9a12eff786baf6 100644
--- a/gso/migrations/env.py
+++ b/gso/migrations/env.py
@@ -1,11 +1,11 @@
 import logging
 import os
-from alembic import context
-from sqlalchemy import engine_from_config, pool
 
 import orchestrator
+from alembic import context
 from orchestrator.db.database import BaseModel
 from orchestrator.settings import app_settings
+from sqlalchemy import engine_from_config, pool
 
 # this is the Alembic Config object, which provides
 # access to the values within the .ini file in use.
@@ -61,7 +61,7 @@ def run_migrations_online() -> None:
     # this callback is used to prevent an auto-migration from being generated
     # when there are no changes to the schema
     # reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
-    def process_revision_directives(context, revision, directives):  # type: ignore
+    def process_revision_directives(context, revision, directives):  # type: ignore[no-untyped-def]
         if getattr(config.cmd_opts, "autogenerate", False):
             script = directives[0]
             if script.upgrade_ops.is_empty():
diff --git a/gso/migrations/versions/2023-10-11_394dc60d5c02_modify_ip_trunk_model.py b/gso/migrations/versions/2023-10-11_394dc60d5c02_modify_ip_trunk_model.py
new file mode 100644
index 0000000000000000000000000000000000000000..ce76bb6d3cb8d701bda8d69e0f474a81ec2e288e
--- /dev/null
+++ b/gso/migrations/versions/2023-10-11_394dc60d5c02_modify_ip_trunk_model.py
@@ -0,0 +1,86 @@
+"""Modify IP trunk model.
+
+Revision ID: 394dc60d5c02
+Revises: 01e42c100448
+Create Date: 2023-10-11 17:55:38.289125
+
+"""
+import sqlalchemy as sa
+from alembic import op
+
+# revision identifiers, used by Alembic.
+revision = '394dc60d5c02'
+down_revision = '01e42c100448'
+branch_labels = None
+depends_on = None
+
+
+def upgrade() -> None:
+    conn = op.get_bind()
+    conn.execute(sa.text("""
+DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members_description'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members_description'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values WHERE subscription_instance_values.resource_type_id IN (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members_description', 'iptrunk_side_ae_members'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM resource_types WHERE resource_types.resource_type IN ('iptrunk_side_ae_members_description', 'iptrunk_side_ae_members')
+    """))
+    conn.execute(sa.text("""
+INSERT INTO product_blocks (name, description, tag, status) VALUES ('IptrunkInterfaceBlock', 'Interface in a LAG as part of an IP trunk', 'IPTINT', 'active') RETURNING product_blocks.product_block_id
+    """))
+    conn.execute(sa.text("""
+INSERT INTO resource_types (resource_type, description) VALUES ('interface_description', 'Description of a LAG interface') RETURNING resource_types.resource_type_id
+    """))
+    conn.execute(sa.text("""
+INSERT INTO resource_types (resource_type, description) VALUES ('interface_name', 'Interface name of a LAG member') RETURNING resource_types.resource_type_id
+    """))
+    conn.execute(sa.text("""
+INSERT INTO product_block_relations (in_use_by_id, depends_on_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock')), (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')))
+    """))
+    conn.execute(sa.text("""
+INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_description')))
+    """))
+    conn.execute(sa.text("""
+INSERT INTO product_block_resource_types (product_block_id, resource_type_id) VALUES ((SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')), (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_name')))
+    """))
+
+
+def downgrade() -> None:
+    conn = op.get_bind()
+    conn.execute(sa.text("""
+DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_description'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_description'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM product_block_resource_types WHERE product_block_resource_types.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_name'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values USING product_block_resource_types WHERE subscription_instance_values.subscription_instance_id IN (SELECT subscription_instances.subscription_instance_id FROM subscription_instances WHERE subscription_instances.subscription_instance_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock'))) AND product_block_resource_types.resource_type_id = (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_name'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instance_values WHERE subscription_instance_values.resource_type_id IN (SELECT resource_types.resource_type_id FROM resource_types WHERE resource_types.resource_type IN ('interface_description', 'interface_name'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM resource_types WHERE resource_types.resource_type IN ('interface_description', 'interface_name')
+    """))
+    conn.execute(sa.text("""
+DELETE FROM product_block_relations WHERE product_block_relations.in_use_by_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkSideBlock')) AND product_block_relations.depends_on_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM subscription_instances WHERE subscription_instances.product_block_id IN (SELECT product_blocks.product_block_id FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock'))
+    """))
+    conn.execute(sa.text("""
+DELETE FROM product_blocks WHERE product_blocks.name IN ('IptrunkInterfaceBlock')
+    """))
diff --git a/gso/oss-params-example.json b/gso/oss-params-example.json
index a06212ef7441a31055fdc0b240982aa9c2c00bf5..771492f66dd9555588215cf6b7f88edd51b9a14d 100644
--- a/gso/oss-params-example.json
+++ b/gso/oss-params-example.json
@@ -48,7 +48,6 @@
   "PROVISIONING_PROXY": {
     "scheme": "https",
     "api_base": "localhost:44444",
-    "auth": "Bearer <token>",
     "api_version": 1123
   }
 }
diff --git a/gso/products/__init__.py b/gso/products/__init__.py
index df2529a3ff74a248f29490cd6fa49bbdd318fb65..e6a8c06f8850748b36233be63006a8fa9709d946 100644
--- a/gso/products/__init__.py
+++ b/gso/products/__init__.py
@@ -1,10 +1,23 @@
-"""Module that updates the domain model of :term:`GSO`. Should contain all types of subscriptions."""
+"""Module that updates the domain model of :term:`GSO`. Should contain all types of subscriptions.
+
+.. warning::
+   Whenever a new product type is added, this should be reflected in the :py:class:`gso.products.ProductType`
+   enumerator.
+"""
 from orchestrator.domain import SUBSCRIPTION_MODEL_REGISTRY
+from pydantic_forms.types import strEnum
 
 from gso.products.product_types.iptrunk import Iptrunk
 from gso.products.product_types.router import Router
 from gso.products.product_types.site import Site
 
+
+class ProductType(strEnum):
+    SITE = "Site"
+    ROUTER = "Router"
+    IP_TRUNK = "IP trunk"
+
+
 SUBSCRIPTION_MODEL_REGISTRY.update(
     {
         "Site": Site,
@@ -12,5 +25,3 @@ SUBSCRIPTION_MODEL_REGISTRY.update(
         "IP trunk": Iptrunk,
     }
 )
-
-__all__ = ["Site", "Iptrunk", "Router"]
diff --git a/gso/products/product_blocks/__init__.py b/gso/products/product_blocks/__init__.py
index de5db46c80c80ee31acfdf549159cde5f4e862b2..304dbd923eb76757961ac7e893cf2dbd564e8a28 100644
--- a/gso/products/product_blocks/__init__.py
+++ b/gso/products/product_blocks/__init__.py
@@ -1,18 +1 @@
-"""Product blocks that store information about subscriptions.
-
-In this file, some enumerators may be declared that are available for use across all subscriptions.
-"""
-
-from orchestrator.types import strEnum
-
-
-class PhyPortCapacity(strEnum):
-    """Physical port capacity enumerator.
-
-    An enumerator that has the different possible capacities of ports that are available to use in subscriptions.
-    """
-
-    ONE_GIGABIT_PER_SECOND = "1G"
-    TEN_GIGABIT_PER_SECOND = "10G"
-    HUNDRED_GIGABIT_PER_SECOND = "100G"
-    FOUR_HUNDRED_GIGABIT_PER_SECOND = "400G"
+"""Product blocks that store information about subscriptions."""
diff --git a/gso/products/product_blocks/iptrunk.py b/gso/products/product_blocks/iptrunk.py
index 3eb992d189955e52ea7ee3fce27c55658fd98f1c..7fab56c0d20be1d1adb9e9b65ffebd8049caa1ff 100644
--- a/gso/products/product_blocks/iptrunk.py
+++ b/gso/products/product_blocks/iptrunk.py
@@ -6,11 +6,22 @@ from typing import TypeVar
 from orchestrator.domain.base import ProductBlockModel
 from orchestrator.forms.validators import UniqueConstrainedList
 from orchestrator.types import SubscriptionLifecycle, strEnum
-from pydantic import Field
 
 from gso.products.product_blocks.router import RouterBlock, RouterBlockInactive, RouterBlockProvisioning
 
 
+class PhyPortCapacity(strEnum):
+    """Physical port capacity enumerator.
+
+    An enumerator that has the different possible capacities of ports that are available to use in subscriptions.
+    """
+
+    ONE_GIGABIT_PER_SECOND = "1G"
+    TEN_GIGABIT_PER_SECOND = "10G"
+    HUNDRED_GIGABIT_PER_SECOND = "100G"
+    FOUR_HUNDRED_GIGABIT_PER_SECOND = "400G"
+
+
 class IptrunkType(strEnum):
     DARK_FIBER = "Dark_fiber"
     LEASED = "Leased"
@@ -19,6 +30,28 @@ class IptrunkType(strEnum):
 T = TypeVar("T", covariant=True)
 
 
+class LAGMemberList(UniqueConstrainedList[T]):  # type: ignore[type-var]
+    pass
+
+
+class IptrunkInterfaceBlockInactive(
+    ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="IptrunkInterfaceBlock"
+):
+    #  TODO: add validation for interface names, making the type a constrained string
+    interface_name: str | None = None
+    interface_description: str | None = None
+
+
+class IptrunkInterfaceBlockProvisioning(IptrunkInterfaceBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
+    interface_name: str
+    interface_description: str
+
+
+class IptrunkInterfaceBlock(IptrunkInterfaceBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
+    interface_name: str
+    interface_description: str
+
+
 class IptrunkSides(UniqueConstrainedList[T]):  # type: ignore[type-var]
     min_items = 2
     max_items = 2
@@ -30,24 +63,21 @@ class IptrunkSideBlockInactive(
     iptrunk_side_node: RouterBlockInactive
     iptrunk_side_ae_iface: str | None = None
     iptrunk_side_ae_geant_a_sid: str | None = None
-    iptrunk_side_ae_members: list[str] = Field(default_factory=list)
-    iptrunk_side_ae_members_description: list[str] = Field(default_factory=list)
+    iptrunk_side_ae_members: LAGMemberList[IptrunkInterfaceBlockInactive]
 
 
 class IptrunkSideBlockProvisioning(IptrunkSideBlockInactive, lifecycle=[SubscriptionLifecycle.PROVISIONING]):
     iptrunk_side_node: RouterBlockProvisioning
     iptrunk_side_ae_iface: str | None = None
     iptrunk_side_ae_geant_a_sid: str | None = None
-    iptrunk_side_ae_members: list[str] = Field(default_factory=list)
-    iptrunk_side_ae_members_description: list[str] = Field(default_factory=list)
+    iptrunk_side_ae_members: LAGMemberList[IptrunkInterfaceBlockProvisioning]
 
 
 class IptrunkSideBlock(IptrunkSideBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTIVE]):
     iptrunk_side_node: RouterBlock
     iptrunk_side_ae_iface: str | None = None
     iptrunk_side_ae_geant_a_sid: str | None = None
-    iptrunk_side_ae_members: list[str] = Field(default_factory=list)
-    iptrunk_side_ae_members_description: list[str] = Field(default_factory=list)
+    iptrunk_side_ae_members: LAGMemberList[IptrunkInterfaceBlock]
 
 
 class IptrunkBlockInactive(
@@ -58,12 +88,11 @@ class IptrunkBlockInactive(
     geant_s_sid: str | None = None
     iptrunk_description: str | None = None
     iptrunk_type: IptrunkType | None = None
-    iptrunk_speed: str | None = None
+    iptrunk_speed: PhyPortCapacity | None = None
     iptrunk_minimum_links: int | None = None
     iptrunk_isis_metric: int | None = None
     iptrunk_ipv4_network: ipaddress.IPv4Network | None = None
     iptrunk_ipv6_network: ipaddress.IPv6Network | None = None
-    #
     iptrunk_sides: IptrunkSides[IptrunkSideBlockInactive]
 
 
@@ -73,12 +102,11 @@ class IptrunkBlockProvisioning(IptrunkBlockInactive, lifecycle=[SubscriptionLife
     geant_s_sid: str | None = None
     iptrunk_description: str | None = None
     iptrunk_type: IptrunkType | None = None
-    iptrunk_speed: str | None = None
+    iptrunk_speed: PhyPortCapacity | None = None
     iptrunk_minimum_links: int | None = None
     iptrunk_isis_metric: int | None = None
     iptrunk_ipv4_network: ipaddress.IPv4Network | None = None
     iptrunk_ipv6_network: ipaddress.IPv6Network | None = None
-    #
     iptrunk_sides: IptrunkSides[IptrunkSideBlockProvisioning]
 
 
@@ -92,7 +120,7 @@ class IptrunkBlock(IptrunkBlockProvisioning, lifecycle=[SubscriptionLifecycle.AC
     #:  The type of trunk, can be either dark fibre or leased capacity.
     iptrunk_type: IptrunkType
     #:  The speed of the trunk, measured per interface associated with it.
-    iptrunk_speed: str  # FIXME: should be of PhyPortCapacity type
+    iptrunk_speed: PhyPortCapacity
     #:  The minimum amount of links the trunk should consist of.
     iptrunk_minimum_links: int
     #:  The :term:`IS-IS` metric of this link
diff --git a/gso/products/product_blocks/router.py b/gso/products/product_blocks/router.py
index 58cf1d3602ffe3a10bc226f71064c88286a0bbf4..a8a820448a1b7388b903b0be69b7da9d4c17d660 100644
--- a/gso/products/product_blocks/router.py
+++ b/gso/products/product_blocks/router.py
@@ -3,9 +3,9 @@ import ipaddress
 
 from orchestrator.domain.base import ProductBlockModel
 from orchestrator.types import SubscriptionLifecycle, strEnum
+from pydantic import ConstrainedInt
 
 from gso.products.product_blocks.site import SiteBlock, SiteBlockInactive, SiteBlockProvisioning
-from gso.products.shared import PortNumber
 
 
 class RouterVendor(strEnum):
@@ -23,6 +23,16 @@ class RouterRole(strEnum):
     AMT = "amt"
 
 
+class PortNumber(ConstrainedInt):
+    """Constrained integer for valid port numbers.
+
+    The range from 49152 to 65535 is marked as ephemeral, and can therefore not be selected for permanent allocation.
+    """
+
+    gt = 0
+    le = 49151
+
+
 class RouterBlockInactive(
     ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="RouterBlock"
 ):
@@ -92,5 +102,5 @@ class RouterBlock(RouterBlockProvisioning, lifecycle=[SubscriptionLifecycle.ACTI
     router_role: RouterRole
     #:  The :class:`Site` that this router resides in. Both physically and computationally.
     router_site: SiteBlock
-    #:  The router is going to have a LT interface between inet0 and IAS
+    #:  The router is going to have an LT interface between inet0 and IAS
     router_is_ias_connected: bool
diff --git a/gso/products/product_blocks/site.py b/gso/products/product_blocks/site.py
index 74f4cf25dd1266424e222f301439a0a9f3dde694..739e1a0a91629b6dc2e427bfbd6ccd93daa8bb63 100644
--- a/gso/products/product_blocks/site.py
+++ b/gso/products/product_blocks/site.py
@@ -1,8 +1,10 @@
 """The product block that describes a site subscription."""
+import re
+from typing import Union
+
 from orchestrator.domain.base import ProductBlockModel
 from orchestrator.types import SubscriptionLifecycle, strEnum
-
-from gso.schemas.types import LatitudeCoordinate, LongitudeCoordinate
+from pydantic import ConstrainedStr
 
 
 class SiteTier(strEnum):
@@ -18,6 +20,42 @@ class SiteTier(strEnum):
     TIER4 = 4
 
 
+class LatitudeCoordinate(ConstrainedStr):
+    """A latitude coordinate, modeled as a constrained string.
+
+    The coordinate must match the format conforming to the latitude range of -90 to +90 degrees. It can be a
+    floating-point number or an integer.
+    Valid examples: 40.7128, -74.0060, 90, -90, 0
+    """
+
+    regex = re.compile(r"^-?([1-8]?\d(\.\d+)?|90(\.0+)?)$")
+
+    @classmethod
+    def validate(cls, value: Union[str]) -> Union[str]:
+        if not cls.regex.match(value):
+            raise ValueError("Invalid latitude coordinate. Valid examples: '40.7128', '-74.0060', '90', '-90', '0'.")
+
+        return value
+
+
+class LongitudeCoordinate(ConstrainedStr):
+    """A longitude coordinate, modeled as a constrained string.
+
+    The coordinate must match the format conforming to the longitude
+    range of -180 to +180 degrees. It can be a floating point number or an integer.
+    Valid examples: 40.7128, -74.0060, 180, -180, 0
+    """
+
+    regex = re.compile(r"^-?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$")
+
+    @classmethod
+    def validate(cls, value: Union[str]) -> Union[str]:
+        if not cls.regex.match(value):
+            raise ValueError("Invalid longitude coordinate. Valid examples: '40.7128', '-74.0060', '180', '-180'")
+
+        return value
+
+
 class SiteBlockInactive(ProductBlockModel, lifecycle=[SubscriptionLifecycle.INITIAL], product_block_name="SiteBlock"):
     """A site that's currently inactive, see :class:`SiteBlock`."""
 
diff --git a/gso/products/shared.py b/gso/products/shared.py
deleted file mode 100644
index 81e080618557b4431b1c58df1c9f9c172d79078f..0000000000000000000000000000000000000000
--- a/gso/products/shared.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from pydantic import ConstrainedInt
-
-
-class PortNumber(ConstrainedInt):
-    """Constrained integer for valid port numbers.
-
-    The range from 49152 to 65535 is marked as ephemeral, and can therefore not be selected for permanent allocation.
-    """
-
-    gt = 0
-    le = 49151
diff --git a/gso/schemas/enums.py b/gso/schemas/enums.py
deleted file mode 100644
index c803ede4acf8757199d5f5e28fe37576378a9dea..0000000000000000000000000000000000000000
--- a/gso/schemas/enums.py
+++ /dev/null
@@ -1,11 +0,0 @@
-from orchestrator.types import strEnum
-
-
-class ProductType(strEnum):
-    SITE = "Site"
-    ROUTER = "Router"
-    IP_TRUNK = "IP trunk"
-
-
-class SubscriptionStatus(strEnum):
-    ACTIVE = "active"
diff --git a/gso/schemas/imports.py b/gso/schemas/imports.py
deleted file mode 100644
index da1e242aab47d85abb7bc01d18cf1a51eaf034d8..0000000000000000000000000000000000000000
--- a/gso/schemas/imports.py
+++ /dev/null
@@ -1,125 +0,0 @@
-import ipaddress
-from typing import Any
-from uuid import UUID
-
-from pydantic import BaseModel, root_validator, validator
-
-from gso.products.product_blocks import PhyPortCapacity
-from gso.products.product_blocks.iptrunk import IptrunkType
-from gso.products.product_blocks.router import RouterRole, RouterVendor
-from gso.products.product_blocks.site import SiteTier
-from gso.services import subscriptions
-from gso.services.crm import CustomerNotFoundError, get_customer_by_name
-
-
-class ImportResponseModel(BaseModel):
-    pid: UUID
-    detail: str
-
-
-class SiteImportModel(BaseModel):
-    site_name: str
-    site_city: str
-    site_country: str
-    site_country_code: str
-    site_latitude: float
-    site_longitude: float
-    site_bgp_community_id: int
-    site_internal_id: int
-    site_tier: SiteTier
-    site_ts_address: str
-    customer: str
-
-
-class RouterImportModel(BaseModel):
-    customer: str
-    router_site: str
-    hostname: str
-    ts_port: int
-    router_vendor: RouterVendor
-    router_role: RouterRole
-    is_ias_connected: bool
-    router_lo_ipv4_address: ipaddress.IPv4Address
-    router_lo_ipv6_address: ipaddress.IPv6Address
-    router_lo_iso_address: str
-    router_si_ipv4_network: ipaddress.IPv4Network | None = None
-    router_ias_lt_ipv4_network: ipaddress.IPv4Network | None = None
-    router_ias_lt_ipv6_network: ipaddress.IPv6Network | None = None
-
-
-class IptrunkImportModel(BaseModel):
-    customer: str
-    geant_s_sid: str
-    iptrunk_type: IptrunkType
-    iptrunk_description: str
-    iptrunk_speed: PhyPortCapacity
-    iptrunk_minimum_links: int
-    iptrunk_sideA_node_id: str
-    iptrunk_sideA_ae_iface: str
-    iptrunk_sideA_ae_geant_a_sid: str
-    iptrunk_sideA_ae_members: list[str]
-    iptrunk_sideA_ae_members_descriptions: list[str]
-    iptrunk_sideB_node_id: str
-    iptrunk_sideB_ae_iface: str
-    iptrunk_sideB_ae_geant_a_sid: str
-    iptrunk_sideB_ae_members: list[str]
-    iptrunk_sideB_ae_members_descriptions: list[str]
-
-    iptrunk_ipv4_network: ipaddress.IPv4Network
-    iptrunk_ipv6_network: ipaddress.IPv6Network
-
-    @classmethod
-    def _get_active_routers(cls) -> set[str]:
-        return {
-            str(router_id) for router_id in subscriptions.get_active_router_subscriptions(fields=["subscription_id"])
-        }
-
-    @validator("customer")
-    def check_if_customer_exists(cls, value: str) -> str:
-        try:
-            get_customer_by_name(value)
-        except CustomerNotFoundError:
-            raise ValueError(f"Customer {value} not found")
-
-        return value
-
-    @validator("iptrunk_sideA_node_id", "iptrunk_sideB_node_id")
-    def check_if_router_side_is_available(cls, value: str) -> str:
-        if value not in cls._get_active_routers():
-            raise ValueError("Router not found")
-
-        return value
-
-    @validator("iptrunk_sideA_ae_members", "iptrunk_sideB_ae_members")
-    def check_side_uniqueness(cls, value: list[str]) -> list[str]:
-        if len(value) != len(set(value)):
-            raise ValueError("Items must be unique")
-
-        return value
-
-    @root_validator
-    def check_members(cls, values: dict[str, Any]) -> dict[str, Any]:
-        min_links = values["iptrunk_minimum_links"]
-        side_a_members = values.get("iptrunk_sideA_ae_members", [])
-        side_a_descriptions = values.get("iptrunk_sideA_ae_members_descriptions", [])
-        side_b_members = values.get("iptrunk_sideB_ae_members", [])
-        side_b_descriptions = values.get("iptrunk_sideB_ae_members_descriptions", [])
-
-        len_a = len(side_a_members)
-        len_a_desc = len(side_a_descriptions)
-        len_b = len(side_b_members)
-        len_b_desc = len(side_b_descriptions)
-
-        if len_a < min_links:
-            raise ValueError(f"Side A members should be at least {min_links} (iptrunk_minimum_links)")
-
-        if len_a != len_a_desc:
-            raise ValueError("Mismatch in Side A members and their descriptions")
-
-        if len_a != len_b:
-            raise ValueError("Mismatch between Side A and B members")
-
-        if len_a != len_b_desc:
-            raise ValueError("Mismatch in Side B members and their descriptions")
-
-        return values
diff --git a/gso/schemas/types.py b/gso/schemas/types.py
deleted file mode 100644
index 114e573611b457a6adf3360517c3bd599021e08b..0000000000000000000000000000000000000000
--- a/gso/schemas/types.py
+++ /dev/null
@@ -1,40 +0,0 @@
-import re
-from typing import Union
-
-from pydantic import ConstrainedStr
-
-
-class LatitudeCoordinate(ConstrainedStr):
-    """A latitude coordinate, modeled as a constrained string.
-
-    The coordinate must match the format conforming to the latitude range of -90 to +90 degrees. It can be a
-    floating-point number or an integer.
-    Valid examples: 40.7128, -74.0060, 90, -90, 0
-    """
-
-    regex = re.compile(r"^-?([1-8]?\d(\.\d+)?|90(\.0+)?)$")
-
-    @classmethod
-    def validate(cls, value: Union[str]) -> Union[str]:
-        if not cls.regex.match(value):
-            raise ValueError("Invalid latitude coordinate. Valid examples: '40.7128', '-74.0060', '90', '-90', '0'.")
-
-        return value
-
-
-class LongitudeCoordinate(ConstrainedStr):
-    """A longitude coordinate, modeled as a constrained string.
-
-    The coordinate must match the format conforming to the longitude
-    range of -180 to +180 degrees. It can be a floating point number or an integer.
-    Valid examples: 40.7128, -74.0060, 180, -180, 0
-    """
-
-    regex = re.compile(r"^-?(180(\.0+)?|((1[0-7]\d)|([1-9]?\d))(\.\d+)?)$")
-
-    @classmethod
-    def validate(cls, value: Union[str]) -> Union[str]:
-        if not cls.regex.match(value):
-            raise ValueError("Invalid longitude coordinate. Valid examples: '40.7128', '-74.0060', '180', '-180'")
-
-        return value
diff --git a/gso/services/crm.py b/gso/services/crm.py
index 8c2af4698aa2c16478f372388df88ae6d125c308..5ccd3e45f80b0febf706f001f5e85e71c1c38bd9 100644
--- a/gso/services/crm.py
+++ b/gso/services/crm.py
@@ -1,5 +1,7 @@
 from typing import Any
 
+from pydantic_forms.validators import Choice
+
 
 class CustomerNotFoundError(Exception):
     """Exception raised when a customer is not found."""
@@ -22,3 +24,11 @@ def get_customer_by_name(name: str) -> dict[str, Any]:
             return customer
 
     raise CustomerNotFoundError(f"Customer {name} not found")
+
+
+def customer_selector() -> Choice:
+    customers = {}
+    for customer in all_customers():
+        customers[customer["id"]] = customer["name"]
+
+    return Choice("Select a customer", zip(customers.keys(), customers.items()))  # type: ignore[arg-type]
diff --git a/gso/services/netbox_client.py b/gso/services/netbox_client.py
index 45b0906b2ad28c212b995de483810c51052b02b5..02f5313a94d88e48447335c7945da5e14ea0aabb 100644
--- a/gso/services/netbox_client.py
+++ b/gso/services/netbox_client.py
@@ -5,13 +5,12 @@ import pydantic
 import pynetbox
 from pynetbox.models.dcim import Devices, DeviceTypes, Interfaces
 
-from gso.products import Router
+from gso.products.product_types.router import Router
 from gso.settings import load_oss_params
 from gso.utils.device_info import DEFAULT_SITE, FEASIBLE_IP_TRUNK_LAG_RANGE, ROUTER_ROLE, TierInfo
 from gso.utils.exceptions import NotFoundError, WorkflowStateError
 
 
-# Define device models
 class Manufacturer(pydantic.BaseModel):
     """Defines the manufacturer of a device."""
 
@@ -22,8 +21,7 @@ class Manufacturer(pydantic.BaseModel):
 class DeviceType(pydantic.BaseModel):
     """Defines the device type.
 
-    The manufacturer should be created first to get the manufacturer id,
-    which is defined here as int.
+    The manufacturer should be created first to get the manufacturer id, which is defined here as int.
     """
 
     manufacturer: int
@@ -46,7 +44,7 @@ class Site(pydantic.BaseModel):
 
 
 class NetboxClient:
-    """Implement all methods to communicate with the NetBox API."""
+    """Implement all methods to communicate with the Netbox :term:`API`."""
 
     def __init__(self) -> None:
         self.netbox_params = load_oss_params().NETBOX
@@ -56,7 +54,7 @@ class NetboxClient:
         return list(self.netbox.dcim.devices.all())
 
     def get_device_by_name(self, device_name: str) -> Devices:
-        """Return the device object by name from netbox, or None if not found."""
+        """Return the device object by name from netbox, or ``None`` if not found."""
         return self.netbox.dcim.devices.get(name=device_name)
 
     def get_interfaces_by_device(self, device_name: str, speed: str) -> list[Interfaces]:
@@ -87,11 +85,13 @@ class NetboxClient:
         )
 
     def create_device_type(self, manufacturer: str, model: str, slug: str) -> DeviceTypes:
-        """Create a new device type in netbox."""
+        """Create a new device type in Netbox."""
 
         # First get manufacturer id
         manufacturer_id = int(self.netbox.dcim.manufacturers.get(name=manufacturer).id)
-        device_type = DeviceType(**{"manufacturer": manufacturer_id, "model": model, "slug": slug})  # type: ignore
+        device_type = DeviceType(
+            **{"manufacturer": manufacturer_id, "model": model, "slug": slug}  # type: ignore[arg-type]
+        )
         return self.netbox.dcim.device_types.create(dict(device_type))
 
     def create_device_role(self, name: str, slug: str) -> DeviceRole:
@@ -116,7 +116,7 @@ class NetboxClient:
         return None
 
     def create_device(self, router_name: str, site_tier: str) -> Devices:
-        """Create a new device in netbox."""
+        """Create a new device in Netbox."""
 
         # Get device type id
         tier_info = TierInfo().get_module_by_name(f"Tier{site_tier}")
@@ -159,7 +159,7 @@ class NetboxClient:
     def attach_interface_to_lag(
         self, device_name: str, lag_name: str, iface_name: str, description: str | None = None
     ) -> Interfaces:
-        """Assign a given interface to a LAG.
+        """Assign a given interface to a :term:`LAG`.
 
         Returns the interface object after assignment.
         """
@@ -225,20 +225,20 @@ class NetboxClient:
         return interface
 
     def get_available_lags(self, router_id: UUID) -> list[str]:
-        """Return all available lags not assigned to a device."""
+        """Return all available :term:`LAG`s not assigned to a device."""
 
         router_name = Router.from_subscription(router_id).router.router_fqdn
         device = self.get_device_by_name(router_name)
 
-        # Get the existing lag interfaces for the device
+        # Get the existing LAG interfaces for the device
         lag_interface_names = [
             interface["name"] for interface in self.netbox.dcim.interfaces.filter(device=device.name, type="lag")
         ]
 
-        # Generate all feasible lags
+        # Generate all feasible LAGs
         all_feasible_lags = [f"LAG-{i}" for i in FEASIBLE_IP_TRUNK_LAG_RANGE]
 
-        # Return available lags not assigned to the device
+        # Return available LAGs not assigned to the device
         return [lag for lag in all_feasible_lags if lag not in lag_interface_names]
 
     @staticmethod
diff --git a/gso/services/subscriptions.py b/gso/services/subscriptions.py
index 5e234813f34dba4818da1e7504e0d33a023070ab..f1c6b075a6fe211217ab3ad407a1ef371df47d9c 100644
--- a/gso/services/subscriptions.py
+++ b/gso/services/subscriptions.py
@@ -8,14 +8,12 @@ from orchestrator.db import (
     SubscriptionInstanceValueTable,
     SubscriptionTable,
 )
+from orchestrator.types import SubscriptionLifecycle
 
-from gso.schemas.enums import ProductType, SubscriptionStatus
+from gso.products import ProductType
 
 
-def get_active_subscriptions(
-    product_type: str,
-    fields: list[str],
-) -> list[Subscription]:
+def get_active_subscriptions(product_type: str, fields: list[str]) -> list[Subscription]:
     """Retrieve active subscriptions for a specific product type.
 
     :param product_type: The type of the product for which to retrieve subscriptions.
@@ -32,7 +30,7 @@ def get_active_subscriptions(
         SubscriptionTable.query.join(ProductTable)
         .filter(
             ProductTable.product_type == product_type,
-            SubscriptionTable.status == SubscriptionStatus.ACTIVE,
+            SubscriptionTable.status == SubscriptionLifecycle.ACTIVE,
         )
         .with_entities(*dynamic_fields)
         .all()
@@ -91,6 +89,6 @@ def get_active_site_subscription_by_name(site_name: str) -> Subscription:
         .join(ResourceTypeTable)
         .filter(SubscriptionInstanceValueTable.value == site_name)
         .filter(ResourceTypeTable.resource_type == "site_name")
-        .filter(SubscriptionTable.status == SubscriptionStatus.ACTIVE)
+        .filter(SubscriptionTable.status == SubscriptionLifecycle.ACTIVE)
         .first()
     )
diff --git a/gso/settings.py b/gso/settings.py
index 701f65fddc74029e2fba309d76f5ec275fd1d9e8..d7b48048872b8de9cc4109fb3eb969ddc41f8ca4 100644
--- a/gso/settings.py
+++ b/gso/settings.py
@@ -1,6 +1,7 @@
 """:term:`GSO` settings.
 
-Ensuring that the required parameters are set correctly.
+Ensuring that the required parameters are set correctly. An example file ``oss-params-example.json`` is present in the
+:term:`GSO` package itself.
 """
 
 import ipaddress
@@ -8,7 +9,7 @@ import json
 import logging
 import os
 
-from pydantic import BaseSettings
+from pydantic import BaseSettings, NonNegativeInt
 
 logger = logging.getLogger(__name__)
 
@@ -31,12 +32,20 @@ class InfoBloxParams(BaseSettings):
     password: str
 
 
+class V4Netmask(NonNegativeInt):
+    le = 32
+
+
+class V6Netmask(NonNegativeInt):
+    le = 128
+
+
 class V4NetworkParams(BaseSettings):
     """A set of parameters that describe an IPv4 network in InfoBlox."""
 
     containers: list[ipaddress.IPv4Network]
     networks: list[ipaddress.IPv4Network]
-    mask: int  # TODO: validation on mask?
+    mask: V4Netmask
 
 
 class V6NetworkParams(BaseSettings):
@@ -44,7 +53,7 @@ class V6NetworkParams(BaseSettings):
 
     containers: list[ipaddress.IPv6Network]
     networks: list[ipaddress.IPv6Network]
-    mask: int  # TODO: validation on mask?
+    mask: V6Netmask
 
 
 class ServiceNetworkParams(BaseSettings):
@@ -75,7 +84,9 @@ class ProvisioningProxyParams(BaseSettings):
 
     scheme: str
     api_base: str
-    auth: str  # FIXME: unfinished
+    #:  .. deprecated:: 0.1
+    #:     Not used anymore, may be left out from config file.
+    auth: str | None
     api_version: int
 
 
diff --git a/gso/workflows/utils.py b/gso/utils/helpers.py
similarity index 64%
rename from gso/workflows/utils.py
rename to gso/utils/helpers.py
index cd461b81131917b704fcc6b7875f8a2ed7264470..ff37395b92b3c3d7a8c9b5c61e392afa7b7bbe31 100644
--- a/gso/workflows/utils.py
+++ b/gso/utils/helpers.py
@@ -2,21 +2,38 @@ import re
 from ipaddress import IPv4Address
 from uuid import UUID
 
-from orchestrator.forms.validators import Choice
-from orchestrator.types import UUIDstr
+from orchestrator import step
+from orchestrator.types import State, UUIDstr
+from pydantic import BaseModel
+from pydantic_forms.validators import Choice
 
 from gso.products.product_blocks.router import RouterVendor
+from gso.products.product_types.iptrunk import Iptrunk
 from gso.products.product_types.router import Router
-from gso.services.crm import all_customers
+from gso.services import provisioning_proxy
 from gso.services.netbox_client import NetboxClient
 
 
-def customer_selector() -> Choice:
-    customers = {}
-    for customer in all_customers():
-        customers[customer["id"]] = customer["name"]
+class LAGMember(BaseModel):
+    #  TODO: validate interface name
+    interface_name: str
+    interface_description: str
 
-    return Choice("Select a customer", zip(customers.keys(), customers.items()))  # type: ignore[arg-type]
+    def __hash__(self) -> int:
+        return hash((self.interface_name, self.interface_description))
+
+
+@step("[COMMIT] Set ISIS metric to 90000")
+def set_isis_to_90000(subscription: Iptrunk, process_id: UUIDstr, tt_number: str) -> State:
+    old_isis_metric = subscription.iptrunk.iptrunk_isis_metric
+    subscription.iptrunk.iptrunk_isis_metric = 90000
+    provisioning_proxy.provision_ip_trunk(subscription, process_id, tt_number, "isis_interface", False)
+
+    return {
+        "subscription": subscription,
+        "old_isis_metric": old_isis_metric,
+        "label_text": "ISIS is being set to 90K by the provisioning proxy, please wait for the results",
+    }
 
 
 def available_interfaces_choices(router_id: UUID, speed: str) -> Choice | None:
@@ -90,3 +107,20 @@ def validate_router_in_netbox(subscription_id: UUIDstr) -> UUIDstr | None:
         if not device:
             raise ValueError("The selected router does not exist in Netbox.")
     return subscription_id
+
+
+def validate_iptrunk_unique_interface(interfaces: list[LAGMember]) -> list[LAGMember]:
+    """Verify if the interfaces are unique.
+
+    Args:
+    ----
+    interfaces (list[LAGMember]): The list of interfaces.
+
+    Returns:
+    -------
+    list[LAGMember]: The list of interfaces or raises an error.
+    """
+    interface_names = [member.interface_name for member in interfaces]
+    if len(interface_names) != len(set(interface_names)):
+        raise ValueError("Interfaces must be unique.")
+    return interfaces
diff --git a/gso/workflows/iptrunk/create_iptrunk.py b/gso/workflows/iptrunk/create_iptrunk.py
index b08da1292fc6f34459a8af0419cc14d21c251eb6..f022181b5060632807b5c67aa9548aba9876505e 100644
--- a/gso/workflows/iptrunk/create_iptrunk.py
+++ b/gso/workflows/iptrunk/create_iptrunk.py
@@ -1,5 +1,7 @@
+from uuid import uuid4
+
 from orchestrator.forms import FormPage
-from orchestrator.forms.validators import Choice, ChoiceList, UniqueConstrainedList
+from orchestrator.forms.validators import Choice, UniqueConstrainedList
 from orchestrator.targets import Target
 from orchestrator.types import FormGenerator, State, SubscriptionLifecycle, UUIDstr
 from orchestrator.workflow import StepList, done, init, step, workflow
@@ -8,19 +10,20 @@ from orchestrator.workflows.utils import wrap_create_initial_input_form
 from pydantic import validator
 from pynetbox.models.dcim import Interfaces
 
-from gso.products.product_blocks import PhyPortCapacity
-from gso.products.product_blocks.iptrunk import IptrunkType
+from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlockInactive, IptrunkType, PhyPortCapacity
 from gso.products.product_blocks.router import RouterVendor
 from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning
 from gso.products.product_types.router import Router
 from gso.services import infoblox, provisioning_proxy, subscriptions
+from gso.services.crm import customer_selector
 from gso.services.netbox_client import NetboxClient
 from gso.services.provisioning_proxy import pp_interaction
-from gso.workflows.utils import (
+from gso.utils.helpers import (
+    LAGMember,
     available_interfaces_choices,
     available_lags_choices,
-    customer_selector,
     get_router_vendor,
+    validate_iptrunk_unique_interface,
     validate_router_in_netbox,
 )
 
@@ -49,84 +52,95 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
 
     initial_user_input = yield CreateIptrunkForm
 
-    router_enum_a = Choice("Select a router", zip(routers.keys(), routers.items()))  # type: ignore
+    router_enum_a = Choice("Select a router", zip(routers.keys(), routers.items()))  # type: ignore[arg-type]
 
     class SelectRouterSideA(FormPage):
         class Config:
             title = "Select a router for side A of the trunk."
 
-        iptrunk_sideA_node_id: router_enum_a  # type: ignore[valid-type]
+        side_a_node_id: router_enum_a  # type: ignore[valid-type]
 
-        @validator("iptrunk_sideA_node_id", allow_reuse=True)
-        def validate_device_exists_in_netbox(cls, iptrunk_sideA_node_id: UUIDstr) -> str | None:
-            return validate_router_in_netbox(iptrunk_sideA_node_id)
+        @validator("side_a_node_id", allow_reuse=True)
+        def validate_device_exists_in_netbox(cls, side_a_node_id: UUIDstr) -> str | None:
+            return validate_router_in_netbox(side_a_node_id)
 
     user_input_router_side_a = yield SelectRouterSideA
-    router_a = user_input_router_side_a.iptrunk_sideA_node_id.name
-    side_a_ae_iface = available_lags_choices(router_a) or str
+    router_a = user_input_router_side_a.side_a_node_id.name
 
-    class AeMembersListA(ChoiceList):
+    class JuniperAeMembers(UniqueConstrainedList[LAGMember]):
         min_items = initial_user_input.iptrunk_minimum_links
-        item_type = available_interfaces_choices(router_a, initial_user_input.iptrunk_speed)  # type: ignore
-        unique_items = True
 
-    class JuniperAeMembers(UniqueConstrainedList[str]):
-        min_items = initial_user_input.iptrunk_minimum_links
-        unique_items = True
+    if get_router_vendor(router_a) == RouterVendor.NOKIA:
 
-    ae_members_side_a = AeMembersListA if get_router_vendor(router_a) == RouterVendor.NOKIA else JuniperAeMembers
+        class NokiaLAGMemberA(LAGMember):
+            interface_name: available_interfaces_choices(  # type: ignore[valid-type]
+                router_a, initial_user_input.iptrunk_speed
+            )
 
-    class AeMembersDescriptionListA(UniqueConstrainedList[str]):
-        min_items = initial_user_input.iptrunk_minimum_links
+        class NokiaAeMembersA(UniqueConstrainedList[NokiaLAGMemberA]):
+            min_items = initial_user_input.iptrunk_minimum_links
+
+        ae_members_side_a = NokiaAeMembersA
+    else:
+        ae_members_side_a = JuniperAeMembers  # type: ignore[assignment]
 
     class CreateIptrunkSideAForm(FormPage):
         class Config:
             title = "Provide subscription details for side A of the trunk."
 
-        iptrunk_sideA_ae_iface: side_a_ae_iface  # type: ignore[valid-type]
-        iptrunk_sideA_ae_geant_a_sid: str
-        iptrunk_sideA_ae_members: ae_members_side_a  # type: ignore[valid-type]
-        iptrunk_sideA_ae_members_descriptions: AeMembersDescriptionListA
+        side_a_ae_iface: available_lags_choices(router_a) or str  # type: ignore[valid-type]
+        side_a_ae_geant_a_sid: str
+        side_a_ae_members: ae_members_side_a  # type: ignore[valid-type]
+
+        @validator("side_a_ae_members", allow_reuse=True)
+        def validate_iptrunk_unique_interface_side_a(cls, side_a_ae_members: list[LAGMember]) -> list[LAGMember]:
+            return validate_iptrunk_unique_interface(side_a_ae_members)
 
     user_input_side_a = yield CreateIptrunkSideAForm
     # Remove the selected router for side A, to prevent any loops
-    routers.pop(str(user_input_router_side_a.iptrunk_sideA_node_id.name))
-    router_enum_b = Choice("Select a router", zip(routers.keys(), routers.items()))  # type: ignore
+    routers.pop(str(router_a))
+    router_enum_b = Choice("Select a router", zip(routers.keys(), routers.items()))  # type: ignore[arg-type]
 
     class SelectRouterSideB(FormPage):
         class Config:
             title = "Select a router for side B of the trunk."
 
-        iptrunk_sideB_node_id: router_enum_b  # type: ignore[valid-type]
+        side_b_node_id: router_enum_b  # type: ignore[valid-type]
 
-        @validator("iptrunk_sideB_node_id", allow_reuse=True)
-        def validate_device_exists_in_netbox(cls, iptrunk_sideB_node_id: UUIDstr) -> str | None:
-            return validate_router_in_netbox(iptrunk_sideB_node_id)
+        @validator("side_b_node_id", allow_reuse=True)
+        def validate_device_exists_in_netbox(cls, side_b_node_id: UUIDstr) -> str | None:
+            return validate_router_in_netbox(side_b_node_id)
 
     user_input_router_side_b = yield SelectRouterSideB
-    router_b = user_input_router_side_b.iptrunk_sideB_node_id.name
-    side_b_ae_iface = available_lags_choices(router_b) or str
+    router_b = user_input_router_side_b.side_b_node_id.name
+
+    if get_router_vendor(router_b) == RouterVendor.NOKIA:
 
-    class AeMembersListB(ChoiceList):
-        min_items = len(user_input_side_a.iptrunk_sideA_ae_members)
-        max_items = len(user_input_side_a.iptrunk_sideA_ae_members)
-        item_type = available_interfaces_choices(router_b, initial_user_input.iptrunk_speed)  # type: ignore
-        unique_items = True
+        class NokiaLAGMemberB(LAGMember):
+            interface_name: available_interfaces_choices(  # type: ignore[valid-type]
+                router_b, initial_user_input.iptrunk_speed
+            )
 
-    ae_members_side_b = AeMembersListB if get_router_vendor(router_b) == RouterVendor.NOKIA else JuniperAeMembers
+        class NokiaAeMembersB(UniqueConstrainedList):
+            min_items = len(user_input_side_a.side_a_ae_members)
+            max_items = len(user_input_side_a.side_a_ae_members)
+            item_type = NokiaLAGMemberB
 
-    class AeMembersDescriptionListB(UniqueConstrainedList[str]):
-        min_items = len(user_input_side_a.iptrunk_sideA_ae_members)
-        max_items = len(user_input_side_a.iptrunk_sideA_ae_members)
+        ae_members_side_b = NokiaAeMembersB
+    else:
+        ae_members_side_b = JuniperAeMembers  # type: ignore[assignment]
 
     class CreateIptrunkSideBForm(FormPage):
         class Config:
             title = "Provide subscription details for side B of the trunk."
 
-        iptrunk_sideB_ae_iface: side_b_ae_iface  # type: ignore[valid-type]
-        iptrunk_sideB_ae_geant_a_sid: str
-        iptrunk_sideB_ae_members: ae_members_side_b  # type: ignore[valid-type]
-        iptrunk_sideB_ae_members_descriptions: AeMembersDescriptionListB
+        side_b_ae_iface: available_lags_choices(router_b) or str  # type: ignore[valid-type]
+        side_b_ae_geant_a_sid: str
+        side_b_ae_members: ae_members_side_b  # type: ignore[valid-type]
+
+        @validator("side_b_ae_members", allow_reuse=True)
+        def validate_iptrunk_unique_interface_side_b(cls, side_b_ae_members: list[LAGMember]) -> list[LAGMember]:
+            return validate_iptrunk_unique_interface(side_b_ae_members)
 
     user_input_side_b = yield CreateIptrunkSideBForm
 
@@ -167,37 +181,39 @@ def initialize_subscription(
     geant_s_sid: str,
     iptrunk_type: IptrunkType,
     iptrunk_description: str,
-    iptrunk_speed: str,
+    iptrunk_speed: PhyPortCapacity,
     iptrunk_minimum_links: int,
-    iptrunk_sideA_node_id: str,
-    iptrunk_sideA_ae_iface: str,
-    iptrunk_sideA_ae_geant_a_sid: str,
-    iptrunk_sideA_ae_members: list[str],
-    iptrunk_sideA_ae_members_descriptions: list[str],
-    iptrunk_sideB_node_id: str,
-    iptrunk_sideB_ae_iface: str,
-    iptrunk_sideB_ae_geant_a_sid: str,
-    iptrunk_sideB_ae_members: list[str],
-    iptrunk_sideB_ae_members_descriptions: list[str],
+    side_a_node_id: str,
+    side_a_ae_iface: str,
+    side_a_ae_geant_a_sid: str,
+    side_a_ae_members: list[dict],
+    side_b_node_id: str,
+    side_b_ae_iface: str,
+    side_b_ae_geant_a_sid: str,
+    side_b_ae_members: list[dict],
 ) -> State:
     subscription.iptrunk.geant_s_sid = geant_s_sid
     subscription.iptrunk.iptrunk_description = iptrunk_description
     subscription.iptrunk.iptrunk_type = iptrunk_type
     subscription.iptrunk.iptrunk_speed = iptrunk_speed
-    subscription.iptrunk.iptrunk_isis_metric = 9000
+    subscription.iptrunk.iptrunk_isis_metric = 90000
     subscription.iptrunk.iptrunk_minimum_links = iptrunk_minimum_links
 
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node = Router.from_subscription(iptrunk_sideA_node_id).router
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_iface = iptrunk_sideA_ae_iface
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid = iptrunk_sideA_ae_geant_a_sid
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members = iptrunk_sideA_ae_members
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members_description = iptrunk_sideA_ae_members_descriptions
-
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node = Router.from_subscription(iptrunk_sideB_node_id).router
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_iface = iptrunk_sideB_ae_iface
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid = iptrunk_sideB_ae_geant_a_sid
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members = iptrunk_sideB_ae_members
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members_description = iptrunk_sideB_ae_members_descriptions
+    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node = Router.from_subscription(side_a_node_id).router
+    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_iface = side_a_ae_iface
+    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid = side_a_ae_geant_a_sid
+    for member in side_a_ae_members:
+        subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members.append(
+            IptrunkInterfaceBlockInactive.new(subscription_id=uuid4(), **member)
+        )
+
+    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node = Router.from_subscription(side_b_node_id).router
+    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_iface = side_b_ae_iface
+    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid = side_b_ae_geant_a_sid
+    for member in side_b_ae_members:
+        subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members.append(
+            IptrunkInterfaceBlockInactive.new(subscription_id=uuid4(), **member)
+        )
 
     subscription.description = f"IP trunk, geant_s_sid:{geant_s_sid}"
     subscription = IptrunkProvisioning.from_other_lifecycle(subscription, SubscriptionLifecycle.PROVISIONING)
@@ -287,12 +303,12 @@ def reserve_interfaces_in_netbox(subscription: IptrunkProvisioning) -> State:
                 nbclient.attach_interface_to_lag(
                     device_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn,
                     lag_name=lag_interface.name,
-                    iface_name=interface,
+                    iface_name=interface.interface_name,
                     description=str(subscription.subscription_id),
                 )
                 nbclient.reserve_interface(
                     device_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn,
-                    iface_name=interface,
+                    iface_name=interface.interface_name,
                 )
     return {
         "subscription": subscription,
@@ -308,7 +324,7 @@ def allocate_interfaces_in_netbox(subscription: IptrunkProvisioning) -> State:
             for interface in subscription.iptrunk.iptrunk_sides[side].iptrunk_side_ae_members:
                 NetboxClient().allocate_interface(
                     device_name=subscription.iptrunk.iptrunk_sides[side].iptrunk_side_node.router_fqdn,
-                    iface_name=interface,
+                    iface_name=interface.interface_name,
                 )
     return {
         "subscription": subscription,
diff --git a/gso/workflows/iptrunk/migrate_iptrunk.py b/gso/workflows/iptrunk/migrate_iptrunk.py
index 3b2c1b4d160dcfa3a51815c6b1b898f7e38e4dfb..4b918d00326114ed1da4f1b87a2a055f406bfe52 100644
--- a/gso/workflows/iptrunk/migrate_iptrunk.py
+++ b/gso/workflows/iptrunk/migrate_iptrunk.py
@@ -18,7 +18,7 @@ from gso.products.product_types.iptrunk import Iptrunk
 from gso.products.product_types.router import Router
 from gso.services import provisioning_proxy
 from gso.services.provisioning_proxy import pp_interaction
-from gso.workflows.iptrunk.utils import set_isis_to_90000
+from gso.utils.helpers import set_isis_to_90000
 
 logger = getLogger(__name__)
 
diff --git a/gso/workflows/iptrunk/modify_trunk_interface.py b/gso/workflows/iptrunk/modify_trunk_interface.py
index 87e3b8f52108b4dca6fd6f4aaba81127f6a3a2e4..284602504eb7af3d7d7563728fbe401a1ce0f424 100644
--- a/gso/workflows/iptrunk/modify_trunk_interface.py
+++ b/gso/workflows/iptrunk/modify_trunk_interface.py
@@ -1,4 +1,5 @@
 import ipaddress
+from uuid import uuid4
 
 from orchestrator.forms import FormPage, ReadOnlyField
 from orchestrator.forms.validators import UniqueConstrainedList
@@ -8,11 +9,11 @@ from orchestrator.workflow import StepList, done, init, step, workflow
 from orchestrator.workflows.steps import resync, store_process_subscription, unsync
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
 
-from gso.products.product_blocks import PhyPortCapacity
-from gso.products.product_blocks.iptrunk import IptrunkType
+from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, IptrunkType, PhyPortCapacity
 from gso.products.product_types.iptrunk import Iptrunk
 from gso.services import provisioning_proxy
 from gso.services.provisioning_proxy import pp_interaction
+from gso.utils.helpers import LAGMember
 
 
 def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
@@ -23,7 +24,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
         geant_s_sid: str = subscription.iptrunk.geant_s_sid
         iptrunk_description: str = subscription.iptrunk.iptrunk_description
         iptrunk_type: IptrunkType = subscription.iptrunk.iptrunk_type
-        iptrunk_speed: PhyPortCapacity = subscription.iptrunk.iptrunk_speed  # type: ignore[assignment]
+        iptrunk_speed: PhyPortCapacity = subscription.iptrunk.iptrunk_speed
         iptrunk_minimum_links: int = subscription.iptrunk.iptrunk_minimum_links
         iptrunk_isis_metric: int = ReadOnlyField(subscription.iptrunk.iptrunk_isis_metric)
         iptrunk_ipv4_network: ipaddress.IPv4Network = ReadOnlyField(subscription.iptrunk.iptrunk_ipv4_network)
@@ -31,38 +32,32 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
     initial_user_input = yield ModifyIptrunkForm
 
-    class AeMembersListA(UniqueConstrainedList[str]):
+    class AeMembersListA(UniqueConstrainedList[LAGMember]):
         min_items = initial_user_input.iptrunk_minimum_links
 
     class ModifyIptrunkSideAForm(FormPage):
         class Config:
             title = "Provide subscription details for side A of the trunk."
 
-        iptrunk_sideA_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn)
-        iptrunk_sideA_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_iface)
-        iptrunk_sideA_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid
-        iptrunk_sideA_ae_members: AeMembersListA = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members
-        iptrunk_sideA_ae_members_descriptions: AeMembersListA = subscription.iptrunk.iptrunk_sides[
-            0
-        ].iptrunk_side_ae_members_description
+        side_a_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_fqdn)
+        side_a_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_iface)
+        side_a_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid
+        side_a_ae_members: AeMembersListA = subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members
 
     user_input_side_a = yield ModifyIptrunkSideAForm
 
-    class AeMembersListB(UniqueConstrainedList[str]):
-        min_items = len(user_input_side_a.iptrunk_sideA_ae_members)
-        max_items = len(user_input_side_a.iptrunk_sideA_ae_members)
+    class AeMembersListB(UniqueConstrainedList[LAGMember]):
+        min_items = len(user_input_side_a.side_a_ae_members)
+        max_items = len(user_input_side_a.side_a_ae_members)
 
     class ModifyIptrunkSideBForm(FormPage):
         class Config:
             title = "Provide subscription details for side B of the trunk."
 
-        iptrunk_sideB_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn)
-        iptrunk_sideB_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_iface)
-        iptrunk_sideB_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid
-        iptrunk_sideB_ae_members: AeMembersListB = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members
-        iptrunk_sideB_ae_members_descriptions: AeMembersListB = subscription.iptrunk.iptrunk_sides[
-            1
-        ].iptrunk_side_ae_members_description
+        side_b_node: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_fqdn)
+        side_b_ae_iface: str = ReadOnlyField(subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_iface)
+        side_b_ae_geant_a_sid: str = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid
+        side_b_ae_members: AeMembersListB = subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members
 
     user_input_side_b = yield ModifyIptrunkSideBForm
 
@@ -75,14 +70,12 @@ def modify_iptrunk_subscription(
     geant_s_sid: str,
     iptrunk_type: IptrunkType,
     iptrunk_description: str,
-    iptrunk_speed: str,
+    iptrunk_speed: PhyPortCapacity,
     iptrunk_minimum_links: int,
-    iptrunk_sideA_ae_geant_a_sid: str,
-    iptrunk_sideA_ae_members: list[str],
-    iptrunk_sideA_ae_members_descriptions: list[str],
-    iptrunk_sideB_ae_geant_a_sid: str,
-    iptrunk_sideB_ae_members: list[str],
-    iptrunk_sideB_ae_members_descriptions: list[str],
+    side_a_ae_geant_a_sid: str,
+    side_a_ae_members: list[dict],
+    side_b_ae_geant_a_sid: str,
+    side_b_ae_members: list[dict],
 ) -> State:
     subscription.iptrunk.geant_s_sid = geant_s_sid
     subscription.iptrunk.iptrunk_description = iptrunk_description
@@ -90,13 +83,21 @@ def modify_iptrunk_subscription(
     subscription.iptrunk.iptrunk_speed = iptrunk_speed
     subscription.iptrunk.iptrunk_minimum_links = iptrunk_minimum_links
 
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid = iptrunk_sideA_ae_geant_a_sid
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members = iptrunk_sideA_ae_members
-    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members_description = iptrunk_sideA_ae_members_descriptions
-
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid = iptrunk_sideB_ae_geant_a_sid
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members = iptrunk_sideB_ae_members
-    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members_description = iptrunk_sideB_ae_members_descriptions
+    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid = side_a_ae_geant_a_sid
+    #  Flush the old list of member interfaces
+    subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members.clear()
+    #  And update the list to only include the new member interfaces
+    for member in side_a_ae_members:
+        subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members.append(
+            IptrunkInterfaceBlock.new(subscription_id=uuid4(), **member)
+        )
+
+    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid = side_b_ae_geant_a_sid
+    subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members.clear()
+    for member in side_b_ae_members:
+        subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members.append(
+            IptrunkInterfaceBlock.new(subscription_id=uuid4(), **member)
+        )
 
     subscription.description = f"IP trunk, geant_s_sid:{geant_s_sid}"
 
diff --git a/gso/workflows/iptrunk/terminate_iptrunk.py b/gso/workflows/iptrunk/terminate_iptrunk.py
index b2310d89c9d1851f168e9c2521fc0052c0c3b74c..c0a0da62cd549962164e6d9cc6afb787a7f63f79 100644
--- a/gso/workflows/iptrunk/terminate_iptrunk.py
+++ b/gso/workflows/iptrunk/terminate_iptrunk.py
@@ -11,7 +11,7 @@ from orchestrator.workflows.utils import wrap_modify_initial_input_form
 from gso.products.product_types.iptrunk import Iptrunk
 from gso.services import infoblox, provisioning_proxy
 from gso.services.provisioning_proxy import pp_interaction
-from gso.workflows.iptrunk.utils import set_isis_to_90000
+from gso.utils.helpers import set_isis_to_90000
 
 
 def initial_input_form_generator() -> FormGenerator:
diff --git a/gso/workflows/iptrunk/utils.py b/gso/workflows/iptrunk/utils.py
deleted file mode 100644
index 690cfc58b692efb143cb0caa5ef22ac081c3c36d..0000000000000000000000000000000000000000
--- a/gso/workflows/iptrunk/utils.py
+++ /dev/null
@@ -1,18 +0,0 @@
-from orchestrator import step
-from orchestrator.types import State, UUIDstr
-
-from gso.products.product_types.iptrunk import Iptrunk
-from gso.services import provisioning_proxy
-
-
-@step("[COMMIT] Set ISIS metric to 90000")
-def set_isis_to_90000(subscription: Iptrunk, process_id: UUIDstr, tt_number: str) -> State:
-    old_isis_metric = subscription.iptrunk.iptrunk_isis_metric
-    subscription.iptrunk.iptrunk_isis_metric = 90000
-    provisioning_proxy.provision_ip_trunk(subscription, process_id, tt_number, "isis_interface", False)
-
-    return {
-        "subscription": subscription,
-        "old_isis_metric": old_isis_metric,
-        "label_text": "ISIS is being set to 90K by the provisioning proxy, please wait for the results",
-    }
diff --git a/gso/workflows/router/create_router.py b/gso/workflows/router/create_router.py
index 91e7e82a3355af50e3159a1e9dc4fa524db83aff..79311e470df340386502cb94f7e13d7b67ddf901 100644
--- a/gso/workflows/router/create_router.py
+++ b/gso/workflows/router/create_router.py
@@ -11,14 +11,14 @@ from orchestrator.workflows.steps import resync, set_status, store_process_subsc
 from orchestrator.workflows.utils import wrap_create_initial_input_form
 from pydantic import validator
 
-from gso.products.product_blocks.router import RouterRole, RouterVendor, generate_fqdn
+from gso.products.product_blocks.router import PortNumber, RouterRole, RouterVendor, generate_fqdn
 from gso.products.product_types.router import RouterInactive, RouterProvisioning
 from gso.products.product_types.site import Site
-from gso.products.shared import PortNumber
 from gso.services import infoblox, provisioning_proxy, subscriptions
+from gso.services.crm import customer_selector
 from gso.services.netbox_client import NetboxClient
 from gso.services.provisioning_proxy import pp_interaction
-from gso.workflows.utils import customer_selector, iso_from_ipv4
+from gso.utils.helpers import iso_from_ipv4
 
 
 def _site_selector() -> Choice:
@@ -159,7 +159,8 @@ def provision_router_real(subscription: RouterProvisioning, process_id: UUIDstr,
 def create_netbox_device(subscription: RouterProvisioning) -> State:
     if subscription.router.router_vendor == RouterVendor.NOKIA:
         NetboxClient().create_device(
-            subscription.router.router_fqdn, subscription.router.router_site.site_tier  # type: ignore
+            subscription.router.router_fqdn,
+            str(subscription.router.router_site.site_tier),  # type: ignore[union-attr]
         )
         return {"subscription": subscription, "label_text": "Creating NetBox device"}
     return {"subscription": subscription, "label_text": "Skipping NetBox device creation for Juniper router."}
diff --git a/gso/workflows/router/terminate_router.py b/gso/workflows/router/terminate_router.py
index 4989823fc1d94ddd883654ae187753abca493f0a..47d09b414c13b94cf93a836da241fcc3c957fa39 100644
--- a/gso/workflows/router/terminate_router.py
+++ b/gso/workflows/router/terminate_router.py
@@ -57,6 +57,8 @@ def deprovision_lt_ips(subscription: Router) -> dict:
 
 @step("Remove configuration from router")
 def remove_config_from_router() -> None:
+    #  FIXME: Add actual content
+    #  TODO: update unit test accordingly
     pass
 
 
diff --git a/gso/workflows/site/create_site.py b/gso/workflows/site/create_site.py
index edcb7a01acfdf9cc5620ac4b95a27e90d4afb72d..429b0d7e489be69a980050c6b4698cddd1a1dd10 100644
--- a/gso/workflows/site/create_site.py
+++ b/gso/workflows/site/create_site.py
@@ -11,9 +11,9 @@ from orchestrator.workflows.utils import wrap_create_initial_input_form
 from pydantic import validator
 
 from gso.products.product_blocks import site as site_pb
+from gso.products.product_blocks.site import LatitudeCoordinate, LongitudeCoordinate
 from gso.products.product_types import site
-from gso.schemas.types import LatitudeCoordinate, LongitudeCoordinate
-from gso.workflows.utils import customer_selector
+from gso.services.crm import customer_selector
 
 
 def initial_input_form_generator(product_name: str) -> FormGenerator:  # noqa: C901
diff --git a/gso/workflows/tasks/import_iptrunk.py b/gso/workflows/tasks/import_iptrunk.py
index f1c3e6cf03612892b08848a6deb5b0a67a0beba3..84c12aa6667962eb1a40184384fad34d0a85ba04 100644
--- a/gso/workflows/tasks/import_iptrunk.py
+++ b/gso/workflows/tasks/import_iptrunk.py
@@ -8,12 +8,12 @@ from orchestrator.types import FormGenerator, State, SubscriptionLifecycle
 from orchestrator.workflow import StepList, done, init, step
 from orchestrator.workflows.steps import resync, set_status, store_process_subscription
 
-from gso.products.product_blocks import PhyPortCapacity
-from gso.products.product_blocks.iptrunk import IptrunkType
+from gso.products import ProductType
+from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity
 from gso.products.product_types.iptrunk import IptrunkInactive, IptrunkProvisioning
-from gso.schemas.enums import ProductType
 from gso.services import subscriptions
 from gso.services.crm import get_customer_by_name
+from gso.utils.helpers import LAGMember
 from gso.workflows.iptrunk.create_iptrunk import initialize_subscription
 
 
@@ -42,17 +42,15 @@ def initial_input_form_generator() -> FormGenerator:
         iptrunk_speed: PhyPortCapacity
         iptrunk_minimum_links: int
 
-        iptrunk_sideA_node_id: RouterEnum  # type: ignore[valid-type]
-        iptrunk_sideA_ae_iface: str
-        iptrunk_sideA_ae_geant_a_sid: str
-        iptrunk_sideA_ae_members: UniqueConstrainedList[str]
-        iptrunk_sideA_ae_members_descriptions: UniqueConstrainedList[str]
+        side_a_node_id: RouterEnum  # type: ignore[valid-type]
+        side_a_ae_iface: str
+        side_a_ae_geant_a_sid: str
+        side_a_ae_members: UniqueConstrainedList[LAGMember]
 
-        iptrunk_sideB_node_id: RouterEnum  # type: ignore[valid-type]
-        iptrunk_sideB_ae_iface: str
-        iptrunk_sideB_ae_geant_a_sid: str
-        iptrunk_sideB_ae_members: UniqueConstrainedList[str]
-        iptrunk_sideB_ae_members_descriptions: UniqueConstrainedList[str]
+        side_b_node_id: RouterEnum  # type: ignore[valid-type]
+        side_b_ae_iface: str
+        side_b_ae_geant_a_sid: str
+        side_b_ae_members: UniqueConstrainedList[LAGMember]
 
         iptrunk_ipv4_network: ipaddress.IPv4Network
         iptrunk_ipv6_network: ipaddress.IPv6Network
@@ -82,6 +80,7 @@ def update_ipam_stub_for_subscription(
 ) -> State:
     subscription.iptrunk.iptrunk_ipv4_network = iptrunk_ipv4_network
     subscription.iptrunk.iptrunk_ipv6_network = iptrunk_ipv6_network
+    subscription.iptrunk.iptrunk_ipv6_network = iptrunk_ipv6_network
 
     return {"subscription": subscription}
 
diff --git a/gso/workflows/tasks/import_router.py b/gso/workflows/tasks/import_router.py
index 0821e31414c822197cb419101562a4d74a5afaec..d7984b9264af68bed7815126bee445d29062daea 100644
--- a/gso/workflows/tasks/import_router.py
+++ b/gso/workflows/tasks/import_router.py
@@ -8,13 +8,12 @@ from orchestrator.types import FormGenerator, State, SubscriptionLifecycle
 from orchestrator.workflow import StepList, done, init, step
 from orchestrator.workflows.steps import resync, set_status, store_process_subscription
 
+from gso.products import ProductType
 from gso.products.product_blocks import router as router_pb
-from gso.products.product_blocks.router import RouterRole, RouterVendor
+from gso.products.product_blocks.router import PortNumber, RouterRole, RouterVendor
 from gso.products.product_types import router
 from gso.products.product_types.router import RouterInactive
 from gso.products.product_types.site import Site
-from gso.products.shared import PortNumber
-from gso.schemas.enums import ProductType
 from gso.services import subscriptions
 from gso.services.crm import get_customer_by_name
 
diff --git a/gso/workflows/tasks/import_site.py b/gso/workflows/tasks/import_site.py
index 20967c71c2f12e16bd78081538eb89979fb1baf1..af96fca24e6e67d0ffac5013c593eed238a16706 100644
--- a/gso/workflows/tasks/import_site.py
+++ b/gso/workflows/tasks/import_site.py
@@ -6,9 +6,9 @@ from orchestrator.types import FormGenerator, State, SubscriptionLifecycle
 from orchestrator.workflow import StepList, done, init, step, workflow
 from orchestrator.workflows.steps import resync, set_status, store_process_subscription
 
+from gso.products import ProductType
 from gso.products.product_blocks.site import SiteTier
 from gso.products.product_types.site import SiteInactive
-from gso.schemas.enums import ProductType
 from gso.services import subscriptions
 from gso.services.crm import get_customer_by_name
 from gso.workflows.site.create_site import initialize_subscription
diff --git a/pyproject.toml b/pyproject.toml
index 7836077742ec1e38f3121726e4168387137e1169..19d2278563cff9f3b1bec5aad1fef0e57e4d18ae 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -46,6 +46,8 @@ show_error_codes = true
 show_column_numbers = true
 # Suppress "note: By default the bodies of untyped functions are not checked"
 disable_error_code = "annotation-unchecked"
+# Forbid the use of a generic "type: ignore" without specifying the exact error that is ignored
+enable_error_code = "ignore-without-code"
 
 [tool.ruff]
 exclude = [
diff --git a/test-docs.sh b/test-docs.sh
index 7415bda463ac80b3ab548a0d457ea9d56d030d92..cdc50473e43e7e6c7356e89a5c994bac86f141c2 100755
--- a/test-docs.sh
+++ b/test-docs.sh
@@ -4,5 +4,5 @@ if [ ! -d ./docs/vale/styles/proselint ] || [ ! -d ./docs/vale/styles/Microsoft
   docker run -it --rm -v "$(pwd)"/docs:/docs jdkato/vale:latest --config="/docs/vale/.vale.ini" sync
 fi
 
-docker run -it --rm -v $(pwd):/gso jdkato/vale:latest --glob='!*/migrations/*' \
+docker run -it --rm -v "$(pwd)":/gso jdkato/vale:latest --glob='!*/migrations/*' \
 --config="/gso/docs/vale/.vale.ini" /gso/docs/source /gso/gso
diff --git a/test/conftest.py b/test/conftest.py
index 2bb17b7a4baf57e34ca19bfadcb70f0790cc5786..f46642a13a3babe429e5edcb3667659d65871223 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -47,6 +47,21 @@ class FakerProvider(BaseProvider):
 
         return ipaddress.IPv6Network(str(network) + "/64")
 
+    def tt_number(self) -> str:
+        random_date = self.generator.date(pattern="%Y%m%d")
+        random_int = self.generator.random_int(min=10000000, max=99999999)
+
+        return f"TT#{random_date}{random_int}"
+
+    def geant_gid(self) -> str:
+        return self.generator.numerify("GID-#####")
+
+    def geant_sid(self) -> str:
+        return self.generator.numerify("SID-#####")
+
+    def network_interface(self) -> str:
+        return self.generator.numerify("ge-@#/@#/@#")
+
 
 @pytest.fixture(scope="session")
 def faker() -> Faker:
diff --git a/test/fixtures.py b/test/fixtures.py
index 2f3ba8993da821982e2792a3b12b125e88958ce6..ec2b2bd56179f2fa6e54dfe69bfa78d21a408c08 100644
--- a/test/fixtures.py
+++ b/test/fixtures.py
@@ -5,11 +5,13 @@ from orchestrator.db import db
 from orchestrator.domain import SubscriptionModel
 from orchestrator.types import SubscriptionLifecycle, UUIDstr
 
+from gso.products import ProductType
+from gso.products.product_blocks.iptrunk import IptrunkInterfaceBlock, IptrunkSideBlock, IptrunkType, PhyPortCapacity
 from gso.products.product_blocks.router import RouterRole, RouterVendor
 from gso.products.product_blocks.site import SiteTier
-from gso.products.product_types.router import RouterInactive
+from gso.products.product_types.iptrunk import IptrunkInactive
+from gso.products.product_types.router import Router, RouterInactive
 from gso.products.product_types.site import Site, SiteInactive
-from gso.schemas.enums import ProductType
 from gso.services import subscriptions
 
 CUSTOMER_ID: UUIDstr = "2f47f65a-0911-e511-80d0-005056956c1a"
@@ -32,7 +34,7 @@ def site_subscription_factory(faker):
         site_ts_address=None,
     ) -> UUIDstr:
         description = description or "Site Subscription"
-        site_name = site_name or faker.name()
+        site_name = site_name or faker.domain_word()
         site_city = site_city or faker.city()
         site_country = site_country or faker.country()
         site_country_code = site_country_code or faker.country_code()
@@ -86,7 +88,7 @@ def router_subscription_factory(site_subscription_factory, faker):
         router_is_ias_connected=True,
     ) -> UUIDstr:
         description = description or faker.text(max_nb_chars=30)
-        router_fqdn = router_fqdn or faker.domain_name()
+        router_fqdn = router_fqdn or faker.domain_name(levels=4)
         router_ts_port = router_ts_port or faker.random_int(min=1, max=49151)
         router_access_via_ts = router_access_via_ts or faker.boolean()
         router_lo_ipv4_address = router_lo_ipv4_address or ipaddress.IPv4Address(faker.ipv4())
@@ -122,3 +124,88 @@ def router_subscription_factory(site_subscription_factory, faker):
         return str(router_subscription.subscription_id)
 
     return subscription_create
+
+
+@pytest.fixture
+def iptrunk_side_subscription_factory(router_subscription_factory, faker):
+    def subscription_create(
+        iptrunk_side_node=None,
+        iptrunk_side_ae_iface=None,
+        iptrunk_side_ae_geant_a_sid=None,
+        iptrunk_side_ae_members=None,
+        iptrunk_side_ae_members_description=None,
+    ) -> IptrunkSideBlock:
+        iptrunk_side_node_id = iptrunk_side_node or router_subscription_factory()
+        iptrunk_side_node = Router.from_subscription(iptrunk_side_node_id).router
+        iptrunk_side_ae_iface = iptrunk_side_ae_iface or faker.pystr()
+        iptrunk_side_ae_geant_a_sid = iptrunk_side_ae_geant_a_sid or faker.geant_sid()
+        iptrunk_side_ae_members = iptrunk_side_ae_members or [
+            IptrunkInterfaceBlock.new(
+                faker.uuid4(), interface_name=faker.network_interface(), interface_description=faker.sentence()
+            ),
+            IptrunkInterfaceBlock.new(
+                faker.uuid4(), interface_name=faker.network_interface(), interface_description=faker.sentence()
+            ),
+        ]
+
+        return IptrunkSideBlock.new(
+            faker.uuid4(),
+            iptrunk_side_node=iptrunk_side_node,
+            iptrunk_side_ae_iface=iptrunk_side_ae_iface,
+            iptrunk_side_ae_geant_a_sid=iptrunk_side_ae_geant_a_sid,
+            iptrunk_side_ae_members=iptrunk_side_ae_members,
+            iptrunk_side_ae_members_description=iptrunk_side_ae_members_description,
+        )
+
+    return subscription_create
+
+
+@pytest.fixture
+def iptrunk_subscription_factory(iptrunk_side_subscription_factory, faker):
+    def subscription_create(
+        description=None,
+        start_date="2023-05-24T00:00:00+00:00",
+        geant_s_sid=None,
+        iptrunk_description=None,
+        iptrunk_type=IptrunkType.DARK_FIBER,
+        iptrunk_speed=PhyPortCapacity.ONE_GIGABIT_PER_SECOND,
+        iptrunk_isis_metric=None,
+        iptrunk_ipv4_network=None,
+        iptrunk_ipv6_network=None,
+        iptrunk_sides=None,
+    ) -> UUIDstr:
+        product_id = subscriptions.get_product_id_by_name(ProductType.IP_TRUNK)
+        description = description or faker.sentence()
+
+        geant_s_sid = geant_s_sid or faker.geant_sid()
+        iptrunk_description = iptrunk_description or faker.sentence()
+        iptrunk_isis_metric = iptrunk_isis_metric or faker.pyint()
+        iptrunk_ipv4_network = iptrunk_ipv4_network or faker.ipv4_network()
+        iptrunk_ipv6_network = iptrunk_ipv6_network or faker.ipv6_network()
+        iptrunk_minimum_links = 1
+        iptrunk_side_a = iptrunk_side_subscription_factory()
+        iptrunk_side_b = iptrunk_side_subscription_factory()
+        iptrunk_sides = iptrunk_sides or [iptrunk_side_a, iptrunk_side_b]
+
+        iptrunk_subscription = IptrunkInactive.from_product_id(product_id, customer_id=CUSTOMER_ID, insync=True)
+        iptrunk_subscription.iptrunk.geant_s_sid = geant_s_sid
+        iptrunk_subscription.iptrunk.iptrunk_description = iptrunk_description
+        iptrunk_subscription.iptrunk.iptrunk_type = iptrunk_type
+        iptrunk_subscription.iptrunk.iptrunk_speed = iptrunk_speed
+        iptrunk_subscription.iptrunk.iptrunk_minimum_links = iptrunk_minimum_links
+        iptrunk_subscription.iptrunk.iptrunk_isis_metric = iptrunk_isis_metric
+        iptrunk_subscription.iptrunk.iptrunk_ipv4_network = iptrunk_ipv4_network
+        iptrunk_subscription.iptrunk.iptrunk_ipv6_network = iptrunk_ipv6_network
+        iptrunk_subscription.iptrunk.iptrunk_sides = iptrunk_sides
+
+        iptrunk_subscription = SubscriptionModel.from_other_lifecycle(
+            iptrunk_subscription, SubscriptionLifecycle.ACTIVE
+        )
+        iptrunk_subscription.description = description
+        iptrunk_subscription.start_date = start_date
+        iptrunk_subscription.save()
+        db.session.commit()
+
+        return str(iptrunk_subscription.subscription_id)
+
+    return subscription_create
diff --git a/test/imports/conftest.py b/test/imports/conftest.py
index 425a0e627a4592241e2c3f81cce910255dd34a5e..4a3e9c07a0c70ef31069d0f624c7437553562a16 100644
--- a/test/imports/conftest.py
+++ b/test/imports/conftest.py
@@ -1 +1,6 @@
-from test.fixtures import router_subscription_factory, site_subscription_factory  # noqa
+from test.fixtures import (  # noqa
+    iptrunk_side_subscription_factory,
+    iptrunk_subscription_factory,
+    router_subscription_factory,
+    site_subscription_factory,
+)
diff --git a/test/imports/test_imports.py b/test/imports/test_imports.py
index c41497c026ad36d7391542542da103b04db49519..3cdfa3ed1d5f87abd72521198220a58687b7960d 100644
--- a/test/imports/test_imports.py
+++ b/test/imports/test_imports.py
@@ -5,10 +5,10 @@ import pytest
 from orchestrator.db import SubscriptionTable
 from orchestrator.services import subscriptions
 
-from gso.products.product_blocks import PhyPortCapacity
-from gso.products.product_blocks.iptrunk import IptrunkType
+from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity
 from gso.products.product_blocks.router import RouterRole, RouterVendor
 from gso.products.product_blocks.site import SiteTier
+from gso.utils.helpers import iso_from_ipv4
 
 SITE_IMPORT_ENDPOINT = "/api/v1/imports/sites"
 ROUTER_IMPORT_ENDPOINT = "/api/v1/imports/routers"
@@ -21,21 +21,23 @@ def iptrunk_data(router_subscription_factory, faker):
     router_side_b = router_subscription_factory()
     return {
         "customer": "GÉANT",
-        "geant_s_sid": faker.pystr(),
+        "geant_s_sid": faker.geant_sid(),
         "iptrunk_type": IptrunkType.DARK_FIBER,
         "iptrunk_description": faker.sentence(),
         "iptrunk_speed": PhyPortCapacity.HUNDRED_GIGABIT_PER_SECOND,
         "iptrunk_minimum_links": 5,
-        "iptrunk_sideA_node_id": router_side_a,
-        "iptrunk_sideA_ae_iface": faker.pystr(),
-        "iptrunk_sideA_ae_geant_a_sid": faker.pystr(),
-        "iptrunk_sideA_ae_members": [faker.pystr() for _ in range(5)],
-        "iptrunk_sideA_ae_members_descriptions": [faker.sentence() for _ in range(5)],
-        "iptrunk_sideB_node_id": router_side_b,
-        "iptrunk_sideB_ae_iface": faker.pystr(),
-        "iptrunk_sideB_ae_geant_a_sid": faker.pystr(),
-        "iptrunk_sideB_ae_members": [faker.pystr() for _ in range(5)],
-        "iptrunk_sideB_ae_members_descriptions": [faker.sentence() for _ in range(5)],
+        "side_a_node_id": router_side_a,
+        "side_a_ae_iface": faker.network_interface(),
+        "side_a_ae_geant_a_sid": faker.geant_sid(),
+        "side_a_ae_members": [
+            {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5)
+        ],
+        "side_b_node_id": router_side_b,
+        "side_b_ae_iface": faker.network_interface(),
+        "side_b_ae_geant_a_sid": faker.geant_sid(),
+        "side_b_ae_members": [
+            {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5)
+        ],
         "iptrunk_ipv4_network": str(faker.ipv4_network()),
         "iptrunk_ipv6_network": str(faker.ipv6_network()),
     }
@@ -43,23 +45,23 @@ def iptrunk_data(router_subscription_factory, faker):
 
 @pytest.fixture
 def mock_routers(iptrunk_data):
-    first_call = [iptrunk_data["iptrunk_sideA_node_id"], iptrunk_data["iptrunk_sideB_node_id"], str(uuid4())]
-    side_effects = [
-        first_call,
-        first_call,
-        [
-            (iptrunk_data["iptrunk_sideA_node_id"], "iptrunk_sideA_node_id description"),
-            (iptrunk_data["iptrunk_sideB_node_id"], "iptrunk_sideB_node_id description"),
-            (str(uuid4()), "random description"),
-        ],
-    ]
     with patch("gso.services.subscriptions.get_active_router_subscriptions") as mock_get_active_router_subscriptions:
-        mock_get_active_router_subscriptions.side_effect = side_effects
+
+        def _active_router_subscriptions(*args, **kwargs):
+            if kwargs["fields"] == ["subscription_id", "description"]:
+                return [
+                    (iptrunk_data["side_a_node_id"], "side_a_node_id description"),
+                    (iptrunk_data["side_b_node_id"], "side_b_node_id description"),
+                    (str(uuid4()), "random description"),
+                ]
+            return [iptrunk_data["side_a_node_id"], iptrunk_data["side_b_node_id"], str(uuid4())]
+
+        mock_get_active_router_subscriptions.side_effect = _active_router_subscriptions
         yield mock_get_active_router_subscriptions
 
 
 @patch("gso.api.v1.imports._start_process")
-def test_import_iptrunk_successful_with_mocked_process(mock_start_process, test_client, iptrunk_data, mock_routers):
+def test_import_iptrunk_successful_with_mocked_process(mock_start_process, test_client, mock_routers, iptrunk_data):
     mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
     response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
 
@@ -70,7 +72,7 @@ def test_import_iptrunk_successful_with_mocked_process(mock_start_process, test_
 @pytest.fixture
 def site_data(faker):
     return {
-        "site_name": faker.name(),
+        "site_name": faker.domain_word(),
         "site_city": faker.city(),
         "site_country": faker.country(),
         "site_country_code": faker.country_code(),
@@ -86,6 +88,7 @@ def site_data(faker):
 
 @pytest.fixture
 def router_data(faker, site_data):
+    mock_ipv4 = faker.ipv4()
     return {
         "hostname": "127.0.0.1",
         "router_role": RouterRole.PE,
@@ -94,9 +97,9 @@ def router_data(faker, site_data):
         "ts_port": 1234,
         "customer": "GÉANT",
         "is_ias_connected": True,
-        "router_lo_ipv4_address": faker.ipv4(),
+        "router_lo_ipv4_address": mock_ipv4,
         "router_lo_ipv6_address": faker.ipv6(),
-        "router_lo_iso_address": "TestAddress",
+        "router_lo_iso_address": iso_from_ipv4(mock_ipv4),
     }
 
 
@@ -167,7 +170,7 @@ def test_import_router_endpoint_with_invalid_data(test_client, site_data, router
     assert response["detail"][1]["msg"] == "value is not a valid IPv6 address"
 
 
-def test_import_iptrunk_successful_with_real_process(test_client, iptrunk_data, mock_routers):
+def test_import_iptrunk_successful_with_real_process(test_client, mock_routers, iptrunk_data):
     response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
     assert response.status_code == 201
 
@@ -182,7 +185,7 @@ def test_import_iptrunk_successful_with_real_process(test_client, iptrunk_data,
 
 
 @patch("gso.api.v1.imports._start_process")
-def test_import_iptrunk_invalid_customer(mock_start_process, test_client, iptrunk_data, mock_routers):
+def test_import_iptrunk_invalid_customer(mock_start_process, test_client, mock_routers, iptrunk_data):
     iptrunk_data["customer"] = "not_existing_customer"
     mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
     response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
@@ -203,29 +206,14 @@ def test_import_iptrunk_invalid_router_id_side_a_and_b(mock_start_process, test_
     assert response.status_code == 422
     assert response.json() == {
         "detail": [
-            {"loc": ["body", "iptrunk_sideA_node_id"], "msg": "Router not found", "type": "value_error"},
-            {"loc": ["body", "iptrunk_sideB_node_id"], "msg": "Router not found", "type": "value_error"},
-        ]
-    }
-
-
-@patch("gso.api.v1.imports._start_process")
-def test_import_iptrunk_non_unique_members_side_a(mock_start_process, test_client, iptrunk_data, mock_routers):
-    mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
-
-    iptrunk_data["iptrunk_sideA_ae_members"] = [5, 5, 5, 5, 5]
-    iptrunk_data["iptrunk_sideB_ae_members"] = [4, 4, 4, 5, 5]
-
-    response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
-
-    assert response.status_code == 422
-    assert response.json() == {
-        "detail": [
-            {"loc": ["body", "iptrunk_sideA_ae_members"], "msg": "Items must be unique", "type": "value_error"},
-            {"loc": ["body", "iptrunk_sideB_ae_members"], "msg": "Items must be unique", "type": "value_error"},
             {
-                "loc": ["body", "__root__"],
-                "msg": "Side A members should be at least 5 (iptrunk_minimum_links)",
+                "loc": ["body", "side_a_node_id"],
+                "msg": f"Router {iptrunk_data['side_a_node_id']} not found",
+                "type": "value_error",
+            },
+            {
+                "loc": ["body", "side_b_node_id"],
+                "msg": f"Router {iptrunk_data['side_b_node_id']} not found",
                 "type": "value_error",
             },
         ]
@@ -233,36 +221,37 @@ def test_import_iptrunk_non_unique_members_side_a(mock_start_process, test_clien
 
 
 @patch("gso.api.v1.imports._start_process")
-def test_iptrunk_import_fails_on_side_a_member_count_mismatch(
-    mock_start_process, test_client, iptrunk_data, mock_routers
-):
+def test_import_iptrunk_non_unique_members_side_a(mock_start_process, test_client, mock_routers, iptrunk_data, faker):
     mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
 
-    iptrunk_data["iptrunk_sideA_ae_members"].remove(iptrunk_data["iptrunk_sideA_ae_members"][0])
+    repeat_interface_a = {"interface_name": faker.network_interface(), "interface_description": faker.sentence()}
+    repeat_interface_b = {"interface_name": faker.network_interface(), "interface_description": faker.sentence()}
+    iptrunk_data["side_a_ae_members"] = [repeat_interface_a for _ in range(5)]
+    iptrunk_data["side_b_ae_members"] = [repeat_interface_b for _ in range(5)]
 
     response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
 
     assert response.status_code == 422
     assert response.json() == {
         "detail": [
+            {"loc": ["body", "side_a_ae_members"], "msg": "Items must be unique", "type": "value_error"},
+            {"loc": ["body", "side_b_ae_members"], "msg": "Items must be unique", "type": "value_error"},
             {
                 "loc": ["body", "__root__"],
                 "msg": "Side A members should be at least 5 (iptrunk_minimum_links)",
                 "type": "value_error",
-            }
+            },
         ]
     }
 
 
 @patch("gso.api.v1.imports._start_process")
-def test_iptrunk_import_fails_on_side_a_member_description_mismatch(
-    mock_start_process, test_client, iptrunk_data, mock_routers
+def test_import_iptrunk_fails_on_side_a_member_count_mismatch(
+    mock_start_process, test_client, mock_routers, iptrunk_data
 ):
     mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
 
-    iptrunk_data["iptrunk_sideA_ae_members_descriptions"].remove(
-        iptrunk_data["iptrunk_sideA_ae_members_descriptions"][0]
-    )
+    iptrunk_data["side_a_ae_members"].remove(iptrunk_data["side_a_ae_members"][0])
 
     response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
 
@@ -271,7 +260,7 @@ def test_iptrunk_import_fails_on_side_a_member_description_mismatch(
         "detail": [
             {
                 "loc": ["body", "__root__"],
-                "msg": "Mismatch in Side A members and their descriptions",
+                "msg": "Side A members should be at least 5 (iptrunk_minimum_links)",
                 "type": "value_error",
             }
         ]
@@ -279,12 +268,12 @@ def test_iptrunk_import_fails_on_side_a_member_description_mismatch(
 
 
 @patch("gso.api.v1.imports._start_process")
-def test_iptrunk_import_fails_on_side_a_and_b_members_mismatch(
+def test_import_iptrunk_fails_on_side_a_and_b_members_mismatch(
     mock_start_process, test_client, iptrunk_data, mock_routers
 ):
     mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
 
-    iptrunk_data["iptrunk_sideB_ae_members"].remove(iptrunk_data["iptrunk_sideB_ae_members"][0])
+    iptrunk_data["side_b_ae_members"].remove(iptrunk_data["side_b_ae_members"][0])
 
     response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
 
@@ -292,27 +281,3 @@ def test_iptrunk_import_fails_on_side_a_and_b_members_mismatch(
     assert response.json() == {
         "detail": [{"loc": ["body", "__root__"], "msg": "Mismatch between Side A and B members", "type": "value_error"}]
     }
-
-
-@patch("gso.api.v1.imports._start_process")
-def test_iptrunk_import_fails_on_side_b_member_description_mismatch(
-    mock_start_process, test_client, iptrunk_data, mock_routers
-):
-    mock_start_process.return_value = "123e4567-e89b-12d3-a456-426655440000"
-
-    iptrunk_data["iptrunk_sideB_ae_members_descriptions"].remove(
-        iptrunk_data["iptrunk_sideB_ae_members_descriptions"][0]
-    )
-
-    response = test_client.post(IPTRUNK_IMPORT_API_URL, json=iptrunk_data)
-
-    assert response.status_code == 422
-    assert response.json() == {
-        "detail": [
-            {
-                "loc": ["body", "__root__"],
-                "msg": "Mismatch in Side B members and their descriptions",
-                "type": "value_error",
-            }
-        ]
-    }
diff --git a/test/schemas/test_types.py b/test/schemas/test_types.py
index dcfb515dbd315a92b205f4faa66a615f81ea0232..e5d757dbb84550df27f05949caf2e1ad78c7a2c8 100644
--- a/test/schemas/test_types.py
+++ b/test/schemas/test_types.py
@@ -1,6 +1,6 @@
 import pytest
 
-from gso.schemas.types import LatitudeCoordinate, LongitudeCoordinate
+from gso.products.product_blocks.site import LatitudeCoordinate, LongitudeCoordinate
 
 
 @pytest.mark.parametrize(
diff --git a/test/workflows/__init__.py b/test/workflows/__init__.py
index 19707ff0ff1b4e53e3309f9c3509c8e24001a9fc..876b0e3e051d8f1f1fda3739dab8983b953dc1c2 100644
--- a/test/workflows/__init__.py
+++ b/test/workflows/__init__.py
@@ -160,14 +160,14 @@ def _sanitize_input(input_data: State | list[State]) -> list[State]:
     if not isinstance(input_data, list):
         input_data = [input_data]
 
-    # We need a copy here and we want to mimic the actual code that returns a serialized version of the state
+    # We need a copy here, and we want to mimic the actual code that returns a serialized version of the state
     return cast(list[State], json_loads(json_dumps(input_data)))
 
 
 def run_workflow(workflow_key: str, input_data: State | list[State]) -> tuple[WFProcess, ProcessStat, list]:
     # ATTENTION!! This code needs to be as similar as possible to `server.services.processes.start_process`
-    # The main differences are: we use a different step log function and we don't run in
-    # a sepperate thread
+    # The main differences are: we use a different step log function, and we don't run in
+    # a separate thread
     user_data = _sanitize_input(input_data)
     user = "john.doe"
 
@@ -201,7 +201,7 @@ def run_workflow(workflow_key: str, input_data: State | list[State]) -> tuple[WF
 
 
 def resume_workflow(
-    process: ProcessStat, step_log: list[tuple[Step, WFProcess]], input_data: State
+    process: ProcessStat, step_log: list[tuple[Step, WFProcess]], input_data: State | list[State]
 ) -> tuple[WFProcess, list]:
     # ATTENTION!! This code needs to be as similar as possible to `server.services.processes.resume_process`
     # The main differences are: we use a different step log function, and we don't run in a separate thread
@@ -294,3 +294,11 @@ def run_form_generator(
         result = stop.value
 
     return forms, result
+
+
+def user_accept_and_assert_suspended(process_stat, step_log, extra_data=None):
+    extra_data = extra_data or {}
+    result, step_log = resume_workflow(process_stat, step_log, extra_data)
+    assert_suspended(result)
+
+    return result, step_log
diff --git a/test/workflows/conftest.py b/test/workflows/conftest.py
index 6e6630890466c6a1f3d1d63000eefff387927472..a4b71a738da3818674500075dd7ce910e2c17382 100644
--- a/test/workflows/conftest.py
+++ b/test/workflows/conftest.py
@@ -1,7 +1,12 @@
 import pytest
 from urllib3_mock import Responses
 
-from test.fixtures import router_subscription_factory, site_subscription_factory  # noqa
+from test.fixtures import (  # noqa
+    iptrunk_side_subscription_factory,
+    iptrunk_subscription_factory,
+    router_subscription_factory,
+    site_subscription_factory,
+)
 
 
 @pytest.fixture(autouse=True)
diff --git a/gso/schemas/__init__.py b/test/workflows/iptrunk/__init__.py
similarity index 100%
rename from gso/schemas/__init__.py
rename to test/workflows/iptrunk/__init__.py
diff --git a/test/workflows/iptrunks/iptrunks/test_create_iptrunks.py b/test/workflows/iptrunk/test_create_iptrunk.py
similarity index 78%
rename from test/workflows/iptrunks/iptrunks/test_create_iptrunks.py
rename to test/workflows/iptrunk/test_create_iptrunk.py
index cf50c2ccbf1d342816372913be351dfe73fa3c82..a8f273f77cb87602fcef887599250f0c0eea153c 100644
--- a/test/workflows/iptrunks/iptrunks/test_create_iptrunks.py
+++ b/test/workflows/iptrunk/test_create_iptrunk.py
@@ -3,13 +3,11 @@ from unittest.mock import patch
 
 import pytest
 
-from gso.products import Iptrunk
-from gso.products.product_blocks import PhyPortCapacity
-from gso.products.product_blocks.iptrunk import IptrunkType
-from gso.schemas.enums import ProductType
-from gso.services.crm import get_customer_by_name
+from gso.products import Iptrunk, ProductType
+from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity
+from gso.services.crm import customer_selector, get_customer_by_name
 from gso.services.subscriptions import get_product_id_by_name
-from gso.workflows.utils import customer_selector
+from gso.utils.helpers import LAGMember
 from test.workflows import (
     assert_aborted,
     assert_complete,
@@ -17,6 +15,7 @@ from test.workflows import (
     extract_state,
     resume_workflow,
     run_workflow,
+    user_accept_and_assert_suspended,
 )
 
 
@@ -34,7 +33,7 @@ class MockedNetboxClient:
 
     def get_available_interfaces(self):
         interfaces = []
-        for interface in range(1, 5):
+        for interface in range(5):
             interface_data = {
                 "name": f"Interface{interface}",
                 "module": {"display": f"Module{interface}"},
@@ -85,28 +84,31 @@ def input_form_wizard_data(router_subscription_factory, faker):
     router_side_b = router_subscription_factory()
 
     create_ip_trunk_step = {
-        "tt_number": faker.pystr(),
+        "tt_number": faker.tt_number(),
         "customer": getattr(customer_selector(), get_customer_by_name("GÉANT")["id"]),
-        "geant_s_sid": faker.pystr(),
+        "geant_s_sid": faker.geant_sid(),
         "iptrunk_type": IptrunkType.DARK_FIBER,
         "iptrunk_description": faker.sentence(),
         "iptrunk_speed": PhyPortCapacity.HUNDRED_GIGABIT_PER_SECOND,
         "iptrunk_minimum_links": 2,
     }
-    create_ip_trunk_side_a_router_name = {"iptrunk_sideA_node_id": router_side_a}
+    create_ip_trunk_side_a_router_name = {"side_a_node_id": router_side_a}
     create_ip_trunk_side_a_step = {
-        "iptrunk_sideA_ae_iface": "LAG1",
-        "iptrunk_sideA_ae_geant_a_sid": faker.pystr(),
-        "iptrunk_sideA_ae_members": ["Interface1", "Interface2"],
-        "iptrunk_sideA_ae_members_descriptions": ["Interface1 Description", "Interface2 Description"],
+        "side_a_ae_iface": "LAG1",
+        "side_a_ae_geant_a_sid": faker.geant_sid(),
+        "side_a_ae_members": [
+            LAGMember(interface_name=f"Interface{interface}", interface_description=faker.sentence())
+            for interface in range(5)
+        ],
     }
-
-    create_ip_trunk_side_b_router_name = {"iptrunk_sideB_node_id": router_side_b}
+    create_ip_trunk_side_b_router_name = {"side_b_node_id": router_side_b}
     create_ip_trunk_side_b_step = {
-        "iptrunk_sideB_ae_iface": "LAG1",
-        "iptrunk_sideB_ae_geant_a_sid": faker.pystr(),
-        "iptrunk_sideB_ae_members": ["Interface1", "Interface2"],
-        "iptrunk_sideB_ae_members_descriptions": ["Interface1 Description", "Interface2 Description"],
+        "side_b_ae_iface": "LAG4",
+        "side_b_ae_geant_a_sid": faker.geant_sid(),
+        "side_b_ae_members": [
+            LAGMember(interface_name=f"Interface{interface}", interface_description=faker.sentence())
+            for interface in range(5)
+        ],
     }
 
     return [
@@ -118,14 +120,6 @@ def input_form_wizard_data(router_subscription_factory, faker):
     ]
 
 
-def _user_accept_and_assert_suspended(process_stat, step_log, extra_data=None):
-    extra_data = extra_data or {}
-    result, step_log = resume_workflow(process_stat, step_log, extra_data)
-    assert_suspended(result)
-
-    return result, step_log
-
-
 @pytest.mark.workflow
 @patch("gso.workflows.iptrunk.create_iptrunk.provisioning_proxy.check_ip_trunk")
 @patch("gso.workflows.iptrunk.create_iptrunk.provisioning_proxy.provision_ip_trunk")
@@ -159,10 +153,10 @@ def test_successful_iptrunk_creation_with_standard_lso_result(
         "confirm": "ACCEPTED",
     }
     for _ in range(5):
-        result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, standard_lso_result)
-        result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, [{}, {}])
+        result, step_log = user_accept_and_assert_suspended(process_stat, step_log, standard_lso_result)
+        result, step_log = user_accept_and_assert_suspended(process_stat, step_log, [{}, {}])
 
-    result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, standard_lso_result)
+    result, step_log = user_accept_and_assert_suspended(process_stat, step_log, standard_lso_result)
     result, step_log = resume_workflow(process_stat, step_log, [{}, {}])
     assert_complete(result)
 
@@ -213,10 +207,10 @@ def test_iptrunk_creation_fails_when_lso_return_code_is_one(
 
     attempts = 3
     for _ in range(0, attempts - 1):
-        result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, standard_lso_result)
-        result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, [{}, {}])
+        result, step_log = user_accept_and_assert_suspended(process_stat, step_log, standard_lso_result)
+        result, step_log = user_accept_and_assert_suspended(process_stat, step_log, [{}, {}])
 
-    result, step_log = _user_accept_and_assert_suspended(process_stat, step_log, standard_lso_result)
+    result, step_log = user_accept_and_assert_suspended(process_stat, step_log, standard_lso_result)
     result, step_log = resume_workflow(process_stat, step_log, [{}, {}])
     assert_aborted(result)
 
diff --git a/test/workflows/iptrunk/test_modify_isis_metric.py b/test/workflows/iptrunk/test_modify_isis_metric.py
new file mode 100644
index 0000000000000000000000000000000000000000..521d6d2739f821b1d923aea06966736cb4722a4e
--- /dev/null
+++ b/test/workflows/iptrunk/test_modify_isis_metric.py
@@ -0,0 +1,58 @@
+from unittest.mock import patch
+
+import pytest
+
+from gso.products import Iptrunk
+from test.workflows import (
+    assert_complete,
+    assert_suspended,
+    extract_state,
+    resume_workflow,
+    run_workflow,
+    user_accept_and_assert_suspended,
+)
+
+
+@pytest.mark.workflow
+@patch("gso.workflows.iptrunk.modify_isis_metric.provisioning_proxy.provision_ip_trunk")
+def test_iptrunk_modify_isis_metric_success(
+    mock_provision_ip_trunk,
+    iptrunk_subscription_factory,
+    faker,
+):
+    #  Set up mock return values
+    product_id = iptrunk_subscription_factory()
+    new_isis_metric = faker.pyint()
+
+    #  Run workflow
+    initial_iptrunk_data = [
+        {"subscription_id": product_id},
+        {"tt_number": faker.tt_number(), "isis_metric": new_isis_metric},
+    ]
+    result, process_stat, step_log = run_workflow("modify_isis_metric", initial_iptrunk_data)
+    assert_suspended(result)
+
+    lso_return = {
+        "pp_run_results": {
+            "status": "ok",
+            "job_id": faker.uuid4(),
+            "output": "parsed_output",
+            "return_code": 0,
+        },
+        "confirm": "ACCEPTED",
+    }
+
+    result, step_log = user_accept_and_assert_suspended(process_stat, step_log, lso_return)
+    result, step_log = user_accept_and_assert_suspended(process_stat, step_log, [{}, {}])
+    result, step_log = user_accept_and_assert_suspended(process_stat, step_log, lso_return)
+    result, step_log = resume_workflow(process_stat, step_log, [{}, {}])
+
+    assert_complete(result)
+
+    state = extract_state(result)
+    subscription_id = state["subscription_id"]
+    subscription = Iptrunk.from_subscription(subscription_id)
+
+    assert "active" == subscription.status
+    assert mock_provision_ip_trunk.call_count == 2
+    assert subscription.iptrunk.iptrunk_isis_metric == new_isis_metric
diff --git a/test/workflows/iptrunk/test_modify_trunk_interface.py b/test/workflows/iptrunk/test_modify_trunk_interface.py
new file mode 100644
index 0000000000000000000000000000000000000000..8f39c4d7f784be6c3beea80c3741f7d856271c20
--- /dev/null
+++ b/test/workflows/iptrunk/test_modify_trunk_interface.py
@@ -0,0 +1,117 @@
+from unittest.mock import patch
+
+import pytest
+
+from gso.products import Iptrunk
+from gso.products.product_blocks.iptrunk import IptrunkType, PhyPortCapacity
+from test.workflows import (
+    assert_complete,
+    assert_suspended,
+    extract_state,
+    resume_workflow,
+    run_workflow,
+    user_accept_and_assert_suspended,
+)
+
+
+@pytest.mark.workflow
+@patch("gso.workflows.iptrunk.modify_trunk_interface.provisioning_proxy.provision_ip_trunk")
+def test_iptrunk_modify_trunk_interface_success(
+    mock_provision_ip_trunk,
+    iptrunk_subscription_factory,
+    faker,
+):
+    #  Set up mock return values
+    product_id = iptrunk_subscription_factory()
+    new_sid = faker.geant_sid()
+    new_description = faker.sentence()
+    new_type = IptrunkType.LEASED
+    new_speed = PhyPortCapacity.FOUR_HUNDRED_GIGABIT_PER_SECOND
+    new_link_count = 2
+
+    new_side_a_sid = faker.geant_sid()
+    new_side_a_ae_members = [
+        {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5)
+    ]
+
+    new_side_b_sid = faker.geant_sid()
+    new_side_b_ae_members = [
+        {"interface_name": faker.network_interface(), "interface_description": faker.sentence()} for _ in range(5)
+    ]
+
+    #  Run workflow
+    initial_iptrunk_data = [
+        {"subscription_id": product_id},
+        {
+            "tt_number": faker.tt_number(),
+            "geant_s_sid": new_sid,
+            "iptrunk_description": new_description,
+            "iptrunk_type": new_type,
+            "iptrunk_speed": new_speed,
+            "iptrunk_minimum_links": new_link_count,
+        },
+        {
+            "side_a_ae_geant_a_sid": new_side_a_sid,
+            "side_a_ae_members": new_side_a_ae_members,
+        },
+        {
+            "side_b_ae_geant_a_sid": new_side_b_sid,
+            "side_b_ae_members": new_side_b_ae_members,
+        },
+    ]
+
+    result, process_stat, step_log = run_workflow("modify_trunk_interface", initial_iptrunk_data)
+    assert_suspended(result)
+
+    lso_return = {
+        "pp_run_results": {
+            "status": "ok",
+            "job_id": faker.uuid4(),
+            "output": "parsed_output",
+            "return_code": 0,
+        },
+        "confirm": "ACCEPTED",
+    }
+
+    result, step_log = user_accept_and_assert_suspended(process_stat, step_log, lso_return)
+    result, step_log = user_accept_and_assert_suspended(process_stat, step_log, [{}, {}])
+    result, step_log = user_accept_and_assert_suspended(process_stat, step_log, lso_return)
+    result, step_log = resume_workflow(process_stat, step_log, [{}, {}])
+
+    assert_complete(result)
+
+    state = extract_state(result)
+    subscription_id = state["subscription_id"]
+    subscription = Iptrunk.from_subscription(subscription_id)
+
+    assert "active" == subscription.status
+    assert mock_provision_ip_trunk.call_count == 2
+
+    # Assert all subscription properties have been updated correctly
+    assert subscription.description == f"IP trunk, geant_s_sid:{new_sid}"
+    assert subscription.iptrunk.geant_s_sid == new_sid
+    assert subscription.iptrunk.iptrunk_description == new_description
+    assert subscription.iptrunk.iptrunk_type == new_type
+    assert subscription.iptrunk.iptrunk_speed == new_speed
+    assert subscription.iptrunk.iptrunk_minimum_links == new_link_count
+    assert subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_geant_a_sid == new_side_a_sid
+
+    def _find_interface_by_name(interfaces: list[dict[str, str]], name: str):
+        for interface in interfaces:
+            if interface["interface_name"] == name:
+                return interface
+        raise IndexError(f"Interface {name} not found!")
+
+    for member in subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members:
+        assert (
+            member.interface_description
+            == _find_interface_by_name(new_side_a_ae_members, member.interface_name)["interface_description"]
+        )
+
+    assert subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_geant_a_sid == new_side_b_sid
+
+    for member in subscription.iptrunk.iptrunk_sides[1].iptrunk_side_ae_members:
+        assert (
+            member.interface_description
+            == _find_interface_by_name(new_side_b_ae_members, member.interface_name)["interface_description"]
+        )
diff --git a/test/workflows/iptrunk/test_terminate_iptrunk.py b/test/workflows/iptrunk/test_terminate_iptrunk.py
new file mode 100644
index 0000000000000000000000000000000000000000..2234f29f643d39268b483d742b8e3be4822f0dca
--- /dev/null
+++ b/test/workflows/iptrunk/test_terminate_iptrunk.py
@@ -0,0 +1,65 @@
+from unittest.mock import patch
+
+import pytest
+
+from gso.products import Iptrunk
+from test.workflows import (
+    assert_complete,
+    assert_suspended,
+    extract_state,
+    resume_workflow,
+    run_workflow,
+    user_accept_and_assert_suspended,
+)
+
+
+@pytest.mark.workflow
+@patch("gso.workflows.iptrunk.terminate_iptrunk.provisioning_proxy.provision_ip_trunk")
+@patch("gso.workflows.iptrunk.terminate_iptrunk.provisioning_proxy.deprovision_ip_trunk")
+@patch("gso.workflows.iptrunk.terminate_iptrunk.infoblox.delete_network")
+def test_iptrunk_modify_isis_metric_success(
+    mock_infoblox_delete_network,
+    mock_deprovision_ip_trunk,
+    mock_provision_ip_trunk,
+    iptrunk_subscription_factory,
+    faker,
+):
+    #  Set up mock return values
+    product_id = iptrunk_subscription_factory()
+
+    #  Run workflow
+    initial_iptrunk_data = [
+        {"subscription_id": product_id},
+        {"tt_number": faker.tt_number(), "remove_configuration": True, "clean_up_ipam": True},
+    ]
+    result, process_stat, step_log = run_workflow("terminate_iptrunk", initial_iptrunk_data)
+    assert_suspended(result)
+
+    lso_return = {
+        "pp_run_results": {
+            "status": "ok",
+            "job_id": faker.uuid4(),
+            "output": "parsed_output",
+            "return_code": 0,
+        },
+        "confirm": "ACCEPTED",
+    }
+
+    for _ in range(2):
+        result, step_log = user_accept_and_assert_suspended(process_stat, step_log, lso_return)
+        result, step_log = user_accept_and_assert_suspended(process_stat, step_log, [{}, {}])
+
+    result, step_log = user_accept_and_assert_suspended(process_stat, step_log, lso_return)
+    result, step_log = resume_workflow(process_stat, step_log, [{}, {}])
+
+    assert_complete(result)
+
+    state = extract_state(result)
+    subscription_id = state["subscription_id"]
+    subscription = Iptrunk.from_subscription(subscription_id)
+
+    assert "terminated" == subscription.status
+    assert mock_provision_ip_trunk.call_count == 1
+    assert mock_deprovision_ip_trunk.call_count == 2
+    assert mock_infoblox_delete_network.call_count == 2
+    assert subscription.iptrunk.iptrunk_isis_metric == 90000
diff --git a/test/workflows/router/__init__.py b/test/workflows/router/__init__.py
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/test/workflows/router/test_create_router.py b/test/workflows/router/test_create_router.py
new file mode 100644
index 0000000000000000000000000000000000000000..5547dc6571bf0979e22920f67c9a0fa375d5a1da
--- /dev/null
+++ b/test/workflows/router/test_create_router.py
@@ -0,0 +1,228 @@
+from unittest.mock import patch
+
+import pytest
+from infoblox_client import objects
+
+from gso.products import ProductType, Site
+from gso.products.product_blocks.router import RouterRole, RouterVendor
+from gso.products.product_types.router import Router
+from gso.services.crm import customer_selector, get_customer_by_name
+from gso.services.subscriptions import get_product_id_by_name
+from test.workflows import (
+    assert_aborted,
+    assert_complete,
+    assert_suspended,
+    extract_state,
+    resume_workflow,
+    run_workflow,
+    user_accept_and_assert_suspended,
+)
+
+
+@pytest.fixture
+def router_creation_input_form_data(site_subscription_factory, faker):
+    router_site = site_subscription_factory()
+
+    return {
+        "tt_number": faker.tt_number(),
+        "customer": getattr(customer_selector(), get_customer_by_name("GÉANT")["id"]),
+        "router_site": router_site,
+        "hostname": faker.pystr(),
+        "ts_port": faker.pyint(),
+        "router_vendor": RouterVendor.NOKIA,
+        "router_role": faker.random_choices(elements=(RouterRole.P, RouterRole.PE, RouterRole.AMT), length=1)[0],
+        "is_ias_connected": True,
+    }
+
+
+@pytest.mark.workflow
+@patch("gso.workflows.router.create_router.provisioning_proxy.provision_router")
+@patch("gso.workflows.router.create_router.NetboxClient.create_device")
+@patch("gso.workflows.router.create_router.infoblox.hostname_available")
+@patch("gso.workflows.router.create_router.infoblox.find_network_by_cidr")
+@patch("gso.workflows.router.create_router.infoblox.find_host_by_fqdn")
+@patch("gso.workflows.router.create_router.infoblox.allocate_v6_network")
+@patch("gso.workflows.router.create_router.infoblox.allocate_v4_network")
+@patch("gso.workflows.router.create_router.infoblox.allocate_host")
+def test_create_router_success(
+    mock_allocate_host,
+    mock_allocate_v4_network,
+    mock_allocate_v6_network,
+    mock_find_host_by_fqdn,
+    mock_find_network_by_cidr,
+    mock_hostname_available,
+    mock_netbox_create_device,
+    mock_provision_router,
+    router_creation_input_form_data,
+    faker,
+):
+    #  Set up mock return values
+    product_id = get_product_id_by_name(ProductType.ROUTER)
+    mock_site = Site.from_subscription(router_creation_input_form_data["router_site"]).site
+    mock_v4 = faker.ipv4()
+    mock_v4_net = faker.ipv4_network()
+    mock_v6 = faker.ipv6()
+    mock_fqdn = (
+        f"{router_creation_input_form_data['hostname']}.{mock_site.site_name.lower()}."
+        f"{mock_site.site_country_code.lower()}.geant.net"
+    )
+    mock_hostname_available.return_value = True
+    mock_allocate_host.return_value = str(mock_v4), str(mock_v6)
+    mock_allocate_v4_network.return_value = mock_v4_net
+    mock_allocate_v6_network.return_value = faker.ipv6_network()
+
+    #  Run workflow
+    initial_router_data = [{"product": product_id}, router_creation_input_form_data]
+    result, process_stat, step_log = run_workflow("create_router", initial_router_data)
+    assert_suspended(result)
+
+    state = extract_state(result)
+    subscription_id = state["subscription_id"]
+    mock_find_host_by_fqdn.return_value = objects.HostRecord(
+        connector=None,
+        aliases=[mock_fqdn],
+        comment=subscription_id,
+        ipv4addrs=[
+            objects.IPv4(
+                ipv4addr=str(mock_v4),
+                configure_for_dhcp=False,
+                mac="00:00:00:00:00:00",
+                ip=str(mock_v4),
+                host=f"lo0.{mock_fqdn}",
+            )
+        ],
+        name=mock_fqdn,
+    )
+    mock_find_network_by_cidr.return_value = objects.NetworkV4(
+        connector=None,
+        comment=subscription_id,
+        network=str(mock_v4_net),
+        network_view="default",
+        cidr=str(mock_v4_net),
+    )
+
+    lso_return = {
+        "pp_run_results": {
+            "status": "ok",
+            "job_id": faker.uuid4(),
+            "output": "parsed_output",
+            "return_code": 0,
+        },
+        "confirm": "ACCEPTED",
+    }
+
+    result, step_log = user_accept_and_assert_suspended(process_stat, step_log, lso_return)
+    result, step_log = user_accept_and_assert_suspended(process_stat, step_log, [{}, {}])
+    result, step_log = user_accept_and_assert_suspended(process_stat, step_log, lso_return)
+    result, step_log = resume_workflow(process_stat, step_log, [{}, {}])
+
+    assert_complete(result)
+
+    state = extract_state(result)
+    subscription = Router.from_subscription(subscription_id)
+
+    assert "active" == subscription.status
+    assert subscription.description == f"Router {mock_fqdn}"
+
+    assert mock_provision_router.call_count == 2
+    assert mock_netbox_create_device.call_count == 1
+    assert mock_find_host_by_fqdn.call_count == 1
+    assert mock_find_network_by_cidr.call_count == 3
+    for error in ["ipam_warning", "ipam_si_warning", "ipam_ias_lt_ipv4_warning", "ipam_ias_lt_ipv6_warning"]:
+        assert error not in state
+
+
+@pytest.mark.workflow
+@patch("gso.workflows.router.create_router.provisioning_proxy.provision_router")
+@patch("gso.workflows.router.create_router.NetboxClient.create_device")
+@patch("gso.workflows.router.create_router.infoblox.hostname_available")
+@patch("gso.workflows.router.create_router.infoblox.find_network_by_cidr")
+@patch("gso.workflows.router.create_router.infoblox.find_host_by_fqdn")
+@patch("gso.workflows.router.create_router.infoblox.allocate_v6_network")
+@patch("gso.workflows.router.create_router.infoblox.allocate_v4_network")
+@patch("gso.workflows.router.create_router.infoblox.allocate_host")
+def test_create_router_lso_failure(
+    mock_allocate_host,
+    mock_allocate_v4_network,
+    mock_allocate_v6_network,
+    mock_find_host_by_fqdn,
+    mock_find_network_by_cidr,
+    mock_hostname_available,
+    mock_netbox_create_device,
+    mock_provision_router,
+    router_creation_input_form_data,
+    faker,
+):
+    #  Set up mock return values
+    mock_site = Site.from_subscription(router_creation_input_form_data["router_site"]).site
+    mock_v4 = faker.ipv4()
+    mock_v4_net = faker.ipv4_network()
+    mock_v6 = faker.ipv6()
+    mock_fqdn = (
+        f"{router_creation_input_form_data['hostname']}.{mock_site.site_name.lower()}."
+        f"{mock_site.site_country_code.lower()}.geant.net"
+    )
+    mock_hostname_available.return_value = True
+    mock_allocate_host.return_value = str(mock_v4), str(mock_v6)
+    mock_allocate_v4_network.return_value = mock_v4_net
+    mock_allocate_v6_network.return_value = faker.ipv6_network()
+    mock_find_host_by_fqdn.return_value = objects.HostRecord(
+        connector=None,
+        aliases=[mock_fqdn],
+        comment=faker.sentence(),
+        ipv4addrs=[
+            objects.IPv4(
+                ipv4addr=str(mock_v4),
+                configure_for_dhcp=False,
+                mac="00:00:00:00:00:00",
+                ip=str(mock_v4),
+                host=f"lo0.{mock_fqdn}",
+            )
+        ],
+        name=mock_fqdn,
+    )
+    mock_find_network_by_cidr.return_value = objects.NetworkV4(
+        connector=None,
+        comment=faker.sentence(),
+        network=str(mock_v4_net),
+        network_view="default",
+        cidr=str(mock_v4_net),
+    )
+
+    #  Run workflow
+    product_id = get_product_id_by_name(ProductType.ROUTER)
+    initial_router_data = [{"product": product_id}, router_creation_input_form_data]
+    result, process_stat, step_log = run_workflow("create_router", initial_router_data)
+    assert_suspended(result)
+
+    lso_return = {
+        "pp_run_results": {
+            "status": "failure",
+            "job_id": faker.uuid4(),
+            "output": "parsed_output",
+            "return_code": 1,
+        },
+        "confirm": "ACCEPTED",
+    }
+
+    attempts = 3
+    for _ in range(attempts - 1):
+        result, step_log = user_accept_and_assert_suspended(process_stat, step_log, lso_return)
+        result, step_log = user_accept_and_assert_suspended(process_stat, step_log, [{}, {}])
+
+    result, step_log = user_accept_and_assert_suspended(process_stat, step_log, lso_return)
+    result, step_log = resume_workflow(process_stat, step_log, [{}, {}])
+
+    assert_aborted(result)
+
+    state = extract_state(result)
+    subscription_id = state["subscription_id"]
+    subscription = Router.from_subscription(subscription_id)
+
+    assert "provisioning" == subscription.status
+    assert subscription.description == f"Router {mock_fqdn}"
+
+    assert mock_provision_router.call_count == attempts
+    assert mock_netbox_create_device.call_count == 0
+    assert mock_find_host_by_fqdn.call_count == 0
+    assert mock_find_network_by_cidr.call_count == 0
diff --git a/test/workflows/router/test_terminate_router.py b/test/workflows/router/test_terminate_router.py
new file mode 100644
index 0000000000000000000000000000000000000000..e580316ba86df856c9bb726c37cd70ac34f95436
--- /dev/null
+++ b/test/workflows/router/test_terminate_router.py
@@ -0,0 +1,41 @@
+from unittest.mock import patch
+
+import pytest
+
+from gso.products import Router
+from test.workflows import assert_complete, extract_state, run_workflow
+
+
+@pytest.fixture
+def router_termination_input_form_data(site_subscription_factory, faker):
+    return {"tt_number": faker.tt_number(), "remove_configuration": True, "clean_up_ipam": True}
+
+
+@pytest.mark.workflow
+@patch("gso.workflows.router.terminate_router.NetboxClient.delete_device")
+@patch("gso.workflows.router.terminate_router.infoblox.delete_host_by_ip")
+@patch("gso.workflows.router.terminate_router.infoblox.delete_network")
+def test_terminate_router_success(
+    mock_delete_network,
+    mock_delete_host_by_ip,
+    mock_delete_device,
+    router_termination_input_form_data,
+    router_subscription_factory,
+    faker,
+):
+    #  Set up active subscription in database
+    product_id = router_subscription_factory()
+
+    #  Run workflow
+    initial_router_data = [{"subscription_id": product_id}, router_termination_input_form_data]
+    result, process_stat, step_log = run_workflow("terminate_router", initial_router_data)
+    assert_complete(result)
+
+    state = extract_state(result)
+    subscription_id = state["subscription_id"]
+    subscription = Router.from_subscription(subscription_id)
+
+    assert "terminated" == subscription.status
+    assert mock_delete_network.call_count == 3
+    assert mock_delete_device.call_count == 1
+    assert mock_delete_host_by_ip.call_count == 1
diff --git a/test/workflows/site/test_create_site.py b/test/workflows/site/test_create_site.py
index da25a2b09f0e3183b3f85f69c75f4fe7c146f2ce..20486ec23bc169bede53bd6668de2b14c4761d28 100644
--- a/test/workflows/site/test_create_site.py
+++ b/test/workflows/site/test_create_site.py
@@ -1,8 +1,8 @@
 import pytest
 
+from gso.products import ProductType
 from gso.products.product_blocks.site import SiteTier
 from gso.products.product_types.site import Site
-from gso.schemas.enums import ProductType
 from gso.services.crm import get_customer_by_name
 from gso.services.subscriptions import get_product_id_by_name
 from test.workflows import assert_complete, extract_state, run_workflow
diff --git a/tox.ini b/tox.ini
index 09bbbd4b7dc3d796a57232be610f66876eb46c99..f79b0ea3fca0500ac30d3c9cd9f21a04d453b645 100644
--- a/tox.ini
+++ b/tox.ini
@@ -7,6 +7,9 @@ select = B,C,D,E,F,G,I,N,S,T,W,B902,B903,R
 max-line-length = 120
 ban-relative-imports = true
 
+[pytest]
+markers = workflow
+
 [testenv]
 passenv = DATABASE_URI_TEST,SKIP_ALL_TESTS
 deps =
@@ -26,8 +29,7 @@ commands =
     mypy .
     flake8
     coverage erase
-    coverage run --source gso -m pytest {posargs}
+    coverage run --source gso --omit="gso/migrations/*" -m pytest {posargs}
     coverage xml
     coverage html
-    # coverage report --fail-under 80
-    coverage report
+    coverage report --fail-under 80