diff --git a/Changelog.md b/Changelog.md
index b72e4597776f75b063a4441e65140efd94609b0a..5d26dcf751d10574727b67aae2a73f24f994b473 100644
--- a/Changelog.md
+++ b/Changelog.md
@@ -1,5 +1,15 @@
 # Changelog
 
+## [2.45] - 2025-04-02
+- Add email notifications to Kentik-related steps in workflows.
+- Improve Kentik license handling in Router termination workflow.
+- Only run prefix list validation workflow on GÉANT IP subscriptions.
+- Prefix list validation workflow no longer validates Juniper routers.
+- Fix a type hint in the OpenGear import model.
+- Add GA-ID to the Edge Port creation summary view.
+- Add the possibility to update a trunk suffix in the IP trunk modification workflow.
+- Fix a bug in the VRF modification workflow.
+
 ## [2.44] - 2025-03-21
 - Refactor the product model for Layer 2 circuits.
 - Merge prefix list deployment and validation workflows into one.
diff --git a/gso/oss-params-example.json b/gso/oss-params-example.json
index 5c8134c56a4f3c82e07dec89adeddbcc2380805e..ea85bf24e4a64aeb2b5b5ce3203bf0c0e70011b5 100644
--- a/gso/oss-params-example.json
+++ b/gso/oss-params-example.json
@@ -95,7 +95,8 @@
     "starttls_enabled": true,
     "smtp_username": "username",
     "smtp_password": "password",
