Skip to main content

max / makenotwork

6.6 KB · 202 lines History Blame Raw
1 //! Waitlist, invite code, and creator wave models.
2
3 use chrono::{DateTime, Utc};
4 use serde::Serialize;
5 use sqlx::FromRow;
6
7 use super::super::id_types::*;
8 use super::super::validated_types::*;
9
10 /// Approved-entry state: fields that are always present when
11 /// `status == Approved`.
12 #[derive(Debug, Clone)]
13 pub struct ApprovedEntryInfo {
14 /// How this entry was selected (hand-picked or lottery).
15 pub selection_method: super::super::SelectionMethod,
16 /// Creator wave this entry was included in.
17 pub wave_id: CreatorWaveId,
18 /// When the admin approved this entry.
19 pub reviewed_at: DateTime<Utc>,
20 }
21
22 /// A creator invite code for controlled alpha growth.
23 #[derive(Debug, Clone, FromRow, Serialize)]
24 pub struct DbInviteCode {
25 pub id: InviteCodeId,
26 pub code: String,
27 pub creator_id: UserId,
28 pub redeemed_by_id: Option<UserId>,
29 pub redeemed_at: Option<DateTime<Utc>>,
30 pub created_at: DateTime<Utc>,
31 }
32
33 /// A creator waitlist application.
34 ///
35 /// **State invariant:** When `status == Approved`, `selection_method`,
36 /// `wave_id`, and `reviewed_at` are all `Some`. When `status == Spam`,
37 /// `reviewed_at` may be `Some` but `selection_method` and `wave_id` are
38 /// `None`. When `status == Pending`, all three are `None`.
39 #[derive(Debug, Clone, FromRow, Serialize)]
40 pub struct DbWaitlistEntry {
41 /// Database primary key.
42 pub id: WaitlistEntryId,
43 /// Applicant user ID.
44 pub user_id: UserId,
45 /// Free-text pitch/description from the applicant. NULL for invited users.
46 pub pitch: Option<String>,
47 /// Current status.
48 pub status: super::super::WaitlistStatus,
49 /// How the entry was approved. Present only when `status == Approved`.
50 pub selection_method: Option<super::super::SelectionMethod>,
51 /// Creator wave this entry was included in. Present only when `status == Approved`.
52 pub wave_id: Option<CreatorWaveId>,
53 /// Internal admin note.
54 pub admin_note: Option<String>,
55 /// When the application was submitted.
56 pub created_at: DateTime<Utc>,
57 /// When an admin reviewed this entry. Present when `status != Pending`.
58 pub reviewed_at: Option<DateTime<Utc>>,
59 /// User who invited this applicant (if invited).
60 pub invited_by_user_id: Option<UserId>,
61 }
62
63 impl DbWaitlistEntry {
64 /// Extract approved-state fields as a coherent unit.
65 ///
66 /// Returns `Some` only for approved entries.
67 pub fn approved_info(&self) -> Option<ApprovedEntryInfo> {
68 Some(ApprovedEntryInfo {
69 selection_method: self.selection_method?,
70 wave_id: self.wave_id?,
71 reviewed_at: self.reviewed_at?,
72 })
73 }
74 }
75
76 /// A batch ("wave") of creators granted access together.
77 #[derive(Debug, Clone, FromRow, Serialize)]
78 pub struct DbCreatorWave {
79 /// Database primary key.
80 pub id: CreatorWaveId,
81 /// Sequential wave number (1, 2, 3, ...).
82 pub wave_number: i32,
83 /// Number of hand-picked creators in this wave.
84 pub hand_picked_count: i32,
85 /// Number of lottery-selected creators in this wave.
86 pub lottery_count: i32,
87 /// Total eligible applicants at the time of this wave.
88 pub total_eligible: i32,
89 /// Optional admin note about this wave.
90 pub note: Option<String>,
91 /// When this wave was created.
92 pub created_at: DateTime<Utc>,
93 }
94
95 /// A waitlist entry enriched with user info for the admin dashboard.
96 #[derive(Debug, Clone, FromRow)]
97 #[allow(dead_code)] // Fields populated by sqlx query, read during type conversion
98 pub struct DbAdminWaitlistRow {
99 /// Waitlist entry primary key.
100 pub id: WaitlistEntryId,
101 /// Applicant user ID.
102 pub user_id: UserId,
103 /// Applicant's pitch text. NULL for invited users.
104 pub pitch: Option<String>,
105 /// Current status.
106 pub status: super::super::WaitlistStatus,
107 /// Selection method if approved.
108 pub selection_method: Option<super::super::SelectionMethod>,
109 /// Internal admin note.
110 pub admin_note: Option<String>,
111 /// When the application was submitted.
112 pub created_at: DateTime<Utc>,
113 /// When an admin reviewed this entry.
114 pub reviewed_at: Option<DateTime<Utc>>,
115 // Joined from users
116 /// Applicant's username (joined from users).
117 pub username: Username,
118 /// Applicant's email (joined from users).
119 pub email: Email,
120 /// Whether the applicant's email is verified (joined from users).
121 pub email_verified: bool,
122 /// User who invited this applicant (if invited).
123 pub invited_by_user_id: Option<UserId>,
124 /// Username of the inviter (joined from users).
125 pub invited_by_username: Option<String>,
126 }
127
128 /// Aggregate counts of waitlist entries by status.
129 #[derive(Debug, Clone, FromRow)]
130 pub struct DbWaitlistStats {
131 /// Number of entries awaiting review.
132 pub pending: i64,
133 /// Number of approved entries.
134 pub approved: i64,
135 /// Number of entries marked as spam.
136 pub spam: i64,
137 }
138
139 #[cfg(test)]
140 mod tests {
141 use super::*;
142
143 fn make_waitlist_entry(
144 status: super::super::super::WaitlistStatus,
145 method: Option<super::super::super::SelectionMethod>,
146 wave: Option<CreatorWaveId>,
147 reviewed: Option<DateTime<Utc>>,
148 ) -> DbWaitlistEntry {
149 DbWaitlistEntry {
150 id: WaitlistEntryId::nil(),
151 user_id: UserId::nil(),
152 pitch: Some("test".to_string()),
153 status,
154 selection_method: method,
155 wave_id: wave,
156 admin_note: None,
157 created_at: Utc::now(),
158 reviewed_at: reviewed,
159 invited_by_user_id: None,
160 }
161 }
162
163 #[test]
164 fn approved_info_for_approved_entry() {
165 let wave = CreatorWaveId::new();
166 let now = Utc::now();
167 let e = make_waitlist_entry(
168 super::super::super::WaitlistStatus::Approved,
169 Some(super::super::super::SelectionMethod::HandPicked),
170 Some(wave),
171 Some(now),
172 );
173 let info = e.approved_info().unwrap();
174 assert_eq!(info.selection_method, super::super::super::SelectionMethod::HandPicked);
175 assert_eq!(info.wave_id, wave);
176 assert_eq!(info.reviewed_at, now);
177 }
178
179 #[test]
180 fn approved_info_none_for_pending() {
181 let e = make_waitlist_entry(
182 super::super::super::WaitlistStatus::Pending,
183 None,
184 None,
185 None,
186 );
187 assert!(e.approved_info().is_none());
188 }
189
190 #[test]
191 fn approved_info_none_for_spam() {
192 // Spam has reviewed_at but no selection_method or wave_id
193 let e = make_waitlist_entry(
194 super::super::super::WaitlistStatus::Spam,
195 None,
196 None,
197 Some(Utc::now()),
198 );
199 assert!(e.approved_info().is_none());
200 }
201 }
202