Help us shape the future of Go! Take the 2026 Go Developer Survey

eligibility

package
v0.3.1 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Oct 25, 2026 License: AGPL-3.0 Imports: 13 Imported by: 0

Documentation

Overview

Package eligibility derives whether a message on a channel from a sending source may reach a destination (ADR 0001, 0013). Eligibility is never a stored flag on the Contact; it is computed in layers against three destination-keyed stores:

  1. (channel, destination) in Suppression → never (global hard floor)
  2. (channel, destination) unsubscribed "everything" → never
  3. (channel, destination) unsubscribed from source → never
  4. workspace requires confirmed opt-in and no Confirmation → never
  5. otherwise → send

The layers are defined exactly once (see layers) and consumed in two shapes that therefore cannot drift: Check decides one (workspace, destination) pair, and Predicate narrows a Contact query to the eligible audience. The rule is keyed by destination, never by Contact — a transactional destination may have no Contact.

A source of "" means a transactional send (ADR 0005): it skips layers 2–4 (you cannot opt out of, nor must you confirm, your own password reset) but still respects Suppression. Whether the workspace requires confirmed opt-in is read inside the rule, so no caller can forget the confirmation gate.

Stored destinations are normalized (lower-cased + trimmed) on write, but contact.email is not, so the Contact-side comparison folds case in SQL while Check normalizes its input with suppression.NormalizeDestination.

Index

Constants

View Source
const (
	ReasonSuppressed             = "suppressed"
	ReasonUnsubscribedEverything = "unsubscribed_everything"
	ReasonUnsubscribedSource     = "unsubscribed_source"
	ReasonUnconfirmed            = "unconfirmed"
)

Reasons a destination is ineligible.

View Source
const (
	SourceBroadcasts = "broadcasts"
	SourceEverything = "everything"
)

Sending sources — the unit a destination unsubscribes from. A source is automatic, never hand-authored: it IS the sender. All broadcasts share one scope; each automation is its own; "everything" is the reserved deliberate "leave entirely" opt-out (only ever an unsubscribe scope, never a send source).

View Source
const ChannelEmail = "email"

ChannelEmail identifies the email channel, the only built one; sms is reserved.

Variables

This section is empty.

Functions

func AutomationSource

func AutomationSource(automationID int64) string

AutomationSource is the per-automation unsubscribe scope.

func GloballyOptedOut

func GloballyOptedOut(channel string) predicate.Contact

GloballyOptedOut matches contacts whose destination is non-mailable on the given channel regardless of source: suppressed, or unsubscribed from "everything". Used for the "unsubscribed" analytics KPI — the derived, dashboard-level view of "fully opted out", since there is no stored contact status (ADR 0001). A per-source opt-out is deliberately not counted: that contact may still be mailable from other sources.

func ParseAutomationSource

func ParseAutomationSource(src string) (int64, bool)

ParseAutomationSource is the inverse of AutomationSource: it returns the automation id when src is an "automation:<id>" scope, and false otherwise.

func Predicate

func Predicate(channel, source string) predicate.Contact

Predicate narrows a Contact query to contacts whose email destination is eligible for (channel, source); "" source means transactional (Suppression only). It composes with a segment predicate (both are predicate.Contact). The correlation joins on lower(contacts.email) — never on contact_id — because the stores are destination-keyed.

Types

type Decision

type Decision struct {
	Eligible bool
	// Reason is empty when Eligible; otherwise one of the Reason* constants.
	Reason string
}

Decision is the outcome of a single eligibility Check.

func Check

func Check(ctx context.Context, s *ent.Scoped, channel, dest, source string) (Decision, error)

Check decides eligibility for one destination in the scoped Workspace. source is the Sending source the message goes out under, or "" for a transactional send. An empty destination is treated as eligible — the caller decides what a missing address means (there is nothing to suppress against).

The rule is one SQL expression (layers) whose Workspace comes from s; the check fails closed (an error) for a Workspace that does not exist; the confirmation gate reads the workspace flag inside it, so no caller can forget it. It is anchored on the Workspace's Suppression query so it runs through the scoped client; HAVING TRUE turns the select into a single-group one, so exactly one row comes back whether or not the Workspace has any Suppression.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL