Skip to main content

max / makenotwork

11.1 KB · 384 lines History Blame Raw
1 //! The storefront around the files: blog posts, promo codes, license keys,
2 //! subscription tiers, collections, and the broadcast that tells buyers about
3 //! any of it.
4
5 use super::{MnwApiClient, empty_response, json_response};
6 use serde::{Deserialize, Serialize};
7
8 /// A blog post summary.
9 #[derive(Debug, Clone, Deserialize, Serialize)]
10 pub(crate) struct BlogPost {
11 pub id: String,
12 pub title: String,
13 pub slug: String,
14 pub is_published: bool,
15 pub publish_at: Option<String>,
16 pub created_at: String,
17 pub updated_at: String,
18 }
19
20 /// A promo code.
21 #[derive(Debug, Clone, Deserialize, Serialize)]
22 pub(crate) struct PromoCode {
23 pub id: String,
24 pub code: String,
25 pub code_purpose: String,
26 pub discount_type: Option<String>,
27 pub discount_value: Option<i32>,
28 pub item_title: Option<String>,
29 pub project_title: Option<String>,
30 pub max_uses: Option<i32>,
31 pub use_count: i32,
32 pub created_at: String,
33 }
34
35 /// A license key.
36 #[derive(Debug, Clone, Deserialize, Serialize)]
37 pub(crate) struct LicenseKey {
38 pub id: String,
39 pub key_code: String,
40 pub activation_count: i32,
41 pub max_activations: Option<i32>,
42 pub is_revoked: bool,
43 pub created_at: String,
44 }
45
46 /// Result of a broadcast send.
47 #[derive(Debug, Deserialize)]
48 #[allow(dead_code)] // wire contract: every field the endpoint returns, read or not
49 pub(crate) struct BroadcastResult {
50 pub success: bool,
51 pub recipient_count: usize,
52 }
53
54 /// A subscription tier.
55 #[derive(Debug, Clone, Deserialize, Serialize)]
56 pub(crate) struct TierInfo {
57 pub id: String,
58 pub name: String,
59 pub description: String,
60 pub price_cents: i32,
61 pub is_active: bool,
62 }
63
64 /// A collection.
65 #[derive(Debug, Clone, Deserialize, Serialize)]
66 pub(crate) struct CollectionInfo {
67 pub id: String,
68 pub slug: String,
69 pub title: String,
70 pub description: String,
71 pub is_public: bool,
72 pub item_count: i64,
73 }
74
75 impl MnwApiClient {
76 /// List blog posts for a project.
77 pub(crate) async fn list_blog_posts(
78 &self,
79 user_id: &str,
80 project_id: &str,
81 ) -> anyhow::Result<Vec<BlogPost>> {
82 let url = format!(
83 "{}/api/internal/creator/projects/{}/blog",
84 self.base_url, project_id
85 );
86 let resp = self
87 .http
88 .get(&url)
89 .bearer_auth(&self.service_token)
90 .header("X-MNW-Actor", self.actor_header())
91 .query(&[("user_id", user_id)])
92 .send()
93 .await?;
94
95 json_response(resp, "list_blog_posts").await
96 }
97
98 /// Create a blog post, optionally scheduled for future publication.
99 pub(crate) async fn create_blog_post(
100 &self,
101 user_id: &str,
102 project_id: &str,
103 title: &str,
104 body_markdown: &str,
105 publish: bool,
106 publish_at: Option<&str>,
107 ) -> anyhow::Result<BlogPost> {
108 let url = format!("{}/api/internal/creator/blog", self.base_url);
109 let mut body = serde_json::json!({
110 "user_id": user_id,
111 "project_id": project_id,
112 "title": title,
113 "body_markdown": body_markdown,
114 "publish": publish,
115 });
116 if let Some(pa) = publish_at {
117 body["publish_at"] = serde_json::Value::String(pa.to_string());
118 }
119 let resp = self
120 .http
121 .post(&url)
122 .bearer_auth(&self.service_token)
123 .header("X-MNW-Actor", self.actor_header())
124 .json(&body)
125 .send()
126 .await?;
127
128 json_response(resp, "create_blog_post").await
129 }
130
131 /// Delete a blog post.
132 pub(crate) async fn delete_blog_post(
133 &self,
134 user_id: &str,
135 post_id: &str,
136 ) -> anyhow::Result<()> {
137 let url = format!("{}/api/internal/creator/blog/{}", self.base_url, post_id);
138 let resp = self
139 .http
140 .delete(&url)
141 .bearer_auth(&self.service_token)
142 .header("X-MNW-Actor", self.actor_header())
143 .query(&[("user_id", user_id)])
144 .send()
145 .await?;
146
147 empty_response(resp, "delete_blog_post").await
148 }
149
150 /// List promo codes for a creator.
151 pub(crate) async fn list_promo_codes(&self, user_id: &str) -> anyhow::Result<Vec<PromoCode>> {
152 let url = format!("{}/api/internal/creator/promo-codes", self.base_url);
153 let resp = self
154 .http
155 .get(&url)
156 .bearer_auth(&self.service_token)
157 .header("X-MNW-Actor", self.actor_header())
158 .query(&[("user_id", user_id)])
159 .send()
160 .await?;
161
162 json_response(resp, "list_promo_codes").await
163 }
164
165 /// Create a promo code.
166 pub(crate) async fn create_promo_code(
167 &self,
168 user_id: &str,
169 code: &str,
170 discount_type: &str,
171 discount_value: i32,
172 max_uses: Option<i32>,
173 project_id: Option<&str>,
174 ) -> anyhow::Result<PromoCode> {
175 let url = format!("{}/api/internal/creator/promo-codes", self.base_url);
176 let mut body = serde_json::json!({
177 "user_id": user_id,
178 "code": code,
179 "code_purpose": "discount",
180 "discount_type": discount_type,
181 "discount_value": discount_value,
182 });
183 if let Some(max) = max_uses {
184 body["max_uses"] = serde_json::json!(max);
185 }
186 if let Some(pid) = project_id {
187 body["project_id"] = serde_json::json!(pid);
188 }
189
190 let resp = self
191 .http
192 .post(&url)
193 .bearer_auth(&self.service_token)
194 .header("X-MNW-Actor", self.actor_header())
195 .json(&body)
196 .send()
197 .await?;
198
199 json_response(resp, "create_promo_code").await
200 }
201
202 /// Delete a promo code.
203 pub(crate) async fn delete_promo_code(
204 &self,
205 user_id: &str,
206 code_id: &str,
207 ) -> anyhow::Result<()> {
208 let url = format!(
209 "{}/api/internal/creator/promo-codes/{}",
210 self.base_url, code_id
211 );
212 let resp = self
213 .http
214 .delete(&url)
215 .bearer_auth(&self.service_token)
216 .header("X-MNW-Actor", self.actor_header())
217 .query(&[("user_id", user_id)])
218 .send()
219 .await?;
220
221 empty_response(resp, "delete_promo_code").await
222 }
223
224 /// List license keys for an item.
225 pub(crate) async fn list_license_keys(
226 &self,
227 user_id: &str,
228 item_id: &str,
229 ) -> anyhow::Result<Vec<LicenseKey>> {
230 let url = format!(
231 "{}/api/internal/creator/items/{}/keys",
232 self.base_url, item_id
233 );
234 let resp = self
235 .http
236 .get(&url)
237 .bearer_auth(&self.service_token)
238 .header("X-MNW-Actor", self.actor_header())
239 .query(&[("user_id", user_id)])
240 .send()
241 .await?;
242
243 json_response(resp, "list_license_keys").await
244 }
245
246 /// Generate a new license key for an item.
247 pub(crate) async fn generate_license_key(
248 &self,
249 user_id: &str,
250 item_id: &str,
251 ) -> anyhow::Result<LicenseKey> {
252 let url = format!(
253 "{}/api/internal/creator/items/{}/keys",
254 self.base_url, item_id
255 );
256 let resp = self
257 .http
258 .post(&url)
259 .bearer_auth(&self.service_token)
260 .header("X-MNW-Actor", self.actor_header())
261 .json(&serde_json::json!({ "user_id": user_id }))
262 .send()
263 .await?;
264
265 json_response(resp, "generate_license_key").await
266 }
267
268 /// Revoke a license key.
269 pub(crate) async fn revoke_license_key(
270 &self,
271 user_id: &str,
272 key_id: &str,
273 ) -> anyhow::Result<()> {
274 let url = format!(
275 "{}/api/internal/creator/keys/{}/revoke",
276 self.base_url, key_id
277 );
278 let resp = self
279 .http
280 .post(&url)
281 .bearer_auth(&self.service_token)
282 .header("X-MNW-Actor", self.actor_header())
283 .json(&serde_json::json!({ "user_id": user_id }))
284 .send()
285 .await?;
286
287 empty_response(resp, "revoke_license_key").await
288 }
289
290 pub(crate) async fn send_broadcast(
291 &self,
292 user_id: &str,
293 subject: &str,
294 body: &str,
295 ) -> anyhow::Result<BroadcastResult> {
296 let url = format!("{}/api/internal/creator/broadcast", self.base_url);
297 let resp = self
298 .http
299 .post(&url)
300 .bearer_auth(&self.service_token)
301 .header("X-MNW-Actor", self.actor_header())
302 .json(&serde_json::json!({"user_id": user_id, "subject": subject, "body": body}))
303 .send()
304 .await?;
305 json_response(resp, "send_broadcast").await
306 }
307
308 pub(crate) async fn list_tiers(
309 &self,
310 user_id: &str,
311 project_id: &str,
312 ) -> anyhow::Result<Vec<TierInfo>> {
313 let url = format!(
314 "{}/api/internal/creator/projects/{}/tiers",
315 self.base_url, project_id
316 );
317 let resp = self
318 .http
319 .get(&url)
320 .bearer_auth(&self.service_token)
321 .header("X-MNW-Actor", self.actor_header())
322 .query(&[("user_id", user_id)])
323 .send()
324 .await?;
325 json_response(resp, "list_tiers").await
326 }
327
328 pub(crate) async fn list_collections(
329 &self,
330 user_id: &str,
331 ) -> anyhow::Result<Vec<CollectionInfo>> {
332 let url = format!("{}/api/internal/creator/collections", self.base_url);
333 let resp = self
334 .http
335 .get(&url)
336 .bearer_auth(&self.service_token)
337 .header("X-MNW-Actor", self.actor_header())
338 .query(&[("user_id", user_id)])
339 .send()
340 .await?;
341 json_response(resp, "list_collections").await
342 }
343
344 #[allow(dead_code)]
345 pub(crate) async fn create_collection(
346 &self,
347 user_id: &str,
348 slug: &str,
349 title: &str,
350 ) -> anyhow::Result<serde_json::Value> {
351 let url = format!("{}/api/internal/creator/collections", self.base_url);
352 let resp = self
353 .http
354 .post(&url)
355 .bearer_auth(&self.service_token)
356 .header("X-MNW-Actor", self.actor_header())
357 .json(&serde_json::json!({"user_id": user_id, "slug": slug, "title": title}))
358 .send()
359 .await?;
360 json_response(resp, "create_collection").await
361 }
362
363 #[allow(dead_code)]
364 pub(crate) async fn delete_collection(
365 &self,
366 user_id: &str,
367 collection_id: &str,
368 ) -> anyhow::Result<()> {
369 let url = format!(
370 "{}/api/internal/creator/collections/{}",
371 self.base_url, collection_id
372 );
373 let resp = self
374 .http
375 .delete(&url)
376 .bearer_auth(&self.service_token)
377 .header("X-MNW-Actor", self.actor_header())
378 .query(&[("user_id", user_id)])
379 .send()
380 .await?;
381 empty_response(resp, "delete_collection").await
382 }
383 }
384