Skip to main content

max / makenotwork

5.8 KB · 185 lines History Blame Raw
1 //! Projects: the creator's top-level containers, their item lists, and the
2 //! storage and revenue totals the dashboard reads.
3
4 use super::{MnwApiClient, json_response, revenue_of};
5 use crate::currency::{Currency, RevenueByCurrency};
6 use serde::{Deserialize, Serialize};
7 use std::collections::BTreeMap;
8
9 /// A creator's project with item count and revenue.
10 #[derive(Debug, Clone, Deserialize, Serialize)]
11 #[allow(
12 clippy::struct_field_names,
13 reason = "project_type mirrors the server JSON/DB schema; renaming would diverge the field from the wire contract"
14 )]
15 pub(crate) struct Project {
16 pub id: String,
17 pub slug: String,
18 pub title: String,
19 pub project_type: String,
20 pub is_public: bool,
21 pub item_count: i64,
22 /// Revenue in `currency` — the project's largest single-currency total, not
23 /// a sum across currencies. Only meaningful next to `currency`.
24 pub revenue_cents: i64,
25 /// The currency `revenue_cents` is denominated in. A project can earn in a
26 /// currency that is not the viewer's: revenue splits are paid in the
27 /// currency of the project that generated them.
28 #[serde(default)]
29 pub currency: Currency,
30 /// Every currency this project earned in, keyed by lowercase ISO code.
31 /// Normally one entry matching `revenue_cents`; empty from a server that
32 /// predates the field.
33 #[serde(default)]
34 pub revenue_cents_by_currency: BTreeMap<String, i64>,
35 }
36
37 impl Project {
38 /// Revenue across every currency it was earned in.
39 pub(crate) fn revenue(&self) -> RevenueByCurrency {
40 revenue_of(
41 self.revenue_cents,
42 self.currency,
43 &self.revenue_cents_by_currency,
44 )
45 }
46 }
47
48 /// An item within a project.
49 #[derive(Debug, Clone, Deserialize, Serialize)]
50 #[allow(
51 clippy::struct_field_names,
52 reason = "item_type mirrors the server JSON/DB schema; renaming would diverge the field from the wire contract"
53 )]
54 pub(crate) struct Item {
55 pub id: String,
56 pub title: String,
57 pub item_type: String,
58 pub price_cents: i32,
59 pub is_public: bool,
60 pub sort_order: i32,
61 }
62
63 /// Period comparison stats for the creator.
64 #[derive(Debug, Clone, Deserialize, Serialize)]
65 pub(crate) struct CreatorStats {
66 pub current_revenue_cents: i64,
67 pub previous_revenue_cents: i64,
68 pub current_sales: i64,
69 pub previous_sales: i64,
70 pub current_followers: i64,
71 pub previous_followers: i64,
72 pub total_projects: i64,
73 pub total_items: i64,
74 }
75
76 /// Response from the storage-info internal endpoint.
77 #[derive(Debug, Clone, Deserialize, Serialize)]
78 pub(crate) struct StorageInfo {
79 pub storage_used_bytes: i64,
80 pub max_storage_bytes: i64,
81 pub allows_file_uploads: bool,
82 }
83
84 impl MnwApiClient {
85 /// Fetch all projects for a creator with item counts and revenue.
86 pub(crate) async fn get_projects(&self, user_id: &str) -> anyhow::Result<Vec<Project>> {
87 let url = format!("{}/api/internal/creator/projects", self.base_url);
88 let resp = self
89 .http
90 .get(&url)
91 .bearer_auth(&self.service_token)
92 .header("X-MNW-Actor", self.actor_header())
93 .query(&[("user_id", user_id)])
94 .send()
95 .await?;
96
97 json_response(resp, "get_projects").await
98 }
99
100 /// Create a new project.
101 pub(crate) async fn create_project(
102 &self,
103 user_id: &str,
104 title: &str,
105 project_type: &str,
106 description: Option<&str>,
107 ) -> anyhow::Result<Project> {
108 let url = format!("{}/api/internal/creator/projects", self.base_url);
109 let mut body = serde_json::json!({
110 "user_id": user_id,
111 "title": title,
112 "project_type": project_type,
113 });
114 if let Some(desc) = description {
115 body["description"] = serde_json::Value::String(desc.to_string());
116 }
117 let resp = self
118 .http
119 .post(&url)
120 .bearer_auth(&self.service_token)
121 .header("X-MNW-Actor", self.actor_header())
122 .json(&body)
123 .send()
124 .await?;
125
126 json_response(resp, "create_project").await
127 }
128
129 /// Fetch items in a project.
130 pub(crate) async fn get_project_items(
131 &self,
132 project_id: &str,
133 user_id: &str,
134 ) -> anyhow::Result<Vec<Item>> {
135 let url = format!(
136 "{}/api/internal/creator/projects/{}/items",
137 self.base_url, project_id
138 );
139 let resp = self
140 .http
141 .get(&url)
142 .bearer_auth(&self.service_token)
143 .header("X-MNW-Actor", self.actor_header())
144 .query(&[("user_id", user_id)])
145 .send()
146 .await?;
147
148 json_response(resp, "get_project_items").await
149 }
150
151 /// Fetch period comparison stats for a creator.
152 pub(crate) async fn get_stats(
153 &self,
154 user_id: &str,
155 range: &str,
156 ) -> anyhow::Result<CreatorStats> {
157 let url = format!("{}/api/internal/creator/stats", self.base_url);
158 let resp = self
159 .http
160 .get(&url)
161 .bearer_auth(&self.service_token)
162 .header("X-MNW-Actor", self.actor_header())
163 .query(&[("user_id", user_id), ("range", range)])
164 .send()
165 .await?;
166
167 json_response(resp, "get_stats").await
168 }
169
170 /// Fetch storage usage and limits for a creator.
171 pub(crate) async fn get_storage_info(&self, user_id: &str) -> anyhow::Result<StorageInfo> {
172 let url = format!("{}/api/internal/creator/storage", self.base_url);
173 let resp = self
174 .http
175 .get(&url)
176 .bearer_auth(&self.service_token)
177 .header("X-MNW-Actor", self.actor_header())
178 .query(&[("user_id", user_id)])
179 .send()
180 .await?;
181
182 json_response(resp, "get_storage_info").await
183 }
184 }
185