tetratto_core/database/
products.rs

1use crate::model::{
2    auth::User,
3    economy::{
4        CoinTransfer, CoinTransferMethod, CoinTransferSource, Product, ProductFulfillmentMethod,
5        ProductUploads,
6    },
7    mail::Letter,
8    permissions::FinePermission,
9    Error, Result,
10};
11use crate::{auto_method, DataManager};
12use oiseau::{cache::Cache, execute, get, params, query_rows, PostgresRow};
13
14impl DataManager {
15    /// Get a [`Product`] from an SQL row.
16    pub(crate) fn get_product_from_row(x: &PostgresRow) -> Product {
17        Product {
18            id: get!(x->0(i64)) as usize,
19            created: get!(x->1(i64)) as usize,
20            owner: get!(x->2(i64)) as usize,
21            title: get!(x->3(String)),
22            description: get!(x->4(String)),
23            method: serde_json::from_str(&get!(x->5(String))).unwrap(),
24            on_sale: get!(x->6(i32)) as i8 == 1,
25            price: get!(x->7(i32)),
26            stock: get!(x->8(i32)),
27            single_use: get!(x->9(i32)) as i8 == 1,
28            data: get!(x->10(String)),
29            uploads: serde_json::from_str(&get!(x->11(String))).unwrap(),
30        }
31    }
32
33    auto_method!(get_product_by_id(usize as i64)@get_product_from_row -> "SELECT * FROM products WHERE id = $1" --name="product" --returns=Product --cache-key-tmpl="atto.product:{}");
34
35    /// Get all products by user.
36    ///
37    /// # Arguments
38    /// * `id` - the ID of the user to fetch products for
39    /// * `batch` - the limit of items in each page
40    /// * `page` - the page number
41    pub async fn get_products_by_user(
42        &self,
43        id: usize,
44        batch: usize,
45        page: usize,
46    ) -> Result<Vec<Product>> {
47        let conn = match self.0.connect().await {
48            Ok(c) => c,
49            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
50        };
51
52        let res = query_rows!(
53            &conn,
54            "SELECT * FROM products WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
55            &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
56            |x| { Self::get_product_from_row(x) }
57        );
58
59        if res.is_err() {
60            return Err(Error::GeneralNotFound("product".to_string()));
61        }
62
63        Ok(res.unwrap())
64    }
65
66    const MAXIMUM_FREE_PRODUCTS: usize = 10;
67
68    /// Create a new product in the database.
69    ///
70    /// # Arguments
71    /// * `data` - a mock [`Product`] object to insert
72    pub async fn create_product(&self, mut data: Product) -> Result<Product> {
73        data.title = data.title.trim().to_string();
74        data.description = data.description.trim().to_string();
75
76        // check values
77        if data.title.len() < 2 {
78            return Err(Error::DataTooShort("title".to_string()));
79        } else if data.title.len() > 128 {
80            return Err(Error::DataTooLong("title".to_string()));
81        }
82
83        if data.description.len() < 2 {
84            return Err(Error::DataTooShort("description".to_string()));
85        } else if data.description.len() > 1024 {
86            return Err(Error::DataTooLong("description".to_string()));
87        }
88
89        // check number of stacks
90        let owner = self.get_user_by_id(data.owner).await?;
91
92        if !owner.permissions.check(FinePermission::SUPPORTER) {
93            let products = self
94                .get_table_row_count_where("products", &format!("owner = {}", owner.id))
95                .await? as usize;
96
97            if products >= Self::MAXIMUM_FREE_PRODUCTS {
98                return Err(Error::MiscError(
99                    "You already have the maximum number of products you can have".to_string(),
100                ));
101            }
102        }
103
104        // ...
105        let conn = match self.0.connect().await {
106            Ok(c) => c,
107            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
108        };
109
110        let res = execute!(
111            &conn,
112            "INSERT INTO products VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
113            params![
114                &(data.id as i64),
115                &(data.created as i64),
116                &(data.owner as i64),
117                &data.title,
118                &data.description,
119                &serde_json::to_string(&data.method).unwrap(),
120                &{ if data.on_sale { 1 } else { 0 } },
121                &data.price,
122                &(data.stock as i32),
123                &{ if data.single_use { 1 } else { 0 } },
124                &data.data,
125                &serde_json::to_string(&data.uploads).unwrap(),
126            ]
127        );
128
129        if let Err(e) = res {
130            return Err(Error::DatabaseError(e.to_string()));
131        }
132
133        Ok(data)
134    }
135
136    /// Purchase the given product as the given user.
137    pub async fn purchase_product(
138        &self,
139        product: usize,
140        customer: &mut User,
141    ) -> Result<CoinTransfer> {
142        let product = self.get_product_by_id(product).await?;
143
144        // handle single_use product
145        if product.single_use {
146            if self
147                .get_transfer_by_sender_method(
148                    customer.id,
149                    CoinTransferMethod::Purchase(product.id),
150                )
151                .await
152                .is_ok()
153            {
154                return Err(Error::MiscError("You already own this product".to_string()));
155            }
156        }
157
158        // ...
159        let mut transfer = CoinTransfer::new(
160            customer.id,
161            product.owner,
162            product.price,
163            CoinTransferMethod::Purchase(product.id),
164            CoinTransferSource::Sale,
165        );
166
167        if !product.stock.is_negative() {
168            // check stock
169            if product.stock == 0 {
170                return Err(Error::MiscError("No remaining stock".to_string()));
171            } else {
172                self.decr_product_stock(product.id).await?;
173            }
174        }
175
176        match product.method {
177            ProductFulfillmentMethod::AutoMail(message) => {
178                // we're basically done, transfer coins and send mail
179                self.create_transfer(&mut transfer, true).await?;
180
181                self.create_letter(Letter::new(
182                    self.0.0.system_user,
183                    vec![customer.id],
184                    format!("Thank you for purchasing \"{}\"", product.title),
185                    format!("The message below was supplied by the product owner, and was automatically sent.\n***\n{message}"),
186                    0,
187                ))
188                .await?;
189
190                Ok(transfer)
191            }
192            ProductFulfillmentMethod::ManualMail => {
193                // mark transfer as pending and create it
194                self.create_transfer(&mut transfer, false).await?;
195
196                // tell the customer to wait
197                self.create_letter(Letter::new(
198                    self.0.0.system_user,
199                    vec![customer.id],
200                    format!("Thank you for purchasing \"{}\"", product.title),
201                    "This product uses manual mail, meaning you won't be charged until the product owner sends you a letter about the product. You'll see a pending transfer in your wallet.".to_string(),
202                    0,
203                ))
204                .await?;
205
206                // tell product owner they have a new pending purchase
207                self.create_letter(Letter::new(
208                    self.0.0.system_user,
209                    vec![product.owner],
210                    "New product purchase pending".to_string(),
211                    format!(
212                        "Somebody has purchased your [product](/product/{}) \"{}\". Per your product's settings, the payment will not be completed until you manually mail them a letter **using the link below**.
213
214If your product is a purchase of goods or services, please be sure to fulfill this purchase either in the letter or elsewhere. The customer may request support if you fail to do so.
215
216***
217<a class=\"button\" href=\"/mail/compose?receivers=id:{}&subject=Product%20fulfillment&transfer_id={}\">Fulfill purchase</a>",
218                        product.id, product.title, customer.id, transfer.id
219                    ),
220                    0,
221                ))
222                .await?;
223
224                // return
225                Ok(transfer)
226            }
227            ProductFulfillmentMethod::ProfileStyle => {
228                // pretty much an automail without the message
229                self.create_transfer(&mut transfer, true).await?;
230
231                self.create_letter(Letter::new(
232                    self.0.0.system_user,
233                    vec![customer.id],
234                    format!("Thank you for purchasing \"{}\"", product.title),
235                    "You've purchased a CSS snippet which can be applied to your profile through the product's page!".to_string(),
236                    0,
237                ))
238                .await?;
239
240                Ok(transfer)
241            }
242        }
243    }
244
245    pub async fn delete_product(&self, id: usize, user: &User) -> Result<()> {
246        let product = self.get_product_by_id(id).await?;
247
248        // check user permission
249        if user.id != product.owner && !user.permissions.check(FinePermission::MANAGE_USERS) {
250            return Err(Error::NotAllowed);
251        }
252
253        // remove uploads
254        for upload in product.uploads.thumbnails {
255            self.delete_upload(upload).await?;
256        }
257
258        if product.uploads.reward != 0 {
259            self.delete_upload(product.uploads.reward).await?;
260        }
261
262        // ...
263        let conn = match self.0.connect().await {
264            Ok(c) => c,
265            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
266        };
267
268        let res = execute!(&conn, "DELETE FROM products WHERE id = $1", &[&(id as i64)]);
269
270        if let Err(e) = res {
271            return Err(Error::DatabaseError(e.to_string()));
272        }
273
274        // ...
275        self.0.1.remove(format!("atto.product:{}", id)).await;
276        Ok(())
277    }
278
279    auto_method!(update_product_title(&str)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET title = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}");
280    auto_method!(update_product_description(&str)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET description = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}");
281    auto_method!(update_product_price(i32)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET price = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}");
282    auto_method!(update_product_on_sale(i32)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET on_sale = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}");
283    auto_method!(update_product_method(ProductFulfillmentMethod)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET method = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.product:{}");
284    auto_method!(update_product_single_use(i32)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET single_use = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}");
285    auto_method!(update_product_data(&str)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET data = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}");
286    auto_method!(update_product_uploads(ProductUploads)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET uploads = $1 WHERE id = $2" --serde --cache-key-tmpl="atto.product:{}");
287
288    auto_method!(update_product_stock(i32)@get_product_by_id:FinePermission::MANAGE_USERS; -> "UPDATE products SET stock = $1 WHERE id = $2" --cache-key-tmpl="atto.product:{}");
289    auto_method!(incr_product_stock() -> "UPDATE products SET stock = stock + 1 WHERE id = $1" --cache-key-tmpl="atto.product:{}" --incr);
290    auto_method!(decr_product_stock()@get_product_by_id -> "UPDATE products SET stock = stock - 1 WHERE id = $1" --cache-key-tmpl="atto.product:{}" --decr=stock);
291}