Skip to main content

max / makenotwork

3.0 KB · 97 lines History Blame Raw
1 //! Wishlist/bookmark queries: fans save items they want to buy later.
2
3 use chrono::{DateTime, Utc};
4 use sqlx::PgPool;
5
6 use super::{ItemId, UserId};
7 use crate::error::Result;
8
9 /// Check if an item is in the user's wishlist.
10 #[tracing::instrument(skip_all)]
11 pub(crate) async fn is_wishlisted(pool: &PgPool, user_id: UserId, item_id: ItemId) -> Result<bool> {
12 let exists: bool = sqlx::query_scalar(
13 "SELECT EXISTS(SELECT 1 FROM wishlists WHERE user_id = $1 AND item_id = $2)",
14 )
15 .bind(user_id)
16 .bind(item_id)
17 .fetch_one(pool)
18 .await?;
19
20 Ok(exists)
21 }
22
23 /// Add an item to the user's wishlist (idempotent).
24 #[tracing::instrument(skip_all)]
25 pub(crate) async fn add_to_wishlist(pool: &PgPool, user_id: UserId, item_id: ItemId) -> Result<()> {
26 sqlx::query("INSERT INTO wishlists (user_id, item_id) VALUES ($1, $2) ON CONFLICT DO NOTHING")
27 .bind(user_id)
28 .bind(item_id)
29 .execute(pool)
30 .await?;
31
32 Ok(())
33 }
34
35 /// Remove an item from the user's wishlist.
36 #[tracing::instrument(skip_all)]
37 pub(crate) async fn remove_from_wishlist(
38 pool: &PgPool,
39 user_id: UserId,
40 item_id: ItemId,
41 ) -> Result<()> {
42 sqlx::query("DELETE FROM wishlists WHERE user_id = $1 AND item_id = $2")
43 .bind(user_id)
44 .bind(item_id)
45 .execute(pool)
46 .await?;
47
48 Ok(())
49 }
50
51 /// A wishlisted item with joined display data.
52 #[derive(Debug, Clone, sqlx::FromRow)]
53 pub struct WishlistItem {
54 pub item_id: ItemId,
55 pub title: String,
56 pub item_type: String,
57 pub price_cents: i32,
58 pub creator: String,
59 /// The seller's settlement currency. Joined per row: a wishlist spans
60 /// creators, so there is no single currency for the page.
61 pub settlement_currency: crate::currency::SettlementCurrency,
62 pub added_at: DateTime<Utc>,
63 }
64
65 impl WishlistItem {
66 /// Pre-formatted price string, call this from templates instead of doing
67 /// inline cents-to-dollars math. Wraps the canonical `format_price` helper
68 /// so wishlist rows render the same way as everywhere else in the app
69 /// (handles the "Free" zero case, the whole-dollar case, etc).
70 pub fn price_display(&self) -> String {
71 crate::formatting::format_price(self.price_cents as i64, self.settlement_currency)
72 }
73 }
74
75 /// Get the user's wishlist with item details.
76 #[tracing::instrument(skip_all)]
77 pub(crate) async fn get_wishlist(pool: &PgPool, user_id: UserId) -> Result<Vec<WishlistItem>> {
78 let items = sqlx::query_as::<_, WishlistItem>(
79 r"
80 SELECT w.item_id, i.title, i.item_type::TEXT as item_type, i.price_cents,
81 u.username AS creator, u.settlement_currency, w.created_at AS added_at
82 FROM wishlists w
83 JOIN items i ON i.id = w.item_id
84 JOIN projects p ON p.id = i.project_id
85 JOIN users u ON u.id = p.user_id
86 WHERE w.user_id = $1 AND i.is_public = true AND i.deleted_at IS NULL
87 ORDER BY w.created_at DESC
88 LIMIT 200
89 ",
90 )
91 .bind(user_id)
92 .fetch_all(pool)
93 .await?;
94
95 Ok(items)
96 }
97