//! Items: create, read, update, publish, and the tags an item carries. use super::{MnwApiClient, empty_response, json_response}; use serde::{Deserialize, Serialize}; /// Response from the create-item internal endpoint. #[derive(Debug, Deserialize)] #[allow(dead_code)] pub(crate) struct ItemCreated { pub item_id: String, pub project_id: String, } /// Full item detail returned from the get/update endpoints. #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct ItemDetail { pub id: String, pub title: String, pub description: Option, pub price_cents: i32, pub item_type: String, pub is_public: bool, pub slug: String, pub sort_order: i32, pub sales_count: i32, pub download_count: i32, pub play_count: i32, pub pwyw_enabled: bool, pub pwyw_min_cents: Option, pub has_audio: bool, pub has_cover: bool, pub created_at: String, pub updated_at: String, } /// A version of an item. #[derive(Debug, Clone, Deserialize, Serialize)] #[allow( clippy::struct_field_names, reason = "version_number mirrors the server JSON/DB schema; renaming would diverge the field from the wire contract" )] pub(crate) struct Version { pub id: String, pub version_number: String, pub changelog: Option, pub file_name: Option, pub file_size_bytes: Option, pub download_count: i32, pub is_current: bool, pub created_at: String, } /// A tag on an item or from search. #[derive(Debug, Clone, Deserialize, Serialize)] pub(crate) struct TagInfo { pub id: String, pub name: String, pub slug: String, pub is_primary: bool, } impl MnwApiClient { /// Create an item in a project. pub(crate) async fn create_item( &self, user_id: &str, project_id: &str, title: &str, item_type: &str, price_cents: i32, ) -> anyhow::Result { let url = format!("{}/api/internal/creator/items", 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, "project_id": project_id, "title": title, "item_type": item_type, "price_cents": price_cents, })) .send() .await?; json_response(resp, "create_item").await } /// Fetch full item detail. pub(crate) async fn get_item_detail( &self, user_id: &str, item_id: &str, ) -> anyhow::Result { let url = format!("{}/api/internal/creator/items/{}", 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, "get_item_detail").await } /// Update item fields. Only non-None fields are changed. pub(crate) async fn update_item( &self, user_id: &str, item_id: &str, title: Option<&str>, description: Option<&str>, price_cents: Option, is_public: Option, ) -> anyhow::Result { let url = format!("{}/api/internal/creator/items/{}", self.base_url, item_id); let mut body = serde_json::json!({ "user_id": user_id }); if let Some(t) = title { body["title"] = serde_json::Value::String(t.to_string()); } if let Some(d) = description { body["description"] = serde_json::Value::String(d.to_string()); } if let Some(p) = price_cents { body["price_cents"] = serde_json::json!(p); } if let Some(v) = is_public { body["is_public"] = serde_json::json!(v); } let resp = self .http .put(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .json(&body) .send() .await?; json_response(resp, "update_item").await } /// Delete an item permanently. pub(crate) async fn delete_item(&self, user_id: &str, item_id: &str) -> anyhow::Result<()> { let url = format!("{}/api/internal/creator/items/{}", self.base_url, item_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_item").await } /// Publish an item (set is_public=true). pub(crate) async fn publish_item( &self, user_id: &str, item_id: &str, ) -> anyhow::Result { let url = format!( "{}/api/internal/creator/items/{}/publish", 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, "publish_item").await } /// Unpublish an item (set is_public=false). pub(crate) async fn unpublish_item( &self, user_id: &str, item_id: &str, ) -> anyhow::Result { let url = format!( "{}/api/internal/creator/items/{}/unpublish", 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, "unpublish_item").await } /// Fetch versions for an item. pub(crate) async fn get_item_versions( &self, user_id: &str, item_id: &str, ) -> anyhow::Result> { let url = format!( "{}/api/internal/creator/items/{}/versions", 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, "get_item_versions").await } pub(crate) async fn list_item_tags( &self, user_id: &str, item_id: &str, ) -> anyhow::Result> { let url = format!( "{}/api/internal/creator/items/{}/tags", 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_item_tags").await } pub(crate) async fn search_tags(&self, query: &str) -> anyhow::Result> { let url = format!("{}/api/internal/tags/search", self.base_url); let resp = self .http .get(&url) .bearer_auth(&self.service_token) .header("X-MNW-Actor", self.actor_header()) .query(&[("q", query)]) .send() .await?; json_response(resp, "search_tags").await } pub(crate) async fn add_item_tag( &self, user_id: &str, item_id: &str, tag_id: &str, ) -> anyhow::Result<()> { let url = format!("{}/api/internal/creator/items/tags", 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, "item_id": item_id, "tag_id": tag_id})) .send() .await?; empty_response(resp, "add_item_tag").await } // Unused by the TUI today; kept so the client mirrors the full // /api/internal surface rather than only the paths one caller happens to hit. #[allow(dead_code)] pub(crate) async fn remove_item_tag( &self, user_id: &str, item_id: &str, tag_id: &str, ) -> anyhow::Result<()> { let url = format!("{}/api/internal/creator/items/tags/remove", 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, "item_id": item_id, "tag_id": tag_id})) .send() .await?; empty_response(resp, "remove_item_tag").await } }