-    "notification_email_destinations": "oc@nren.local, neteng@nren.local, ceo@nren.local"
+    "notification_email_destinations": "oc@nren.local, neteng@nren.local, ceo@nren.local",
+    "kentik_email_destinations": "service-management-team@nren.local, operations-team@nren.local"
   },
   "SHAREPOINT": {
     "client_id": "UUID",
diff --git a/gso/products/__init__.py b/gso/products/__init__.py
index a1f5c19360301a18c539c1472d6c8fb3c7f8b535..2d73ad8981b36080b6341b6e66e9709596a0b889 100644
--- a/gso/products/__init__.py
+++ b/gso/products/__init__.py
@@ -103,7 +103,7 @@ class ProductType(strEnum):
     IMPORTED_SUPER_POP_SWITCH = ImportedSuperPopSwitch.__name__
     IMPORTED_OFFICE_ROUTER = ImportedOfficeRouter.__name__
     OPENGEAR = Opengear.__name__
-    IMPORTED_OPENGEAR = Opengear.__name__
+    IMPORTED_OPENGEAR = ImportedOpengear.__name__
     EDGE_PORT = EdgePort.__name__
     IMPORTED_EDGE_PORT = ImportedEdgePort.__name__
     GEANT_IP = L3_CORE_SERVICE_PRODUCT_TYPE
diff --git a/gso/services/kentik_client.py b/gso/services/kentik_client.py
index 86e81a0c418d251e9c385b8273706388f1f9efc6..0b9d5174336d078d0708559ba3270e667f6584e5 100644
--- a/gso/services/kentik_client.py
+++ b/gso/services/kentik_client.py
@@ -84,6 +84,8 @@ class KentikClient:
         devices = self.get_devices()
         for device in devices:
             if device["device_name"] == device_name:
+                device.pop("custom_columns", None)
+                device.pop("custom_column_data", None)
                 return device
 
         return {}
diff --git a/gso/services/mailer.py b/gso/services/mailer.py
index b6344b830f90a22e8237241207a1d6e861a21466..15703849bf26484777de5e7207e2431951a79991 100644
--- a/gso/services/mailer.py
+++ b/gso/services/mailer.py
@@ -1,13 +1,16 @@
 """The mailer service sends notification emails, as part of workflows that require interaction with external parties."""
 
+import logging
 import smtplib
 from email.message import EmailMessage
 from ssl import create_default_context
 
 from gso.settings import load_oss_params
 
+logger = logging.getLogger(__name__)
 
-def send_mail(subject: str, body: str) -> None:
+
+def send_mail(subject: str, body: str, *, destination: str | None = None) -> None:
     """Send an email message to the given addresses.
 
     Only supports STARTTLS, not SSL.
@@ -15,11 +18,12 @@ def send_mail(subject: str, body: str) -> None:
     Args:
         subject: The email subject.
         body: The contents of the email message.
+        destination: The destination of the email, optional.
     """
     email_params = load_oss_params().EMAIL
     msg = EmailMessage()
     msg["From"] = email_params.from_address
-    msg["To"] = email_params.notification_email_destinations
+    msg["To"] = destination or email_params.notification_email_destinations
     msg["Subject"] = subject
     msg.set_content(body)
 
@@ -30,3 +34,11 @@ def send_mail(subject: str, body: str) -> None:
         if email_params.smtp_username and email_params.smtp_password:
             s.login(email_params.smtp_username, email_params.smtp_password)
         s.send_message(msg)
+
+        logger.info({
+            "event": "Sent an email",
+            "from": msg["From"],
+            "to": msg["To"],
+            "subject": msg["Subject"],
+            "body": body,
+        })
diff --git a/gso/services/subscriptions.py b/gso/services/subscriptions.py
index 9f3623b723969e5d7caef5488db951b9e4af0c4a..182e54a9783f28b3b286a729c87f17d5fb93fd42 100644
--- a/gso/services/subscriptions.py
+++ b/gso/services/subscriptions.py
@@ -178,10 +178,7 @@ def get_trunks_that_terminate_on_router(
     return (
         query_in_use_by_subscriptions(UUID(subscription_id))
         .join(ProductTable)
-        .filter(
-            ProductTable.product_type == ProductType.IP_TRUNK,
-            SubscriptionTable.status == lifecycle_state,
-        )
+        .filter(and_(ProductTable.product_type == ProductType.IP_TRUNK, SubscriptionTable.status == lifecycle_state))
         .all()
     )
 
@@ -210,6 +207,36 @@ def get_active_l3_services_linked_to_edge_port(edge_port_id: UUIDstr) -> list[Su
     return [SubscriptionModel.from_subscription(result.subscription_id) for result in results]
 
 
+def get_active_layer_3_services_on_router(subscription_id: UUID) -> list[SubscriptionModel]:
+    """Get all active Layer 3 services that insist on a given router `subscription_id`.
+
+    TODO: Update this method when refactoring layer 3 services.
+
+    Args:
+        subscription_id: Subscription ID of a Router.
+
+    Returns:
+        A list of Router subscriptions.
+    """
+    active_edge_ports = (
+        query_in_use_by_subscriptions(subscription_id)
+        .join(ProductTable)
+        .filter(
+            and_(
+                ProductTable.product_type == ProductType.EDGE_PORT,
+                SubscriptionTable.status == SubscriptionLifecycle.ACTIVE,
+            )
+        )
+        .all()
+    )
+
+    active_l3_services = []
+    for edge_port in active_edge_ports:
+        active_l3_services.extend(get_active_l3_services_linked_to_edge_port(str(edge_port.subscription_id)))
+
+    return active_l3_services
+
+
 def get_active_l2_circuit_services_linked_to_edge_port(edge_port_id: UUIDstr) -> list[SubscriptionModel]:
     """Retrieve all active l2 circuit services that are on top of the given edge port."""
     results = (
diff --git a/gso/settings.py b/gso/settings.py
index fbd4286411019fe56be9dedfcb615a86dcbbfc81..bf5dd53e1793d7f67e2f5b6ca6fa0eb935c69411 100644
--- a/gso/settings.py
+++ b/gso/settings.py
@@ -171,6 +171,8 @@ class EmailParams(BaseSettings):
     Attributes:
         notification_email_destinations: List of email addresses that should receive notifications when validation of a
             subscription fails. Can be a comma-separated list of multiple addresses.
+        kentik_email_destinations: A List of email addresses formatted similarly, but for notifications related to
+            Kentik.
     """
 
     from_address: EmailStr
@@ -180,6 +182,7 @@ class EmailParams(BaseSettings):
     smtp_username: str | None = None
     smtp_password: str | None = None
     notification_email_destinations: str
+    kentik_email_destinations: str
 
 
 class SharepointParams(BaseSettings):
diff --git a/gso/workflows/edge_port/create_edge_port.py b/gso/workflows/edge_port/create_edge_port.py
index 6686e2669472ae68ee7965e726c515ec8d013307..2455ee3de58a5b7b71dfaf0702422d497c8fbc33 100644
--- a/gso/workflows/edge_port/create_edge_port.py
+++ b/gso/workflows/edge_port/create_edge_port.py
@@ -106,6 +106,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
 
     input_forms_data = initial_user_input.model_dump() | interface_form_input_data.model_dump()
     summary_form_data = input_forms_data | {
+        "ga_id": input_forms_data["ga_id"] if not input_forms_data["generate_ga_id"] else "Will be generated",
         "node": Router.from_subscription(initial_user_input.node).router.router_fqdn,
         "partner": get_partner_by_id(initial_user_input.partner).name,
         "edge_port_ae_members": input_forms_data["ae_members"],
@@ -115,6 +116,7 @@ def initial_input_form_generator(product_name: str) -> FormGenerator:
         "custom_service_name": input_forms_data["custom_service_name"],
     }
     summary_fields = [
+        "ga_id",
         "node",
         "partner",
         "edge_port_type",
diff --git a/gso/workflows/edge_port/modify_edge_port.py b/gso/workflows/edge_port/modify_edge_port.py
index 5a836cfe16b10f05ea2bf5ea34fc957417d0ee27..8a82d146761d3d850781226b2d9b3bcd609718ea 100644
--- a/gso/workflows/edge_port/modify_edge_port.py
+++ b/gso/workflows/edge_port/modify_edge_port.py
@@ -40,7 +40,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
         tt_number: TTNumber
         enable_lacp: bool = subscription.edge_port.enable_lacp
         member_speed: PhysicalPortCapacity = subscription.edge_port.member_speed
-        encapsulation: EncapsulationType = subscription.edge_port.encapsulation
+        encapsulation: EncapsulationType | str = subscription.edge_port.encapsulation  # FIXME: remove str workaround
         number_of_members: int = len(subscription.edge_port.edge_port_ae_members)
         minimum_links: int | None = subscription.edge_port.minimum_links or None
         mac_address: str | None = subscription.edge_port.mac_address or None
diff --git a/gso/workflows/iptrunk/modify_trunk_interface.py b/gso/workflows/iptrunk/modify_trunk_interface.py
index 46de31d9940e1bb542fa23d4f56346aba895a111..58277b0c58f31e8db877370cb941a84ea15527b8 100644
--- a/gso/workflows/iptrunk/modify_trunk_interface.py
+++ b/gso/workflows/iptrunk/modify_trunk_interface.py
@@ -95,13 +95,14 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
             | None
         ) = subscription.iptrunk.gs_id
         iptrunk_description: str | None = subscription.iptrunk.iptrunk_description
-        iptrunk_type: IptrunkType = subscription.iptrunk.iptrunk_type
+        iptrunk_type: IptrunkType | str = subscription.iptrunk.iptrunk_type  # FIXME: remove str workaround
         warning_label: Label = (
             "Changing the PhyPortCapacity will result in the deletion of all AE members. "
             "You will need to add the new AE members in the next steps."
         )
-        iptrunk_speed: PhysicalPortCapacity = subscription.iptrunk.iptrunk_speed
+        iptrunk_speed: PhysicalPortCapacity | str = subscription.iptrunk.iptrunk_speed  # FIXME: remove str workaround
         iptrunk_number_of_members: int = len(subscription.iptrunk.iptrunk_sides[0].iptrunk_side_ae_members)
+        iptrunk_description_suffix: str | None = subscription.iptrunk.iptrunk_description_suffix
         iptrunk_isis_metric: ReadOnlyField(subscription.iptrunk.iptrunk_isis_metric, default_type=int)  # type: ignore[valid-type]
         iptrunk_ipv4_network: ReadOnlyField(  # type: ignore[valid-type]
             str(subscription.iptrunk.iptrunk_ipv4_network), default_type=IPv4AddressType
@@ -266,6 +267,7 @@ def modify_iptrunk_subscription(
     iptrunk_description: str | None,
     iptrunk_speed: PhysicalPortCapacity,
     iptrunk_minimum_links: int,
+    iptrunk_description_suffix: str | None,
     side_a_ga_id: str | None,
     side_a_ae_members: list[dict],
     side_b_ga_id: str | None,
@@ -299,6 +301,7 @@ def modify_iptrunk_subscription(
     subscription.iptrunk.iptrunk_type = iptrunk_type
     subscription.iptrunk.iptrunk_speed = iptrunk_speed
     subscription.iptrunk.iptrunk_minimum_links = iptrunk_minimum_links
+    subscription.iptrunk.iptrunk_description_suffix = iptrunk_description_suffix
 
     subscription.iptrunk.iptrunk_sides[0].ga_id = side_a_ga_id
     update_side_members(subscription, 0, side_a_ae_members)
@@ -309,8 +312,12 @@ def modify_iptrunk_subscription(
         subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_site.site_name,
         subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_site.site_name,
     ])
-    subscription.description = f"IP trunk {side_names[0]} {side_names[1]}, {gs_id}"
-
+    description = f"IP trunk {side_names[0]} {side_names[1]}"
+    if iptrunk_description_suffix:
+        description += f" {iptrunk_description_suffix}"
+    if gs_id:
+        description += f", {gs_id}"
+    subscription.description = description
     return {
         "subscription": subscription,
         "removed_ae_members": removed_ae_members,
diff --git a/gso/workflows/l2_circuit/modify_layer_2_circuit.py b/gso/workflows/l2_circuit/modify_layer_2_circuit.py
index 0135abaea550af81691daa034778c01f64253067..91c791359b99194e9e089e7a0823b3b857aa37af 100644
--- a/gso/workflows/l2_circuit/modify_layer_2_circuit.py
+++ b/gso/workflows/l2_circuit/modify_layer_2_circuit.py
@@ -31,7 +31,8 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
         partner: ReadOnlyField(get_partner_by_id(subscription.customer_id).name, default_type=str)  # type: ignore[valid-type]
         divider: Divider = Field(None, exclude=True)
 
-        layer_2_circuit_type: Layer2CircuitType = subscription.layer_2_circuit.layer_2_circuit_type
+        # FIXME: remove str workaround in type hint below
+        layer_2_circuit_type: Layer2CircuitType | str = subscription.layer_2_circuit.layer_2_circuit_type
         policer_enabled: bool = subscription.layer_2_circuit.policer_enabled
         custom_service_name: str | None = subscription.layer_2_circuit.custom_service_name
 
diff --git a/gso/workflows/l3_core_service/modify_l3_core_service.py b/gso/workflows/l3_core_service/modify_l3_core_service.py
index 4f59c1afaf95c008b875c2837735fdae706660c7..427717fdf037de34953c935fffbc3ec33ef75f93 100644
--- a/gso/workflows/l3_core_service/modify_l3_core_service.py
+++ b/gso/workflows/l3_core_service/modify_l3_core_service.py
@@ -264,7 +264,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
                 gs_id: str = current_ap.sbp.gs_id
                 custom_service_name: str | None = current_ap.custom_service_name
                 is_tagged: bool = current_ap.sbp.is_tagged
-                ap_type: APType | str = current_ap.ap_type
+                ap_type: APType | str = current_ap.ap_type  # FIXME: remove str workaround
                 # The SBP model does not require these five fields, but in the case of L3 Core Services this will never
                 # occur since it's a layer 3 service. The ignore statements are there to put our type checker at ease.
                 vlan_id: VLAN_ID = current_ap.sbp.vlan_id  # type: ignore[assignment]
diff --git a/gso/workflows/l3_core_service/validate_prefix_list.py b/gso/workflows/l3_core_service/validate_prefix_list.py
index 85f1580526f75bd37a15ec9b3c0a3cd4d72383fd..9104f04a35500d98ab72bd4d856407a6e9cd6481 100644
--- a/gso/workflows/l3_core_service/validate_prefix_list.py
+++ b/gso/workflows/l3_core_service/validate_prefix_list.py
@@ -15,13 +15,18 @@ from pydantic_forms.validators import Label
 from gso.products.product_types.l3_core_service import L3CoreService, L3CoreServiceType
 from gso.services.lso_client import LSOState, anonymous_lso_interaction, lso_interaction
 from gso.services.partners import get_partner_by_id
+from gso.utils.shared_enums import Vendor
 
 
 @step("Prepare list of all Access Ports")
 def build_fqdn_list(subscription_id: UUIDstr) -> State:
-    """Build the list of all FQDNs that are in the list of access ports of a L3 Core Service subscription."""
+    """Build the list of all FQDNs in the access ports of L3 Core Service subscription, excluding Juniper devices."""
     subscription = L3CoreService.from_subscription(subscription_id)
-    ap_fqdn_list = [ap.sbp.edge_port.node.router_fqdn for ap in subscription.l3_core_service.ap_list]
+    ap_fqdn_list = [
+        ap.sbp.edge_port.node.router_fqdn
+        for ap in subscription.l3_core_service.ap_list
+        if ap.sbp.edge_port.node.vendor != Vendor.JUNIPER
+    ]
     return {"ap_fqdn_list": ap_fqdn_list, "subscription": subscription}
 
 
@@ -107,8 +112,7 @@ def deploy_prefix_lists_real(subscription: dict[str, Any], process_id: UUIDstr,
 def validate_prefix_list() -> StepList:
     """Validate prefix-lists for an existing L3 Core Service subscription."""
     prefix_list_should_be_validated = conditional(
-        lambda state: state["subscription"]["l3_core_service_type"]
-        in {L3CoreServiceType.GEANT_IP, L3CoreServiceType.IAS}
+        lambda state: state["subscription"]["l3_core_service_type"] == L3CoreServiceType.GEANT_IP
     )
     prefix_list_has_drifted = conditional(lambda state: bool(state["prefix_list_drift"]))
 
diff --git a/gso/workflows/router/modify_connection_strategy.py b/gso/workflows/router/modify_connection_strategy.py
index a520a581c05e41cb72d5fd24ef60ebaee787f039..a10da6893923b16578010867294c3bf3b8436e5b 100644
--- a/gso/workflows/router/modify_connection_strategy.py
+++ b/gso/workflows/router/modify_connection_strategy.py
@@ -28,7 +28,7 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
     class ModifyConnectionStrategyForm(SubmitFormPage):
         model_config = ConfigDict(title=f"Modify the connection strategy of {subscription.router.router_fqdn}.")
 
-        connection_strategy: ConnectionStrategy = current_connection_strategy
+        connection_strategy: ConnectionStrategy | str = current_connection_strategy  # FIXME: remove str workaround
         router_ts_port: PortNumber = subscription.router.router_ts_port
 
     user_input = yield ModifyConnectionStrategyForm
diff --git a/gso/workflows/router/terminate_router.py b/gso/workflows/router/terminate_router.py
index 9c73b13e5ca3ae08dc6ef8e1c6bbffd133fd37e9..b5464513ef0d5cb0574d826ca65883ea68e8e03f 100644
--- a/gso/workflows/router/terminate_router.py
+++ b/gso/workflows/router/terminate_router.py
@@ -18,6 +18,7 @@ The workflow consists of the following steps:
 import ipaddress
 import json
 import logging
+from typing import Any
 
 from orchestrator.forms import SubmitFormPage
 from orchestrator.forms.validators import Label
@@ -42,6 +43,7 @@ from gso.services import infoblox
 from gso.services.kentik_client import KentikClient
 from gso.services.librenms_client import LibreNMSClient
 from gso.services.lso_client import LSOState, lso_interaction
+from gso.services.mailer import send_mail
 from gso.services.netbox_client import NetboxClient
 from gso.settings import load_oss_params
 from gso.utils.helpers import generate_inventory_for_routers
@@ -252,19 +254,65 @@ def remove_device_from_librenms(subscription: Router) -> State:
 
 
 @step("Apply the archiving license in Kentik")
-def kentik_apply_archive_license(subscription: Router) -> State:
+def kentik_apply_archive_license(subscription: Router, process_id: UUIDstr) -> State:
     """Apply the archiving license to a PE router in Kentik.
 
-    This includes setting the flow rate to one flow per second.
+    This includes setting the flow rate to one flow per second, and the BGP type to `none`. Service Management will also
+    be emailed to inform them of an archiving license being consumed. If this step is unsuccessful, an email is sent to
+    inform them as well. This could be caused by a device being missing in Kentik, or having no more licenses available.
     """
     kentik_client = KentikClient()
-    kentik_archive_plan_id = kentik_client.get_plan_by_name(load_oss_params().KENTIK.archive_license_key)["id"]
-    kentik_device = kentik_client.get_device_by_name(subscription.router.router_fqdn)
-    if "id" not in kentik_device:
-        return {"kentik_device": "Device not found, no license applied"}
-
-    updated_device = {"device": {"plan_id": kentik_archive_plan_id, "device_sample_rate": 1}}
+    oss_params = load_oss_params()
+
+    def _get_valid_kentik_device() -> dict[str, Any] | None:
+        #  Attempt fetching a device from Kentik.
+        device = kentik_client.get_device_by_name(subscription.router.router_fqdn)
+        if "id" not in device and subscription.router.vendor == Vendor.JUNIPER:
+            #  If the device is a Juniper, there is a chance that the FQDN is written with underscores as delimiter.
+            #  We try again when fetching the device was unsuccessful the first time.
+            device = kentik_client.get_device_by_name(subscription.router.router_fqdn.replace(".", "_"))
+
+        #  If still unsuccessful after two attempts, we give up.
+        return device if "id" in device else None
+
+    if not (kentik_device := _get_valid_kentik_device()):
+        send_mail(
+            "[GSO][Kentik] Failed to terminate router",
+            f"During the execution of a router termination workflow in GSO, we were unable to find the device "
+            f"{subscription.router.router_fqdn}.\nPlease update this device manually in Kentik.\n\n"
+            f"For reference, the workflow run can be found at: "
+            f"{oss_params.GENERAL.public_hostname}/workflows/{process_id}\n\nRegards, the GÉANT Automation Platform.",
+            destination=oss_params.EMAIL.kentik_email_destinations,
+        )
+        return {"kentik_device": f"Device {subscription.router.router_fqdn} not found in Kentik, no license applied!"}
+
+    #  Send an email if we are out of archiving licenses.
+    kentik_archive_plan = kentik_client.get_plan_by_name(oss_params.KENTIK.archive_license_key)
+    if len(kentik_archive_plan["devices"]) >= kentik_archive_plan["max_devices"]:
+        send_mail(
+            "[GSO][Kentik] Failed to apply historical license",
+            f"During the execution of a router termination workflow on GSO, we were unable to apply a historical "
+            f"license to device {subscription.router.router_fqdn}.\nNo changes have been made, please update this "
+            f"device manually.\nIt appears we have run out of available historical licenses, all "
+            f"{kentik_archive_plan["max_devices"]} licenses are currently in use.\n\nFor reference, the workflow run "
+            f"can be found at: {oss_params.GENERAL.public_hostname}/workflows/{process_id}\n\nRegards, the GÉANT "
+            f"Automation Platform.",
+            destination=oss_params.EMAIL.kentik_email_destinations,
+        )
+        return {"kentik_device": "No more archiving licenses available. Nothing is updated in Kentik."}
+
+    updated_device = {
+        "device": {"plan_id": kentik_archive_plan["id"], "device_sample_rate": 1, "device_bgp_type": "none"}
+    }
     kentik_device = kentik_client.update_device(kentik_device["id"], updated_device)
+    send_mail(
+        "[GSO][Kentik] Historical license has been applied",
+        f"A historical license has been applied to device {subscription.router.router_fqdn}.\n"
+        f"Currently, {len(kentik_archive_plan["devices"]) + 1} out of {kentik_archive_plan["max_devices"]} historical "
+        f"licenses are in use.\n\nFor reference, the workflow run can be found at: "
+        f"{oss_params.GENERAL.public_hostname}/workflows/{process_id}\n\nRegards, the GÉANT Automation Platform.",
+        destination=oss_params.EMAIL.kentik_email_destinations,
+    )
 
     return {"kentik_device": kentik_device}
 
diff --git a/gso/workflows/router/validate_router.py b/gso/workflows/router/validate_router.py
index 25bd3176c50b8506117dc3c09767944c85707baa..675def732c33824ac386713707303b5968afef9d 100644
--- a/gso/workflows/router/validate_router.py
+++ b/gso/workflows/router/validate_router.py
@@ -16,7 +16,8 @@ from gso.services.kentik_client import KentikClient
 from gso.services.librenms_client import LibreNMSClient
 from gso.services.lso_client import LSOState, anonymous_lso_interaction
 from gso.services.netbox_client import NetboxClient
-from gso.services.subscriptions import get_active_vrfs_linked_to_router
+from gso.services.subscriptions import get_active_layer_3_services_on_router, get_active_vrfs_linked_to_router
+from gso.settings import load_oss_params
 from gso.utils.helpers import generate_inventory_for_routers
 from gso.utils.shared_enums import Vendor
 
@@ -142,15 +143,41 @@ def check_librenms_entry_exists(subscription: Router) -> None:
 def check_kentik_entry_exists(subscription: Router) -> None:
     """Validate the Kentik entry for a PE Router.
 
-    Raises an HTTP error 404 when the device is not present in Kentik.
+    If a router has at least one layer 3 service insisting on it, there should be a valid Kentik license applied to this
+    device. The only thing we can check for reliably, is whether this device does not have an archiving or placeholder
+    license on it. This is because there can be multiple, valid, non-archiving licenses for devices.
+
+    Raises:
+        ProcessFailureError when a Kentik device is missing, or configured incorrectly.
     """
     client = KentikClient()
+
+    #  Check if the device exists in Kentik.
     device = client.get_device_by_name(subscription.router.router_fqdn)
     if not device:
         raise ProcessFailureError(
             message="Device not found in Kentik", details={"device": subscription.router.router_fqdn}
         )
 
+    #  If there are active layer 3 services, check the license type. It may not be the placeholder or archiving license.
+    if bool(get_active_layer_3_services_on_router(subscription.subscription_id)):
+        kentik_params = load_oss_params().KENTIK
+        archive_plan = client.get_plan_by_name(kentik_params.archive_license_key)
+        if any(device["device_name"] == subscription.router.router_fqdn for device in archive_plan["devices"]):
+            raise ProcessFailureError(
+                message="Device in Kentik incorrectly configured",
+                details=f"Kentik device {subscription.router.router_fqdn} has the archiving license "
+                f"{archive_plan["name"]} applied to it, despite the existence of active layer 3 services.",
+            )
+
+        placeholder_plan = client.get_plan_by_name(kentik_params.placeholder_license_key)
+        if any(device["device_name"] == subscription.router.router_fqdn for device in placeholder_plan["devices"]):
+            raise ProcessFailureError(
+                message="Device in Kentik incorrectly configured",
+                details=f"Kentik device {subscription.router.router_fqdn} has the placeholder license "
+                f"{placeholder_plan["name"]} applied to it, despite the existence of active layer 3 services.",
+            )
+
 
 @step("Check base config for drift")
 def verify_base_config(subscription: dict[str, Any]) -> LSOState:
diff --git a/gso/workflows/vrf/modify_vrf_router_list.py b/gso/workflows/vrf/modify_vrf_router_list.py
index 2ea7ed79bf9f303b547f1e5044931802d8a50f52..6f0dc2229dfdd0371718a7646467a14643fc1eb9 100644
--- a/gso/workflows/vrf/modify_vrf_router_list.py
+++ b/gso/workflows/vrf/modify_vrf_router_list.py
@@ -4,7 +4,7 @@ This workflow allows for adding or removing one router to the VRF router list.
 """
 
 import logging
-from typing import Any, cast
+from typing import Any, TypeAlias, cast
 
 from orchestrator.forms import FormPage, SubmitFormPage
 from orchestrator.targets import Target
@@ -13,7 +13,7 @@ from orchestrator.workflows.steps import resync, store_process_subscription, uns
 from orchestrator.workflows.utils import wrap_modify_initial_input_form
 from pydantic import ConfigDict, Field
 from pydantic_forms.types import FormGenerator, State, UUIDstr, strEnum
-from pydantic_forms.validators import Divider, ReadOnlyField
+from pydantic_forms.validators import Choice, Divider, ReadOnlyField
 
 from gso.products.product_types.router import Router
 from gso.products.product_types.vrf import VRF
@@ -62,12 +62,24 @@ def initial_input_form_generator(subscription_id: UUIDstr) -> FormGenerator:
 
         case Operation.REMOVE:
 
+            def existing_router_selector() -> TypeAlias:
+                router_subscriptions = {
+                    str(router.owner_subscription_id): router.router_fqdn for router in subscription.vrf.vrf_router_list
+                }
+
+                return cast(
+                    type[Choice],
+                    Choice.__call__(
+                        "Select a router", zip(router_subscriptions.keys(), router_subscriptions.items(), strict=True)
+                    ),
+                )
+
             class RemoveVRFRouterListForm(SubmitFormPage):
                 model_config = ConfigDict(title=f"Modify the {subscription.vrf.vrf_name} VRF to remove a router.")
                 existing_routers: existing_router_list()  # type: ignore[valid-type]
 
                 divider: Divider = Field(None, exclude=True)
-                selected_router: active_router_selector()  # type: ignore[valid-type]
+                selected_router: existing_router_selector()  # type: ignore[valid-type]
 
             user_input = yield RemoveVRFRouterListForm
 
diff --git a/setup.py b/setup.py
index c1a77e6ac6a976f59bd473c068e08b994e1ad782..f0cf0320874b1d1fb4e6fbedea918c2dad9f9e16 100644
--- a/setup.py
+++ b/setup.py
@@ -4,7 +4,7 @@ from setuptools import find_packages, setup
 
 setup(
     name="geant-service-orchestrator",
-    version="2.44",
+    version="2.45",
     author="GÉANT Orchestration and Automation Team",
     author_email="goat@geant.org",
     description="GÉANT Service Orchestrator",
diff --git a/test/conftest.py b/test/conftest.py
index 92d8893fada48e02ef520e28c3599e1a7a6103a2..2c015bd1b54c0467322e716daa6fb3dbf53c8c5e 100644
--- a/test/conftest.py
+++ b/test/conftest.py
@@ -33,12 +33,14 @@ from sqlalchemy.orm import scoped_session, sessionmaker
 from starlette.testclient import TestClient
 from urllib3_mock import Responses
 
+import gso.services.mailer
 from gso.services.partners import PartnerSchema, create_partner
 from gso.services.subscriptions import is_resource_type_value_unique
 from gso.utils.types.interfaces import LAGMember, LAGMemberList
 from test.fixtures import *  # noqa: F403
 
-logging.getLogger("faker.factory").setLevel(logging.WARNING)
+logger = logging.getLogger("faker.factory")
+logger.setLevel(logging.WARNING)
 
 
 class UseJuniperSide(strEnum):
@@ -595,4 +597,9 @@ def responses():
 @pytest.fixture(autouse=True)
 def _no_mail(monkeypatch):
     """Remove sending mails from all tests."""
-    monkeypatch.delattr("smtplib.SMTP")
+
+    def send_mail(subject: str, body: str, *, destination: str | None = None) -> None:
+        email = f"*** SENT AN EMAIL ***\nTO: {destination}\nSUBJECT: {subject}\nCONTENT:\n{body}"
+        logger.info(email)
+
+    monkeypatch.setattr(gso.services.mailer, "send_mail", send_mail)
diff --git a/test/workflows/iptrunk/test_modify_trunk_interface.py b/test/workflows/iptrunk/test_modify_trunk_interface.py
index e13b5e068a2fee2680c02442e24351feb776667c..e636c2cc3db61f6b4adf5041f7c125a21b42ef70 100644
--- a/test/workflows/iptrunk/test_modify_trunk_interface.py
+++ b/test/workflows/iptrunk/test_modify_trunk_interface.py
@@ -72,6 +72,7 @@ def input_form_iptrunk_data(
             "iptrunk_type": new_type,
             "iptrunk_speed": new_speed,
             "iptrunk_number_of_members": new_link_count,
+            "iptrunk_description_suffix": faker.word(),
         },
         {},
         {
@@ -164,13 +165,17 @@ def test_iptrunk_modify_trunk_interface_success(
         subscription.iptrunk.iptrunk_sides[0].iptrunk_side_node.router_site.site_name,
         subscription.iptrunk.iptrunk_sides[1].iptrunk_side_node.router_site.site_name,
     ])
-    assert subscription.description == f"IP trunk {side_names[0]} {side_names[1]}, {new_sid}"
+    assert subscription.description == (
+        f"IP trunk {side_names[0]} {side_names[1]} "
+        f"{input_form_iptrunk_data[1]["iptrunk_description_suffix"]}, {new_sid}"
+    )
     assert subscription.iptrunk.gs_id == input_form_iptrunk_data[1]["gs_id"]
     assert subscription.iptrunk.iptrunk_description == input_form_iptrunk_data[1]["iptrunk_description"]
     assert subscription.iptrunk.iptrunk_type == input_form_iptrunk_data[1]["iptrunk_type"]
     assert subscription.iptrunk.iptrunk_speed == input_form_iptrunk_data[1]["iptrunk_speed"]
     assert subscription.iptrunk.iptrunk_minimum_links == input_form_iptrunk_data[1]["iptrunk_number_of_members"] - 1
     assert subscription.iptrunk.iptrunk_sides[0].ga_id == new_side_a_gid
+    assert subscription.iptrunk.iptrunk_description_suffix == input_form_iptrunk_data[1]["iptrunk_description_suffix"]
 
     def _find_interface_by_name(interfaces: LAGMemberList, name: str):
         for interface in interfaces:
diff --git a/test/workflows/l3_core_service/test_validate_prefix_list.py b/test/workflows/l3_core_service/test_validate_prefix_list.py
index 98f16f2503f2a37f1e1075e9595576654b5bd9a5..c4fe2ed976d1e89d555838a2fb54e8e90b0c586a 100644
--- a/test/workflows/l3_core_service/test_validate_prefix_list.py
+++ b/test/workflows/l3_core_service/test_validate_prefix_list.py
@@ -3,6 +3,7 @@ from unittest.mock import patch
 import pytest
 
 from gso.products.product_types.l3_core_service import L3_CORE_SERVICE_TYPES, L3CoreService, L3CoreServiceType
+from gso.utils.shared_enums import Vendor
 from test import USER_CONFIRM_EMPTY_FORM
 from test.workflows import (
     assert_complete,
@@ -22,7 +23,7 @@ from test.workflows import (
 def test_validate_prefix_list_success(
     mock_lso_interaction, l3_core_service_subscription_factory, faker, l3_core_service_type
 ):
-    should_run_validation = l3_core_service_type in {L3CoreServiceType.GEANT_IP, L3CoreServiceType.IAS}
+    should_run_validation = l3_core_service_type == L3CoreServiceType.GEANT_IP
     subscription_id = str(
         l3_core_service_subscription_factory(l3_core_service_type=l3_core_service_type).subscription_id
     )
@@ -40,6 +41,9 @@ def test_validate_prefix_list_success(
     subscription = L3CoreService.from_subscription(subscription_id)
     assert subscription.status == "active"
     assert subscription.insync is True
+    # Verify the subscription has no Juniper devices
+    for ap in subscription.l3_core_service.ap_list:
+        assert ap.sbp.edge_port.node.vendor != Vendor.JUNIPER
     # Verify the number of LSO interactions
     assert mock_lso_interaction.call_count == (1 if should_run_validation else 0)
 
@@ -83,7 +87,7 @@ def test_validate_prefix_list_with_diff(mock_lso_interaction, l3_core_service_su
 def test_validate_prefix_list_without_diff(mock_lso_interaction, l3_core_service_subscription_factory, faker):
     """Test case where playbook_has_diff does not qualify and skips additional steps."""
     subscription_id = str(
-        l3_core_service_subscription_factory(l3_core_service_type=L3CoreServiceType.IAS).subscription_id
+        l3_core_service_subscription_factory(l3_core_service_type=L3CoreServiceType.GEANT_IP).subscription_id
     )
     initial_l3_core_service_data = [{"subscription_id": subscription_id}]
     # Run the workflow and extract results