AshDispatch.Transports.Preferences (AshDispatch v0.7.0)

View Source

The opt-out gate, in one place.

Every transport that reaches a person must ask the same question before it delivers: did this recipient say no? Until 0.7.0 only :email, :in_app and :webhook asked. :slack, :discord, :sms and :push did not — which meant a preference honoured on one channel was silently ignored on another, and the difference was invisible to the person who set it.

Why a shared function rather than a copied block

The check is eight lines, and eight lines copied six times is six chances to drift. The copies had already begun to: :in_app logged the user id, :webhook logged the receipt id, and neither said which was intended. One implementation makes the verdict, the skip reason and the log line the same everywhere — and "user_opted_out" becomes a value you can count.

The verdict is per RECIPIENT, not per event

A receipt is one recipient. allows_receipt?/4 reads the receipt's own user_id; reading context.user would apply the event subject's verdict to all N recipients of a fan-out — the bug this function exists to make impossible to reintroduce.

Summary

Functions

The reason written on a receipt skipped by an opt-out. Public so it can be counted.

Runs fun when the recipient allows this delivery, otherwise skips.

Functions

reason()

@spec reason() :: String.t()

The reason written on a receipt skipped by an opt-out. Public so it can be counted.

with_consent(receipt, context, channel, event_config, fun)

@spec with_consent(map(), map(), map(), keyword(), (-> term())) :: term()

Runs fun when the recipient allows this delivery, otherwise skips.

The skipped receipt is returned as {:ok, receipt} — a skip is a successful outcome, not a failure. The person said no, and we did what they asked.