Skip to content

One Brevo list, two ports, and a raw client behind one of them #1169

Description

@ExtraToast

cohort reaches one external system — a Brevo list — through two ports, and one of them is not a port at all.

CohortPort (with CohortPortRegistry, BrevoCohortAdapter, MockCohortPort) and TargetStrategy (with TargetStrategies, BrevoTargetStrategy, MockTargetStrategy) both abstract the same operations on the same list: create it, add and remove members, delete it, list what is on it. Each has exactly one production implementation, and both delegate to the same ContactListAdapter bean. Their member types — MemberRef(externalUserId, label) and ExternalMember(externalUserId, label) — are structurally identical. Both registries dispatch on TargetSystem, whose only cohort-valid value is BREVO: GOOGLE_CALENDAR has neither a CohortPort nor a TargetStrategy, so require(system) is a lookup with one possible answer.

Two consumers use CohortPortRegistry; five use TargetStrategies.

The part that is not a port

BrevoTargetStrategy injects the raw ContactsApi alongside the ContactListAdapter it also uses, and imports three Brevo model types directly, for folder and list paging. api ADR-019 forbids an external model reaching a domain, and cohort's Modulith whitelist grants it only contact :: api — but Modulith does not police third-party packages, so it verifies clean.

#1158 added VendorClientArchitectureTest, which now catches this. The violation is pinned there with a note naming the fix:

Removing these means publishing the catalog side through contact :: api so cohort speaks only to the wrapper.

ArchUnit sees six reaches where git grep sees three, because the return types count too.

Also worth settling

The two ports disagree about what an external id is. ContactListAdapter types every id as Long and its comment justifies a signature in Brevo's terms; CohortPort explicitly rejects that — "ids are String so Discord snowflakes and Google group emails sit alongside Brevo's numeric list ids" — and the codebase pays for the disagreement with a private String.toBrevoId() in two places. Worse, the numeric assumption is hard-coded into the generic base: ContactAdapterSyncTarget calls currentExternalId!!.toLong(), so a non-numeric system would break the shared class rather than its own adapter.

Suggested shape

Collapse CohortPort into TargetStrategy — the richer of the two, since it already has folders, move, catalog and capabilities — and push folder and list paging down into contact's adapter so cohort never imports a vendor package. That empties the pin in VendorClientArchitectureTest, which is the check that this actually landed.

Acceptance criteria

  • One port over the Brevo list, with its test double
  • cohort imports nothing from net.blueshell.clients, and the pinned entry in VendorClientArchitectureTest is gone rather than edited
  • The id shape is decided once, and toBrevoId has one definition or none
  • Existing cohort tests pass unchanged

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentSized and specified for an agent to pick up

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions