Skip to main content

max / makenotwork

2.8 KB · 94 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 pub added_at: DateTime<Utc>,
60 }
61
62 impl WishlistItem {
63 /// Pre-formatted price string, call this from templates instead of doing
64 /// inline cents-to-dollars math. Wraps the canonical `format_price` helper
65 /// so wishlist rows render the same way as everywhere else in the app
66 /// (handles the "Free" zero case, the whole-dollar case, etc).
67 pub fn price_display(&self) -> String {
68 crate::formatting::format_price(self.price_cents as i64)
69 }
70 }
71
72 /// Get the user's wishlist with item details.
73 #[tracing::instrument(skip_all)]
74 pub(crate) async fn get_wishlist(pool: &PgPool, user_id: UserId) -> Result<Vec<WishlistItem>> {
75 let items = sqlx::query_as::<_, WishlistItem>(
76 r"
77 SELECT w.item_id, i.title, i.item_type::TEXT as item_type, i.price_cents,
78 u.username AS creator, w.created_at AS added_at
79 FROM wishlists w
80 JOIN items i ON i.id = w.item_id
81 JOIN projects p ON p.id = i.project_id
82 JOIN users u ON u.id = p.user_id
83 WHERE w.user_id = $1 AND i.is_public = true AND i.deleted_at IS NULL
84 ORDER BY w.created_at DESC
85 LIMIT 200
86 ",
87 )
88 .bind(user_id)
89 .fetch_all(pool)
90 .await?;
91
92 Ok(items)
93 }
94