Skip to main content

max / makenotwork

4.7 KB · 142 lines History Blame Raw
1 //! Seed each project's items, at least one per `ItemType`, across pricing kinds
2 //! and tags, plus the project's headline pricing model and subscription tiers.
3 //!
4 //! Items are seeded **hidden** until Phase 3. Note `items.scan_status` now
5 //! defaults to `'clean'` (migration 146, item-level scan status is vestigial,
6 //! real gating is per-version at download time), so a fresh file-less item would
7 //! be discover-visible. Since these items have no media yet, this module
8 //! explicitly forces `scan_status = 'pending'` (which the discover gate excludes)
9 //! so nothing shows a broken download; Phase 3 attaches real files and flips them
10 //! back to `'clean'`. So this phase establishes rows, pricing, tags, and tiers only.
11
12 use super::SeedError;
13 use super::creators::{ItemPricing, ProjectPricing};
14 use super::projects::SeededProject;
15 use crate::db::{self, AiTier, PriceCents, PricingKind};
16
17 /// Seed items, pricing, and tiers for every seeded project.
18 ///
19 /// Called from [`super::run`] after [`super::projects::seed_projects`]. All writes
20 /// are ownership-checked against the project's creator (`user_id`), matching the
21 /// real authoring paths.
22 pub async fn seed_items(pool: &sqlx::PgPool, projects: &[SeededProject]) -> Result<(), SeedError> {
23 for seeded in projects {
24 seed_project_pricing(pool, seeded).await?;
25 for spec in seeded.spec.items {
26 seed_one_item(pool, seeded, spec).await?;
27 }
28 }
29 Ok(())
30 }
31
32 /// Set the project's headline `pricing_model` and, for a subscription project,
33 /// its tiers.
34 async fn seed_project_pricing(
35 pool: &sqlx::PgPool,
36 seeded: &SeededProject,
37 ) -> Result<(), SeedError> {
38 let (kind, price, pwyw_min) = match seeded.spec.pricing {
39 ProjectPricing::Free => (PricingKind::Free, PriceCents::ZERO, None),
40 ProjectPricing::BuyOnce(cents) => (PricingKind::BuyOnce, PriceCents::from_db(cents), None),
41 ProjectPricing::Pwyw { min } => (
42 PricingKind::Pwyw,
43 PriceCents::ZERO,
44 Some(PriceCents::from_db(min)),
45 ),
46 ProjectPricing::Subscription => (PricingKind::Subscription, PriceCents::ZERO, None),
47 };
48 db::projects::update_project_pricing(
49 pool,
50 seeded.project.id,
51 seeded.user_id,
52 kind,
53 price,
54 pwyw_min,
55 )
56 .await?;
57
58 for tier in seeded.spec.tiers {
59 db::subscriptions::create_subscription_tier(
60 pool,
61 seeded.project.id,
62 tier.name,
63 Some(tier.description),
64 PriceCents::from_db(tier.price_cents),
65 )
66 .await?;
67 }
68 Ok(())
69 }
70
71 /// Create one item, apply its per-item pricing / text body, and attach its tags.
72 async fn seed_one_item(
73 pool: &sqlx::PgPool,
74 seeded: &SeededProject,
75 spec: &super::creators::ItemSpec,
76 ) -> Result<(), SeedError> {
77 // create_item takes the fixed price; Pwyw is enabled via a follow-up update.
78 let price = match spec.pricing {
79 ItemPricing::Free | ItemPricing::Pwyw { .. } => PriceCents::ZERO,
80 ItemPricing::BuyOnce(cents) => PriceCents::from_db(cents),
81 };
82 let item = db::items::create_item(
83 pool,
84 seeded.project.id,
85 spec.title,
86 Some(spec.description),
87 price,
88 spec.item_type,
89 AiTier::Handmade,
90 None,
91 )
92 .await?;
93
94 if let ItemPricing::Pwyw { min } = spec.pricing {
95 db::items::update_item(
96 pool,
97 item.id,
98 seeded.user_id,
99 None,
100 None,
101 None,
102 None,
103 None,
104 Some(true),
105 Some(PriceCents::from_db(min)),
106 None,
107 None,
108 None,
109 None,
110 )
111 .await?;
112 }
113
114 if let Some(body) = spec.body {
115 db::items::update_item_text(pool, item.id, seeded.user_id, body).await?;
116 }
117
118 // Keep the item hidden until Phase 3 attaches media. `items.scan_status`
119 // defaults to 'clean' (migration 146), so an unfinished, file-less item would
120 // otherwise show in discover with nothing to download.
121 db::scanning::update_item_scan_status(pool, item.id, db::FileScanStatus::Pending).await?;
122
123 // Tags: resolve each pre-seeded slug; the first that resolves is primary.
124 let mut primary_assigned = false;
125 for slug in spec.tags {
126 if let Some(tag) = db::tags::get_tag_by_slug(pool, slug).await? {
127 let is_primary = !primary_assigned;
128 db::tags::add_tag_to_item(pool, item.id, tag.id, is_primary).await?;
129 primary_assigned = true;
130 } else {
131 tracing::warn!(slug, "example seed: tag slug not found, skipping");
132 }
133 }
134
135 tracing::info!(
136 title = spec.title,
137 item_type = ?spec.item_type,
138 "example seed: created item"
139 );
140 Ok(())
141 }
142