Notification preferences
Notification preferences
Not every lifecycle email is the customer's to switch off. Cbox Billing splits the transactional mails into two classes and lets a customer manage exactly the ones that are courtesies — from the customer portal, no support ticket required.
Optional vs mandatory
The split is a single source of truth on MailEventType::isOptional(), so the notifier's
gate and the portal's toggle list can never drift:
| Class | Events | Customer can disable? |
|---|---|---|
| Optional | Renewal reminder · Trial ending · Payment receipt | Yes |
| Mandatory | Invoice issued · Past-due / dunning · Payment retry · Subscription changed · License delivered · Plan retiring | No — always sent |
Mandatory mails are transactional or legal (a customer must always receive an invoice, a dunning notice, or a cancellation confirmation), so they ignore preferences entirely.
Where preferences live
Preferences are stored per organization in notification_preferences — one row per
(organization_id, event_type). Absence of a row means opted-in: a customer who never
touches the toggles still receives every courtesy mail, and a row is written only when they
change a default. Mandatory events are never written to this table.
The notifier seam
BillingNotifier consults ManagesNotificationPreferences::allows() in its private send()
seam before an optional mail leaves. An optional mail to an opted-out org is suppressed
and logged (never delivered); a mandatory mail passes no gate and always sends. In test mode
the same optional gate applies before a mail is captured, so a suppressed optional mail is
neither delivered nor captured.
optional mail → allows(org, event)? → yes → deliver / capture
→ no → suppress + log
mandatory mail → (no gate) → deliver / capture
The portal surface
The portal renders the optional events as toggles (with their current opt-in state) and
lists the mandatory events as "Always sent". Flipping a toggle posts to
POST /billing/portal/{token}/notifications with {event, opted_in}, scoped to the session
org; a request to toggle a mandatory event is refused (422).