Skip to main content

max / makenotwork

5.6 KB · 164 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 backdate_release(pool, item.id, spec.released_days_ago).await?;
119
120 // Keep the item hidden until Phase 3 attaches media. `items.scan_status`
121 // defaults to 'clean' (migration 146), so an unfinished, file-less item would
122 // otherwise show in discover with nothing to download.
123 db::scanning::update_item_scan_status(pool, item.id, db::FileScanStatus::Pending).await?;
124
125 // Tags: resolve each pre-seeded slug; the first that resolves is primary.
126 let mut primary_assigned = false;
127 for slug in spec.tags {
128 if let Some(tag) = db::tags::get_tag_by_slug(pool, slug).await? {
129 let is_primary = !primary_assigned;
130 db::tags::add_tag_to_item(pool, item.id, tag.id, is_primary).await?;
131 primary_assigned = true;
132 } else {
133 tracing::warn!(slug, "example seed: tag slug not found, skipping");
134 }
135 }
136
137 tracing::info!(
138 title = spec.title,
139 item_type = ?spec.item_type,
140 "example seed: created item"
141 );
142 Ok(())
143 }
144
145 /// Date the item's release, which `create_item` sets to now.
146 ///
147 /// `items.created_at` is what the item page renders as "Released" and what
148 /// discover sorts by, so leaving it at the seed run's timestamp gives every item
149 /// in the catalog the same release date. There is no authoring path for this
150 /// (a real creator's item really was created when they created it), hence the
151 /// direct write rather than a `db::items` call.
152 async fn backdate_release(
153 pool: &sqlx::PgPool,
154 item_id: db::ItemId,
155 days_ago: i64,
156 ) -> Result<(), SeedError> {
157 sqlx::query("UPDATE items SET created_at = NOW() - ($2 || ' days')::interval WHERE id = $1")
158 .bind(item_id)
159 .bind(days_ago.to_string())
160 .execute(pool)
161 .await?;
162 Ok(())
163 }
164