Skip to main content

max / makenotwork

6.5 KB · 226 lines History Blame Raw
1 //! Mailing lists: what a list is attached to, what it carries, and every state
2 //! a subscription to one can be in.
3
4 use super::str_enum::impl_str_enum;
5 use serde::{Deserialize, Serialize};
6
7 // --- Mailing Lists ---
8
9 #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
10 #[serde(rename_all = "lowercase")]
11 pub enum MailingListType {
12 Content,
13 Devlog,
14 Patches,
15 }
16
17 impl_str_enum!(MailingListType {
18 Content => "content",
19 Devlog => "devlog",
20 Patches => "patches",
21 });
22
23 // --- Mailing lists (wiki: mnw-mailing-lists) ---
24
25 /// The legacy per-project list types map onto the unified kinds one-for-one.
26 /// Kept as a conversion rather than merging the two enums, because
27 /// `MailingListType` is pinned by a CHECK on the old table and will be dropped
28 /// with it rather than grown.
29 impl From<MailingListType> for ListKind {
30 fn from(t: MailingListType) -> Self {
31 match t {
32 MailingListType::Content => Self::Content,
33 MailingListType::Devlog => Self::Devlog,
34 MailingListType::Patches => Self::Patches,
35 }
36 }
37 }
38
39 /// What a list is attached to. `Platform` lists have no `scope_id`; every other
40 /// scope requires one, and the database enforces the pairing.
41 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
42 pub enum ListScope {
43 Platform,
44 Project,
45 Repo,
46 Creator,
47 }
48
49 impl_str_enum!(ListScope {
50 Platform => "platform",
51 Project => "project",
52 Repo => "repo",
53 Creator => "creator",
54 });
55
56 /// What the list carries. Kinds are deliberately coarse: the unsubscribe page
57 /// shows one row per list, and a subscriber who has to reason about fifteen
58 /// near-identical kinds will unsubscribe from all of them.
59 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
60 pub enum ListKind {
61 Content,
62 Devlog,
63 Patches,
64 Releases,
65 Issues,
66 Announce,
67 Marketing,
68 // Account notification preferences, each mirroring a users.notify_* column.
69 // Releases and Issues above double as these at platform scope; the rest are
70 // their own kinds.
71 Sale,
72 Follower,
73 Login,
74 Status,
75 Tip,
76 // Someone redeemed one of your invite codes. Added by migration 195, when
77 // that notice stopped being mail you could not turn off.
78 Invite,
79 }
80
81 impl_str_enum!(ListKind {
82 Content => "content",
83 Devlog => "devlog",
84 Patches => "patches",
85 Releases => "releases",
86 Issues => "issues",
87 Announce => "announce",
88 Marketing => "marketing",
89 Sale => "sale",
90 Follower => "follower",
91 Login => "login",
92 Status => "status",
93 Tip => "tip",
94 Invite => "invite",
95 });
96
97 impl ListKind {
98 /// Every kind. Exists so a test can assert the enum against the
99 /// `lists_kind_check` constraint: a kind lives in two places, and adding it
100 /// to only one fails at INSERT on a deployed database rather than at
101 /// compile time here.
102 pub const ALL: &'static [ListKind] = &[
103 ListKind::Content,
104 ListKind::Devlog,
105 ListKind::Patches,
106 ListKind::Releases,
107 ListKind::Issues,
108 ListKind::Announce,
109 ListKind::Marketing,
110 ListKind::Sale,
111 ListKind::Follower,
112 ListKind::Login,
113 ListKind::Status,
114 ListKind::Tip,
115 ListKind::Invite,
116 ];
117 }
118
119 /// What is waiting to be acknowledged.
120 ///
121 /// Closed and asserted against the `pending_acknowledgements.kind` CHECK
122 /// constraint by a test in `db::acknowledgements`: a variant added here without
123 /// a migration compiles, passes every unit test, and then fails at INSERT on a
124 /// deployed database.
125 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
126 pub enum AckKind {
127 /// The creator's Stripe account changed settlement currency, so every price
128 /// they have already set is now a number denominated in different money.
129 SettlementCurrencyChanged,
130 }
131
132 impl_str_enum!(AckKind {
133 SettlementCurrencyChanged => "settlement_currency_changed",
134 });
135
136 impl AckKind {
137 pub const ALL: &'static [AckKind] = &[AckKind::SettlementCurrencyChanged];
138
139 /// Subject line and page heading, one source for both.
140 pub fn title(self) -> &'static str {
141 match self {
142 Self::SettlementCurrencyChanged => "Your prices are now in a different currency",
143 }
144 }
145 }
146
147 /// Where a subscription stands.
148 ///
149 /// `Imported` is its own state on purpose. Everything the step-2 backfill
150 /// carried over predates any consent record: calling it `Confirmed` would
151 /// manufacture evidence we do not have, and calling it `Pending` would assert a
152 /// double-opt-in is in flight when none is. The state says exactly what is
153 /// known and no more.
154 ///
155 /// It is provenance, not a quarantine. An imported subscriber may be mailed:
156 /// sendable, marketing included, and no re-confirmation pass is coming. See
157 /// `db::lists::SENDABLE_STATES` for the reasoning. The
158 /// state stays distinct because "we carried this over" is worth being able to
159 /// say years later, not because anything gates on it.
160 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
161 pub enum SubscriptionState {
162 Pending,
163 Confirmed,
164 Imported,
165 Unsubscribed,
166 Bounced,
167 }
168
169 impl_str_enum!(SubscriptionState {
170 Pending => "pending",
171 Confirmed => "confirmed",
172 Imported => "imported",
173 Unsubscribed => "unsubscribed",
174 Bounced => "bounced",
175 });
176
177 /// How the subscription was created, which the table this replaces could not
178 /// say.
179 ///
180 /// Recorded for a re-confirmation pass that was then decided against (GoingsOn
181 /// 04a882b4). It stays because "where did this address come from" is the first
182 /// question asked of a complaint, and answering it is worth one column whether
183 /// or not anything ever filters on it.
184 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
185 pub enum SubscriptionSource {
186 LandingForm,
187 ProjectPage,
188 Checkout,
189 Import,
190 Admin,
191 Api,
192 }
193
194 impl_str_enum!(SubscriptionSource {
195 LandingForm => "landing_form",
196 ProjectPage => "project_page",
197 Checkout => "checkout",
198 Import => "import",
199 Admin => "admin",
200 Api => "api",
201 });
202
203 /// An entry in a subscription's consent history. The table is append-only: an
204 /// opt-out adds a row rather than editing the opt-in that came before, and a
205 /// database trigger rejects `UPDATE` so that stays true.
206 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
207 pub enum ConsentEvent {
208 OptIn,
209 Confirm,
210 OptOut,
211 Bounce,
212 Complaint,
213 AdminRemoval,
214 Import,
215 }
216
217 impl_str_enum!(ConsentEvent {
218 OptIn => "opt_in",
219 Confirm => "confirm",
220 OptOut => "opt_out",
221 Bounce => "bounce",
222 Complaint => "complaint",
223 AdminRemoval => "admin_removal",
224 Import => "import",
225 });
226