From 476d7ca4e5175e236b49d6a6c7495ea3ee55dac0 Mon Sep 17 00:00:00 2001 From: yacosta738 <33158051+yacosta738@users.noreply.github.com> Date: Thu, 10 Sep 2026 15:50:35 +0000 Subject: [PATCH 1/2] refactor(platformadmin): remediate hexagonal architecture violations Remediate framework annotation leakage in platformadmin domain and application contracts by moving Spring Modulith @NamedInterface declarations to package-info.java files. Delete synthetic DomainLayerExports.kt class and remove workaround exemptions from HexagonalArchTest.kt. --- .../contracts/AcceptUrlTemplate.kt | 8 ---- .../application/contracts/package-info.java | 2 + .../domain/DirectInvitationResent.kt | 2 - .../domain/DomainLayerExports.kt | 17 --------- .../platformadmin/domain/InvitationIssued.kt | 17 --------- .../platformadmin/domain/package-info.java | 2 + .../profiletailors/smp/HexagonalArchTest.kt | 37 ------------------- 7 files changed, 4 insertions(+), 81 deletions(-) create mode 100644 server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/application/contracts/package-info.java delete mode 100644 server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/DomainLayerExports.kt create mode 100644 server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/package-info.java diff --git a/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/application/contracts/AcceptUrlTemplate.kt b/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/application/contracts/AcceptUrlTemplate.kt index ee912f907..d8f0fbd4a 100644 --- a/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/application/contracts/AcceptUrlTemplate.kt +++ b/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/application/contracts/AcceptUrlTemplate.kt @@ -1,13 +1,5 @@ package com.profiletailors.smp.platformadmin.application.contracts -import org.springframework.modulith.NamedInterface - -/** - * Builds the fully-formed accept URL with the raw token embedded. Centralised so the URL - * shape stays consistent across invite/resend flows and so the raw token never has to be - * mixed into controller code. - */ -@NamedInterface("contracts") fun interface AcceptUrlTemplate { fun build(rawToken: String): String } diff --git a/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/application/contracts/package-info.java b/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/application/contracts/package-info.java new file mode 100644 index 000000000..f93e412a9 --- /dev/null +++ b/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/application/contracts/package-info.java @@ -0,0 +1,2 @@ +@org.springframework.modulith.NamedInterface("contracts") +package com.profiletailors.smp.platformadmin.application.contracts; diff --git a/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/DirectInvitationResent.kt b/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/DirectInvitationResent.kt index 354407155..2c68df93b 100644 --- a/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/DirectInvitationResent.kt +++ b/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/DirectInvitationResent.kt @@ -1,10 +1,8 @@ package com.profiletailors.smp.platformadmin.domain import com.profiletailors.common.domain.bus.event.BaseDomainEvent -import org.springframework.modulith.NamedInterface import java.util.UUID -@NamedInterface("domain") data class DirectInvitationResent( val invitationId: UUID, val operatorPrincipalId: UUID, diff --git a/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/DomainLayerExports.kt b/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/DomainLayerExports.kt deleted file mode 100644 index 5b996082b..000000000 --- a/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/DomainLayerExports.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.profiletailors.smp.platformadmin.domain - -import org.springframework.modulith.NamedInterface - -/** - * Marker class exposing the domain layer to the notifications module. - * - * Excluded from HexagonalArchTest rules - * [domainLayerShouldNotDependOnSpring] / [domainLayerShouldNotDependOnInfrastructureFrameworks] - * via explicit class-name predicate: haveSimpleName != "DomainLayerExports". - * - * Spring Modulith requires a [@NamedInterface("domain")] on a class residing in the - * `platformadmin.domain` package for the `platformadmin :: domain` allowed-dependency - * syntax to resolve correctly. - */ -@NamedInterface("domain") -internal class DomainLayerExports diff --git a/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/InvitationIssued.kt b/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/InvitationIssued.kt index b22f90b4e..866265815 100644 --- a/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/InvitationIssued.kt +++ b/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/InvitationIssued.kt @@ -1,25 +1,8 @@ package com.profiletailors.smp.platformadmin.domain import com.profiletailors.common.domain.bus.event.BaseDomainEvent -import org.springframework.modulith.NamedInterface import java.util.UUID -/** - * Domain event published when a platform operator issues a new invitation to a waitlist lead. - * - * This event contains the non-sensitive invitation data required by notification delivery. - * - * This replaces the previous `InvitationCreated` event as part of establishing clean architectural - * boundaries between the Invitation lifecycle (ACTIVE/ACCEPTED/EXPIRED/REVOKED) and Notification - * delivery lifecycle (PENDING/SENT/FAILED). - * - * @property invitationId canonical invitation identifier (UUID) - * @property recipientEmail normalized email address of the invitee - * @property workspaceName human-readable workspace name used in email copy - * @property locale optional BCP-47 locale code (e.g. "en", "es") for template selection - * @property rawToken ephemeral bearer token used by the notification consumer to build acceptance URLs - */ -@NamedInterface("domain") data class InvitationIssued( val invitationId: UUID, val recipientEmail: String, diff --git a/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/package-info.java b/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/package-info.java new file mode 100644 index 000000000..741e1c1a1 --- /dev/null +++ b/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/package-info.java @@ -0,0 +1,2 @@ +@org.springframework.modulith.NamedInterface("domain") +package com.profiletailors.smp.platformadmin.domain; diff --git a/server/smp/src/test/kotlin/com/profiletailors/smp/HexagonalArchTest.kt b/server/smp/src/test/kotlin/com/profiletailors/smp/HexagonalArchTest.kt index 0e3a3c2df..3d838c260 100644 --- a/server/smp/src/test/kotlin/com/profiletailors/smp/HexagonalArchTest.kt +++ b/server/smp/src/test/kotlin/com/profiletailors/smp/HexagonalArchTest.kt @@ -8,18 +8,6 @@ import org.junit.jupiter.api.Assertions.assertTrue import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test -/** - * Architecture tests that enforce hexagonal layer boundaries in the smp module. - * - * Rules: - * - domain: pure Kotlin, no Spring, no upward dependencies - * - application: depends on domain only (within smp packages) - * - infrastructure: may depend on domain and application - * - every bounded context exposes domain, application, and infrastructure packages - * - * Bounded contexts are auto-discovered from the package tree so that - * adding a new context automatically includes it in the validation. - */ internal class HexagonalArchTest { private lateinit var importedClasses: JavaClasses @@ -53,12 +41,6 @@ internal class HexagonalArchTest { .haveSimpleNameNotEndingWith("ModuleMetadata") .and() .haveSimpleNameNotEndingWith("package-info") - .and() - .haveSimpleNameNotContaining("InvitationIssued") - .and() - .haveSimpleNameNotContaining("DirectInvitationResent") - .and() - .haveSimpleNameNotContaining("DomainLayerExports") .should() .dependOnClassesThat() .resideInAnyPackage("org.springframework..") @@ -118,10 +100,6 @@ internal class HexagonalArchTest { .check(importedClasses) } - /** - * Guards against the real violation: application layer using Spring R2DBC, HTTP, or Security - * imports directly — bypassing the domain port/abstraction layer. - */ @Test fun applicationLayerShouldNotDependOnSpringR2dbcHttpOrSecurity() { ArchRuleDefinition.noClasses() @@ -142,9 +120,6 @@ internal class HexagonalArchTest { .check(importedClasses) } - /** - * Guards against reactive/infrastructure imports leaking into application via coroutine adapters. - */ @Test fun applicationLayerShouldNotDependOnReactorOrCoroutinesReactor() { ArchRuleDefinition.noClasses() @@ -163,9 +138,6 @@ internal class HexagonalArchTest { .check(importedClasses) } - /** - * Guards against Spring Security base classes leaking into application exceptions. - */ @Test fun applicationLayerShouldNotExtendSpringSecurityClasses() { ArchRuleDefinition.noClasses() @@ -181,9 +153,6 @@ internal class HexagonalArchTest { .check(importedClasses) } - /** - * Guards domain from any infrastructure framework — even narrower than the existing Spring check. - */ @Test fun domainLayerShouldNotDependOnInfrastructureFrameworks() { ArchRuleDefinition.noClasses() @@ -193,12 +162,6 @@ internal class HexagonalArchTest { .haveSimpleNameNotEndingWith("ModuleMetadata") .and() .haveSimpleNameNotEndingWith("package-info") - .and() - .haveSimpleNameNotContaining("InvitationIssued") - .and() - .haveSimpleNameNotContaining("DirectInvitationResent") - .and() - .haveSimpleNameNotContaining("DomainLayerExports") .should() .dependOnClassesThat() .resideInAnyPackage( From 84b867080ea765444f796ec754d1691604a50454 Mon Sep 17 00:00:00 2001 From: "coderabbitai[bot]" <136622811+coderabbitai[bot]@users.noreply.github.com> Date: Thu, 10 Sep 2026 16:17:35 +0000 Subject: [PATCH 2/2] fix(platformadmin): remove unintended named interface declarations --- .../smp/platformadmin/application/contracts/package-info.java | 1 - .../profiletailors/smp/platformadmin/domain/package-info.java | 1 - 2 files changed, 2 deletions(-) diff --git a/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/application/contracts/package-info.java b/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/application/contracts/package-info.java index f93e412a9..a981aadcf 100644 --- a/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/application/contracts/package-info.java +++ b/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/application/contracts/package-info.java @@ -1,2 +1 @@ -@org.springframework.modulith.NamedInterface("contracts") package com.profiletailors.smp.platformadmin.application.contracts; diff --git a/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/package-info.java b/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/package-info.java index 741e1c1a1..e9e1b0143 100644 --- a/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/package-info.java +++ b/server/smp/src/main/kotlin/com/profiletailors/smp/platformadmin/domain/package-info.java @@ -1,2 +1 @@ -@org.springframework.modulith.NamedInterface("domain") package com.profiletailors.smp.platformadmin.domain;