//! Projects: the creator's top-level containers, their item lists, and the //! storage and revenue totals the dashboard reads. use super::{MnwApiClient, json_response, revenue_of}; use crate::currency::{Currency, RevenueByCurrency}; use serde::{Deserialize, Serialize}; use std::collections::BTreeMap; /// A creator's project with item count and revenue. #[derive(Debug, Clone, Deserialize, Serialize)] #[allow( clippy::struct_field_names, reason = "project_type mirrors the server JSON/DB schema; renaming would diverge the field from the wire contract" )] pub(crate) struct Project { pub id: String, pub slug: String, pub title: String, pub project_type: String, pub is_public: bool, pub item_count: i64, /// Revenue in `currency` — the project's largest single-currency total, not /// a sum across currencies. Only meaningful next to `currency`. pub revenue_cents: i64, /// The currency `revenue_cents` is denominated in. A project can earn in a /// currency that is not the viewer's: revenue splits are paid in the /// currency of the project that generated them. #[serde(default)] pub currency: Currency, /// Every currency this project earned in, keyed by lowercase ISO code. /// Normally one entry matching `revenue_cents`; empty from a server that /// predates the field. #[serde(default)] pub revenue_cents_by_currency: BTreeMap, } impl Project { /// Revenue across every currency it was earned in. pub(crate) fn revenue(&self) -> RevenueByCurrency { revenue_of( self.revenue_cents, self.currency, &self.revenue_cents_by_currency, ) } } /// An item within a project. #[derive(Debug, Clone, Deserialize, Serialize)] #[allow( clippy::struct_field_names, reason = "item_type mirrors the server JSON/DB schema; renaming would diverge the field from the wire contract" )] pub(crate) struct Item { pub id: String, pub title: String, pub item_type: String, pub price_cents: i32, pub is_public: bool, pub sort_order: i32, } /// Period comparison stats for the creator. #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct CreatorStats { pub current_revenue_cents: i64, pub previous_revenue_cents: i64, pub current_sales: i64, pub previous_sales: i64, pub current_followers: i64, pub previous_followers: i64, pub total_projects: i64, pub total_items: i64, } /// Response from the storage-info internal endpoint. #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct StorageInfo { pub storage_used_bytes: i64, pub max_storage_bytes: i64, pub allows_file_uploads: bool, } impl MnwApiClient { /// Fetch all projects for a creator with item counts and revenue. pub(crate) async fn get_projects(&self, user_id: &str) -> anyhow::Result> { let url = format!("{}/api/internal/creator/projects", self.base_url); let resp = self .http .get(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("user_id", user_id)]) .send() .await?; json_response(resp, "get_projects").await } /// Create a new project. pub(crate) async fn create_project( &self, user_id: &str, title: &str, project_type: &str, description: Option<&str>, ) -> anyhow::Result { let url = format!("{}/api/internal/creator/projects", self.base_url); let mut body = serde_json::json!({ "user_id": user_id, "title": title, "project_type": project_type, }); if let Some(desc) = description { body["description"] = serde_json::Value::String(desc.to_string()); } let resp = self .http .post(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .json(&body) .send() .await?; json_response(resp, "create_project").await } /// Fetch items in a project. pub(crate) async fn get_project_items( &self, project_id: &str, user_id: &str, ) -> anyhow::Result> { let url = format!( "{}/api/internal/creator/projects/{}/items", self.base_url, project_id ); let resp = self .http .get(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("user_id", user_id)]) .send() .await?; json_response(resp, "get_project_items").await } /// Fetch period comparison stats for a creator. pub(crate) async fn get_stats( &self, user_id: &str, range: &str, ) -> anyhow::Result { let url = format!("{}/api/internal/creator/stats", self.base_url); let resp = self .http .get(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("user_id", user_id), ("range", range)]) .send() .await?; json_response(resp, "get_stats").await } /// Fetch storage usage and limits for a creator. pub(crate) async fn get_storage_info(&self, user_id: &str) -> anyhow::Result { let url = format!("{}/api/internal/creator/storage", self.base_url); let resp = self .http .get(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("user_id", user_id)]) .send() .await?; json_response(resp, "get_storage_info").await } }