//! Seed each project's items, at least one per `ItemType`, across pricing kinds //! and tags, plus the project's headline pricing model and subscription tiers. //! //! Items are seeded **hidden** until Phase 3. Note `items.scan_status` now //! defaults to `'clean'` (migration 146, item-level scan status is vestigial, //! real gating is per-version at download time), so a fresh file-less item would //! be discover-visible. Since these items have no media yet, this module //! explicitly forces `scan_status = 'pending'` (which the discover gate excludes) //! so nothing shows a broken download; Phase 3 attaches real files and flips them //! back to `'clean'`. So this phase establishes rows, pricing, tags, and tiers only. use super::SeedError; use super::creators::{ItemPricing, ProjectPricing}; use super::projects::SeededProject; use crate::db::{self, AiTier, PriceCents, PricingKind}; /// Seed items, pricing, and tiers for every seeded project. /// /// Called from [`super::run`] after [`super::projects::seed_projects`]. All writes /// are ownership-checked against the project's creator (`user_id`), matching the /// real authoring paths. pub async fn seed_items(pool: &sqlx::PgPool, projects: &[SeededProject]) -> Result<(), SeedError> { for seeded in projects { seed_project_pricing(pool, seeded).await?; for spec in seeded.spec.items { seed_one_item(pool, seeded, spec).await?; } } Ok(()) } /// Set the project's headline `pricing_model` and, for a subscription project, /// its tiers. async fn seed_project_pricing( pool: &sqlx::PgPool, seeded: &SeededProject, ) -> Result<(), SeedError> { let (kind, price, pwyw_min) = match seeded.spec.pricing { ProjectPricing::Free => (PricingKind::Free, PriceCents::ZERO, None), ProjectPricing::BuyOnce(cents) => (PricingKind::BuyOnce, PriceCents::from_db(cents), None), ProjectPricing::Pwyw { min } => ( PricingKind::Pwyw, PriceCents::ZERO, Some(PriceCents::from_db(min)), ), ProjectPricing::Subscription => (PricingKind::Subscription, PriceCents::ZERO, None), }; db::projects::update_project_pricing( pool, seeded.project.id, seeded.user_id, kind, price, pwyw_min, ) .await?; for tier in seeded.spec.tiers { db::subscriptions::create_subscription_tier( pool, seeded.project.id, tier.name, Some(tier.description), PriceCents::from_db(tier.price_cents), ) .await?; } Ok(()) } /// Create one item, apply its per-item pricing / text body, and attach its tags. async fn seed_one_item( pool: &sqlx::PgPool, seeded: &SeededProject, spec: &super::creators::ItemSpec, ) -> Result<(), SeedError> { // create_item takes the fixed price; Pwyw is enabled via a follow-up update. let price = match spec.pricing { ItemPricing::Free | ItemPricing::Pwyw { .. } => PriceCents::ZERO, ItemPricing::BuyOnce(cents) => PriceCents::from_db(cents), }; let item = db::items::create_item( pool, seeded.project.id, spec.title, Some(spec.description), price, spec.item_type, AiTier::Handmade, None, ) .await?; if let ItemPricing::Pwyw { min } = spec.pricing { db::items::update_item( pool, item.id, seeded.user_id, None, None, None, None, None, Some(true), Some(PriceCents::from_db(min)), None, None, None, None, ) .await?; } if let Some(body) = spec.body { db::items::update_item_text(pool, item.id, seeded.user_id, body).await?; } // Keep the item hidden until Phase 3 attaches media. `items.scan_status` // defaults to 'clean' (migration 146), so an unfinished, file-less item would // otherwise show in discover with nothing to download. db::scanning::update_item_scan_status(pool, item.id, db::FileScanStatus::Pending).await?; // Tags: resolve each pre-seeded slug; the first that resolves is primary. let mut primary_assigned = false; for slug in spec.tags { if let Some(tag) = db::tags::get_tag_by_slug(pool, slug).await? { let is_primary = !primary_assigned; db::tags::add_tag_to_item(pool, item.id, tag.id, is_primary).await?; primary_assigned = true; } else { tracing::warn!(slug, "example seed: tag slug not found, skipping"); } } tracing::info!( title = spec.title, item_type = ?spec.item_type, "example seed: created item" ); Ok(()) }