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 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 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 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 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 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 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 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 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 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 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 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 self.create_transfer(&mut transfer, false).await?;
195
196 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 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 Ok(transfer)
226 }
227 ProductFulfillmentMethod::ProfileStyle => {
228 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 if user.id != product.owner && !user.permissions.check(FinePermission::MANAGE_USERS) {
250 return Err(Error::NotAllowed);
251 }
252
253 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 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 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}