Skip to main content

Context

Every module needs to notify somebody, and without a single answer to that question each one invents its own — one checks consent, another forgets quiet hours, a third sends anyway because its message felt important. That is how a platform ends up with four delivery paths and one complaint. ADR-019 makes it one pure function over four configuration levels and a set of recipient gates. Pure matters: resolveChannels does no I/O, so it is exhaustively unit-testable, and a change to the rules is a change to one tested function rather than to four call sites. The channel row and the port exist from day one even when the adapter arrives later (DEC-E1). A tenant can see that WhatsApp is coming; they simply cannot enable it yet.

Scope (normative)

  • The four configuration levels: platform, tenant, module, event routing.
  • resolveChannels(event, tenant, module, recipient) as a pure function in packages/core.
  • Recipient gates: consent, suppression, preference center, and for marketing quiet hours and caps.
  • Category per routing entry: transactional | marketing | product.
  • A typed reason for every exclusion, recorded when a message is not sent.
  • Management endpoints for each configuration level.

Non-scope (normative)

  • Rendering — FS-MSG-0002. Resolution decides whether and where, not what.
  • Queueing and delivery — FS-MSG-0005 and FS-MSG-0006.
  • Consent storage, which belongs to core and is read here, never written.
  • Cross-channel fallback (push fails → email), which is F2 and noted (DEC-E4).

Behaviour (normative)

  1. Resolution runs the levels in order, and any level saying no ends it: platform operative → tenant enabled and configured → module enabled → event routing exists.
  2. Then the recipient gates: consent for that channelnot suppressed ∧ preference center allows it ∧ (marketing only) quiet hours and frequency caps.
  3. Transactional is not opt-out (DEC-E3). It passes the preference center, quiet hours and caps, but never passes suppression: a hard bounce means the address does not work, and category does not change that.
  4. Every “no” returns a typed reason, and the reason is recorded. “Not sent” without a reason makes support impossible and makes a compliance question unanswerable.
  5. The function is pure: no database, no cache, no clock read. Its inputs are the event, the already-loaded configuration and the recipient snapshot. Testability is the point.
  6. Quiet hours are evaluated in the recipient’s timezone where known, falling back to the tenant’s. A quiet-hours rule in the wrong timezone is worse than none.
  7. The result is a list of channels, not one. An event may legitimately go to in-app and email.
  8. FORBIDDEN: any send path that does not call this function · a tenant setting that disables suppression or consent checking · a category that bypasses the cascade.

Data (normative)

API (normative)

simulate answers “if this event happened for this contact right now, what would we send, and why not the rest” — the single most useful support tool in the module.

Events (normative)

None emitted. This feature consumes every module’s events and decides what happens next.

Acceptance criteria (normative)

  1. 100% branch coverage on resolveChannels, with a fixture per level and per gate.
  2. A suppressed recipient receives nothing, including transactional.
  3. A recipient who opted out of marketing still receives transactional.
  4. Quiet hours are evaluated in the recipient’s timezone, verified across a date line.
  5. Every exclusion returns a distinct typed reason, and the reason is recorded.
  6. Disabling a channel at the module level stops that module’s sends and leaves other modules unaffected.
  7. simulate writes nothing and returns both the resolved channels and the reasons for the rest.
  8. Negative: no configuration anywhere disables the consent or suppression gate.

Execution

Single slice, synchronous command. The pure function lives in packages/core beside the stacking evaluator; the configuration tables and endpoints in backend/api. This is part of TS-002.

Open questions

Changelog

Delivery record

Not implemented yet.