//! Mailing lists: what a list is attached to, what it carries, and every state //! a subscription to one can be in. use super::str_enum::impl_str_enum; use serde::{Deserialize, Serialize}; // --- Mailing Lists --- #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum MailingListType { Content, Devlog, Patches, } impl_str_enum!(MailingListType { Content => "content", Devlog => "devlog", Patches => "patches", }); // --- Mailing lists (wiki: mnw-mailing-lists) --- /// The legacy per-project list types map onto the unified kinds one-for-one. /// Kept as a conversion rather than merging the two enums, because /// `MailingListType` is pinned by a CHECK on the old table and will be dropped /// with it rather than grown. impl From for ListKind { fn from(t: MailingListType) -> Self { match t { MailingListType::Content => Self::Content, MailingListType::Devlog => Self::Devlog, MailingListType::Patches => Self::Patches, } } } /// What a list is attached to. `Platform` lists have no `scope_id`; every other /// scope requires one, and the database enforces the pairing. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ListScope { Platform, Project, Repo, Creator, } impl_str_enum!(ListScope { Platform => "platform", Project => "project", Repo => "repo", Creator => "creator", }); /// What the list carries. Kinds are deliberately coarse: the unsubscribe page /// shows one row per list, and a subscriber who has to reason about fifteen /// near-identical kinds will unsubscribe from all of them. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ListKind { Content, Devlog, Patches, Releases, Issues, Announce, Marketing, // Account notification preferences, each mirroring a users.notify_* column. // Releases and Issues above double as these at platform scope; the rest are // their own kinds. Sale, Follower, Login, Status, Tip, // Someone redeemed one of your invite codes. Added by migration 195, when // that notice stopped being mail you could not turn off. Invite, } impl_str_enum!(ListKind { Content => "content", Devlog => "devlog", Patches => "patches", Releases => "releases", Issues => "issues", Announce => "announce", Marketing => "marketing", Sale => "sale", Follower => "follower", Login => "login", Status => "status", Tip => "tip", Invite => "invite", }); impl ListKind { /// Every kind. Exists so a test can assert the enum against the /// `lists_kind_check` constraint: a kind lives in two places, and adding it /// to only one fails at INSERT on a deployed database rather than at /// compile time here. pub const ALL: &'static [ListKind] = &[ ListKind::Content, ListKind::Devlog, ListKind::Patches, ListKind::Releases, ListKind::Issues, ListKind::Announce, ListKind::Marketing, ListKind::Sale, ListKind::Follower, ListKind::Login, ListKind::Status, ListKind::Tip, ListKind::Invite, ]; } /// What is waiting to be acknowledged. /// /// Closed and asserted against the `pending_acknowledgements.kind` CHECK /// constraint by a test in `db::acknowledgements`: a variant added here without /// a migration compiles, passes every unit test, and then fails at INSERT on a /// deployed database. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum AckKind { /// The creator's Stripe account changed settlement currency, so every price /// they have already set is now a number denominated in different money. SettlementCurrencyChanged, } impl_str_enum!(AckKind { SettlementCurrencyChanged => "settlement_currency_changed", }); impl AckKind { pub const ALL: &'static [AckKind] = &[AckKind::SettlementCurrencyChanged]; /// Subject line and page heading, one source for both. pub fn title(self) -> &'static str { match self { Self::SettlementCurrencyChanged => "Your prices are now in a different currency", } } } /// Where a subscription stands. /// /// `Imported` is its own state on purpose. Everything the step-2 backfill /// carried over predates any consent record: calling it `Confirmed` would /// manufacture evidence we do not have, and calling it `Pending` would assert a /// double-opt-in is in flight when none is. The state says exactly what is /// known and no more. /// /// It is provenance, not a quarantine. An imported subscriber may be mailed: /// sendable, marketing included, and no re-confirmation pass is coming. See /// `db::lists::SENDABLE_STATES` for the reasoning. The /// state stays distinct because "we carried this over" is worth being able to /// say years later, not because anything gates on it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SubscriptionState { Pending, Confirmed, Imported, Unsubscribed, Bounced, } impl_str_enum!(SubscriptionState { Pending => "pending", Confirmed => "confirmed", Imported => "imported", Unsubscribed => "unsubscribed", Bounced => "bounced", }); /// How the subscription was created, which the table this replaces could not /// say. /// /// Recorded for a re-confirmation pass that was then decided against (GoingsOn /// 04a882b4). It stays because "where did this address come from" is the first /// question asked of a complaint, and answering it is worth one column whether /// or not anything ever filters on it. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum SubscriptionSource { LandingForm, ProjectPage, Checkout, Import, Admin, Api, } impl_str_enum!(SubscriptionSource { LandingForm => "landing_form", ProjectPage => "project_page", Checkout => "checkout", Import => "import", Admin => "admin", Api => "api", }); /// An entry in a subscription's consent history. The table is append-only: an /// opt-out adds a row rather than editing the opt-in that came before, and a /// database trigger rejects `UPDATE` so that stays true. #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum ConsentEvent { OptIn, Confirm, OptOut, Bounce, Complaint, AdminRemoval, Import, } impl_str_enum!(ConsentEvent { OptIn => "opt_in", Confirm => "confirm", OptOut => "opt_out", Bounce => "bounce", Complaint => "complaint", AdminRemoval => "admin_removal", Import => "import", });