//! Bundle management handlers for bundle-type items. use axum::{ extract::{Path, State}, http::StatusCode, response::IntoResponse, Json, }; use serde::{Deserialize, Serialize}; use crate::{ auth::AuthUser, db::{self, ItemId, ItemType}, error::{AppError, Result}, AppState, }; use super::super::verify_item_ownership; #[derive(Debug, Deserialize)] pub struct BundleAddRequest { pub item_id: ItemId, } #[derive(Debug, Deserialize)] pub struct BundleListedRequest { pub listed: bool, } /// POST /api/items/{id}/bundle/add: add an item to this bundle. #[tracing::instrument(skip_all, name = "items::bundle_add")] pub async fn bundle_add( State(state): State, AuthUser(user): AuthUser, Path(bundle_id): Path, Json(req): Json, ) -> Result { user.check_not_suspended()?; let (item, _project) = verify_item_ownership(&state, bundle_id, user.id).await?; if item.item_type != ItemType::Bundle { return Err(AppError::BadRequest("Item is not a bundle".to_string())); } let target = db::items::get_item_by_id(&state.db, req.item_id) .await? .ok_or(AppError::NotFound)?; if target.project_id != item.project_id { return Err(AppError::BadRequest("Item must be in the same project".to_string())); } if target.item_type == ItemType::Bundle { return Err(AppError::BadRequest("Cannot nest bundles".to_string())); } let count = db::bundles::get_bundle_item_count(&state.db, bundle_id).await?; db::bundles::add_item_to_bundle(&state.db, bundle_id, req.item_id, count as i32).await?; Ok(StatusCode::OK) } /// DELETE /api/items/{id}/bundle/{child_id}: remove an item from this bundle. #[tracing::instrument(skip_all, name = "items::bundle_remove")] pub async fn bundle_remove( State(state): State, AuthUser(user): AuthUser, Path((bundle_id, child_id)): Path<(ItemId, ItemId)>, ) -> Result { user.check_not_suspended()?; let (item, _project) = verify_item_ownership(&state, bundle_id, user.id).await?; if item.item_type != ItemType::Bundle { return Err(AppError::BadRequest("Item is not a bundle".to_string())); } db::bundles::remove_item_from_bundle(&state.db, bundle_id, child_id).await?; Ok(StatusCode::OK) } /// PUT /api/items/{id}/bundle/{child_id}/listed: toggle listed status. #[tracing::instrument(skip_all, name = "items::bundle_toggle_listed")] pub async fn bundle_toggle_listed( State(state): State, AuthUser(user): AuthUser, Path((bundle_id, child_id)): Path<(ItemId, ItemId)>, Json(req): Json, ) -> Result { user.check_not_suspended()?; let (item, _project) = verify_item_ownership(&state, bundle_id, user.id).await?; if item.item_type != ItemType::Bundle { return Err(AppError::BadRequest("Item is not a bundle".to_string())); } // Verify child actually belongs to this bundle before toggling if !db::bundles::is_bundle_member(&state.db, bundle_id, child_id).await? { return Err(AppError::NotFound); } db::bundles::set_item_listed(&state.db, child_id, req.listed).await?; Ok(StatusCode::OK) } #[derive(Debug, Deserialize)] pub struct BundleCreateChildRequest { pub title: String, pub description: Option, } #[derive(Debug, Serialize)] pub struct BundleCreateChildResponse { pub item_id: ItemId, pub title: String, } /// POST /api/items/{id}/bundle/create-child: create a new item, add to bundle, set unlisted. #[tracing::instrument(skip_all, name = "items::bundle_create_child")] pub async fn bundle_create_child( State(state): State, AuthUser(user): AuthUser, Path(bundle_id): Path, Json(req): Json, ) -> Result { user.check_not_suspended()?; let (bundle, _project) = verify_item_ownership(&state, bundle_id, user.id).await?; if bundle.item_type != ItemType::Bundle { return Err(AppError::BadRequest("Item is not a bundle".to_string())); } crate::validation::validate_item_title(&req.title)?; if let Some(ref desc) = req.description { crate::validation::validate_item_description(desc)?; } let child = db::items::create_item( &state.db, bundle.project_id, &req.title, req.description.as_deref(), crate::db::PriceCents::from_db(0), ItemType::Digital, crate::db::AiTier::Handmade, None, ) .await?; // Add to bundle and set unlisted let count = db::bundles::get_bundle_item_count(&state.db, bundle_id).await?; db::bundles::add_item_to_bundle(&state.db, bundle_id, child.id, count as i32).await?; db::bundles::set_item_listed(&state.db, child.id, false).await?; // Publish the child so it's downloadable via the bundle db::items::bulk_publish(&state.db, &[child.id], bundle.project_id, user.id).await?; db::projects::bump_cache_generation(&state.db, bundle.project_id).await?; Ok(Json(BundleCreateChildResponse { item_id: child.id, title: child.title, })) }