//! The storefront around the files: blog posts, promo codes, license keys, //! subscription tiers, collections, and the broadcast that tells buyers about //! any of it. use super::{MnwApiClient, empty_response, json_response}; use serde::{Deserialize, Serialize}; /// A blog post summary. #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct BlogPost { pub id: String, pub title: String, pub slug: String, pub is_published: bool, pub publish_at: Option, pub created_at: String, pub updated_at: String, } /// A promo code. #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct PromoCode { pub id: String, pub code: String, pub code_purpose: String, pub discount_type: Option, pub discount_value: Option, pub item_title: Option, pub project_title: Option, pub max_uses: Option, pub use_count: i32, pub created_at: String, } /// A license key. #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct LicenseKey { pub id: String, pub key_code: String, pub activation_count: i32, pub max_activations: Option, pub is_revoked: bool, pub created_at: String, } /// Result of a broadcast send. #[derive(Debug, Deserialize)] #[allow(dead_code)] // wire contract: every field the endpoint returns, read or not pub(crate) struct BroadcastResult { pub success: bool, pub recipient_count: usize, } /// A subscription tier. #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct TierInfo { pub id: String, pub name: String, pub description: String, pub price_cents: i32, pub is_active: bool, } /// A collection. #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct CollectionInfo { pub id: String, pub slug: String, pub title: String, pub description: String, pub is_public: bool, pub item_count: i64, } impl MnwApiClient { /// List blog posts for a project. pub(crate) async fn list_blog_posts( &self, user_id: &str, project_id: &str, ) -> anyhow::Result> { let url = format!( "{}/api/internal/creator/projects/{}/blog", 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, "list_blog_posts").await } /// Create a blog post, optionally scheduled for future publication. pub(crate) async fn create_blog_post( &self, user_id: &str, project_id: &str, title: &str, body_markdown: &str, publish: bool, publish_at: Option<&str>, ) -> anyhow::Result { let url = format!("{}/api/internal/creator/blog", self.base_url); let mut body = serde_json::json!({ "user_id": user_id, "project_id": project_id, "title": title, "body_markdown": body_markdown, "publish": publish, }); if let Some(pa) = publish_at { body["publish_at"] = serde_json::Value::String(pa.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_blog_post").await } /// Delete a blog post. pub(crate) async fn delete_blog_post( &self, user_id: &str, post_id: &str, ) -> anyhow::Result<()> { let url = format!("{}/api/internal/creator/blog/{}", self.base_url, post_id); let resp = self .http .delete(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("user_id", user_id)]) .send() .await?; empty_response(resp, "delete_blog_post").await } /// List promo codes for a creator. pub(crate) async fn list_promo_codes(&self, user_id: &str) -> anyhow::Result> { let url = format!("{}/api/internal/creator/promo-codes", 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, "list_promo_codes").await } /// Create a promo code. pub(crate) async fn create_promo_code( &self, user_id: &str, code: &str, discount_type: &str, discount_value: i32, max_uses: Option, project_id: Option<&str>, ) -> anyhow::Result { let url = format!("{}/api/internal/creator/promo-codes", self.base_url); let mut body = serde_json::json!({ "user_id": user_id, "code": code, "code_purpose": "discount", "discount_type": discount_type, "discount_value": discount_value, }); if let Some(max) = max_uses { body["max_uses"] = serde_json::json!(max); } if let Some(pid) = project_id { body["project_id"] = serde_json::json!(pid); } 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_promo_code").await } /// Delete a promo code. pub(crate) async fn delete_promo_code( &self, user_id: &str, code_id: &str, ) -> anyhow::Result<()> { let url = format!( "{}/api/internal/creator/promo-codes/{}", self.base_url, code_id ); let resp = self .http .delete(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("user_id", user_id)]) .send() .await?; empty_response(resp, "delete_promo_code").await } /// List license keys for an item. pub(crate) async fn list_license_keys( &self, user_id: &str, item_id: &str, ) -> anyhow::Result> { let url = format!( "{}/api/internal/creator/items/{}/keys", self.base_url, item_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, "list_license_keys").await } /// Generate a new license key for an item. pub(crate) async fn generate_license_key( &self, user_id: &str, item_id: &str, ) -> anyhow::Result { let url = format!( "{}/api/internal/creator/items/{}/keys", self.base_url, item_id ); let resp = self .http .post(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .json(&serde_json::json!({ "user_id": user_id })) .send() .await?; json_response(resp, "generate_license_key").await } /// Revoke a license key. pub(crate) async fn revoke_license_key( &self, user_id: &str, key_id: &str, ) -> anyhow::Result<()> { let url = format!( "{}/api/internal/creator/keys/{}/revoke", self.base_url, key_id ); let resp = self .http .post(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .json(&serde_json::json!({ "user_id": user_id })) .send() .await?; empty_response(resp, "revoke_license_key").await } pub(crate) async fn send_broadcast( &self, user_id: &str, subject: &str, body: &str, ) -> anyhow::Result { let url = format!("{}/api/internal/creator/broadcast", self.base_url); let resp = self .http .post(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .json(&serde_json::json!({"user_id": user_id, "subject": subject, "body": body})) .send() .await?; json_response(resp, "send_broadcast").await } pub(crate) async fn list_tiers( &self, user_id: &str, project_id: &str, ) -> anyhow::Result> { let url = format!( "{}/api/internal/creator/projects/{}/tiers", 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, "list_tiers").await } pub(crate) async fn list_collections( &self, user_id: &str, ) -> anyhow::Result> { let url = format!("{}/api/internal/creator/collections", 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, "list_collections").await } #[allow(dead_code)] pub(crate) async fn create_collection( &self, user_id: &str, slug: &str, title: &str, ) -> anyhow::Result { let url = format!("{}/api/internal/creator/collections", self.base_url); let resp = self .http .post(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .json(&serde_json::json!({"user_id": user_id, "slug": slug, "title": title})) .send() .await?; json_response(resp, "create_collection").await } #[allow(dead_code)] pub(crate) async fn delete_collection( &self, user_id: &str, collection_id: &str, ) -> anyhow::Result<()> { let url = format!( "{}/api/internal/creator/collections/{}", self.base_url, collection_id ); let resp = self .http .delete(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("user_id", user_id)]) .send() .await?; empty_response(resp, "delete_collection").await } }