tetratto-core 17.0.0

The core behind Tetratto
Documentation
use crate::model::{
    auth::User,
    economy::{
        CoinTransfer, CoinTransferMethod, CoinTransferSource, Product, ProductFulfillmentMethod,
        ProductUploads,
    },
    mail::Letter,
    permissions::FinePermission,
    Error, Result,
};
use crate::{auto_method, DataManager};
use oiseau::{cache::Cache, execute, get, params, query_rows, PostgresRow};

impl DataManager {
    /// Get a [`Product`] from an SQL row.
    pub(crate) fn get_product_from_row(x: &PostgresRow) -> Product {
        Product {
            id: get!(x->0(i64)) as usize,
            created: get!(x->1(i64)) as usize,
            owner: get!(x->2(i64)) as usize,
            title: get!(x->3(String)),
            description: get!(x->4(String)),
            method: serde_json::from_str(&get!(x->5(String))).unwrap(),
            on_sale: get!(x->6(i32)) as i8 == 1,
            price: get!(x->7(i32)),
            stock: get!(x->8(i32)),
            single_use: get!(x->9(i32)) as i8 == 1,
            data: get!(x->10(String)),
            uploads: serde_json::from_str(&get!(x->11(String))).unwrap(),
        }
    }

    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:{}");

    /// Get all products by user.
    ///
    /// # Arguments
    /// * `id` - the ID of the user to fetch products for
    /// * `batch` - the limit of items in each page
    /// * `page` - the page number
    pub async fn get_products_by_user(
        &self,
        id: usize,
        batch: usize,
        page: usize,
    ) -> Result<Vec<Product>> {
        let conn = match self.0.connect().await {
            Ok(c) => c,
            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
        };

        let res = query_rows!(
            &conn,
            "SELECT * FROM products WHERE owner = $1 ORDER BY created DESC LIMIT $2 OFFSET $3",
            &[&(id as i64), &(batch as i64), &((page * batch) as i64)],
            |x| { Self::get_product_from_row(x) }
        );

        if res.is_err() {
            return Err(Error::GeneralNotFound("product".to_string()));
        }

        Ok(res.unwrap())
    }

    const MAXIMUM_FREE_PRODUCTS: usize = 10;

    /// Create a new product in the database.
    ///
    /// # Arguments
    /// * `data` - a mock [`Product`] object to insert
    pub async fn create_product(&self, mut data: Product) -> Result<Product> {
        data.title = data.title.trim().to_string();
        data.description = data.description.trim().to_string();

        // check values
        if data.title.len() < 2 {
            return Err(Error::DataTooShort("title".to_string()));
        } else if data.title.len() > 128 {
            return Err(Error::DataTooLong("title".to_string()));
        }

        if data.description.len() < 2 {
            return Err(Error::DataTooShort("description".to_string()));
        } else if data.description.len() > 1024 {
            return Err(Error::DataTooLong("description".to_string()));
        }

        // check number of stacks
        let owner = self.get_user_by_id(data.owner).await?;

        if !owner.permissions.check(FinePermission::SUPPORTER) {
            let products = self
                .get_table_row_count_where("products", &format!("owner = {}", owner.id))
                .await? as usize;

            if products >= Self::MAXIMUM_FREE_PRODUCTS {
                return Err(Error::MiscError(
                    "You already have the maximum number of products you can have".to_string(),
                ));
            }
        }

        // ...
        let conn = match self.0.connect().await {
            Ok(c) => c,
            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
        };

        let res = execute!(
            &conn,
            "INSERT INTO products VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)",
            params![
                &(data.id as i64),
                &(data.created as i64),
                &(data.owner as i64),
                &data.title,
                &data.description,
                &serde_json::to_string(&data.method).unwrap(),
                &{ if data.on_sale { 1 } else { 0 } },
                &data.price,
                &(data.stock as i32),
                &{ if data.single_use { 1 } else { 0 } },
                &data.data,
                &serde_json::to_string(&data.uploads).unwrap(),
            ]
        );

        if let Err(e) = res {
            return Err(Error::DatabaseError(e.to_string()));
        }

        Ok(data)
    }

    /// Purchase the given product as the given user.
    pub async fn purchase_product(
        &self,
        product: usize,
        customer: &mut User,
    ) -> Result<CoinTransfer> {
        let product = self.get_product_by_id(product).await?;

        // handle single_use product
        if product.single_use {
            if self
                .get_transfer_by_sender_method(
                    customer.id,
                    CoinTransferMethod::Purchase(product.id),
                )
                .await
                .is_ok()
            {
                return Err(Error::MiscError("You already own this product".to_string()));
            }
        }

        // ...
        let mut transfer = CoinTransfer::new(
            customer.id,
            product.owner,
            product.price,
            CoinTransferMethod::Purchase(product.id),
            CoinTransferSource::Sale,
        );

        if !product.stock.is_negative() {
            // check stock
            if product.stock == 0 {
                return Err(Error::MiscError("No remaining stock".to_string()));
            } else {
                self.decr_product_stock(product.id).await?;
            }
        }

        match product.method {
            ProductFulfillmentMethod::AutoMail(message) => {
                // we're basically done, transfer coins and send mail
                self.create_transfer(&mut transfer, true).await?;

                self.create_letter(Letter::new(
                    self.0.0.system_user,
                    vec![customer.id],
                    format!("Thank you for purchasing \"{}\"", product.title),
                    format!("The message below was supplied by the product owner, and was automatically sent.\n***\n{message}"),
                    0,
                ))
                .await?;

                Ok(transfer)
            }
            ProductFulfillmentMethod::ManualMail => {
                // mark transfer as pending and create it
                self.create_transfer(&mut transfer, false).await?;

                // tell the customer to wait
                self.create_letter(Letter::new(
                    self.0.0.system_user,
                    vec![customer.id],
                    format!("Thank you for purchasing \"{}\"", product.title),
                    "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(),
                    0,
                ))
                .await?;

                // tell product owner they have a new pending purchase
                self.create_letter(Letter::new(
                    self.0.0.system_user,
                    vec![product.owner],
                    "New product purchase pending".to_string(),
                    format!(
                        "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**.

If 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.

***
<a class=\"button\" href=\"/mail/compose?receivers=id:{}&subject=Product%20fulfillment&transfer_id={}\">Fulfill purchase</a>",
                        product.id, product.title, customer.id, transfer.id
                    ),
                    0,
                ))
                .await?;

                // return
                Ok(transfer)
            }
            ProductFulfillmentMethod::ProfileStyle => {
                // pretty much an automail without the message
                self.create_transfer(&mut transfer, true).await?;

                self.create_letter(Letter::new(
                    self.0.0.system_user,
                    vec![customer.id],
                    format!("Thank you for purchasing \"{}\"", product.title),
                    "You've purchased a CSS snippet which can be applied to your profile through the product's page!".to_string(),
                    0,
                ))
                .await?;

                Ok(transfer)
            }
        }
    }

    pub async fn delete_product(&self, id: usize, user: &User) -> Result<()> {
        let product = self.get_product_by_id(id).await?;

        // check user permission
        if user.id != product.owner && !user.permissions.check(FinePermission::MANAGE_USERS) {
            return Err(Error::NotAllowed);
        }

        // remove uploads
        for upload in product.uploads.thumbnails {
            if let Err(e) = self.2.delete_upload(upload).await {
                return Err(Error::MiscError(e.to_string()));
            };
        }

        if product.uploads.reward != 0 {
            if let Err(e) = self.2.delete_upload(product.uploads.reward).await {
                return Err(Error::MiscError(e.to_string()));
            }
        }

        // ...
        let conn = match self.0.connect().await {
            Ok(c) => c,
            Err(e) => return Err(Error::DatabaseConnection(e.to_string())),
        };

        let res = execute!(&conn, "DELETE FROM products WHERE id = $1", &[&(id as i64)]);

        if let Err(e) = res {
            return Err(Error::DatabaseError(e.to_string()));
        }

        // ...
        self.0.1.remove(format!("atto.product:{}", id)).await;
        Ok(())
    }

    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:{}");
    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:{}");
    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:{}");
    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:{}");
    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:{}");
    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:{}");
    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:{}");
    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:{}");

    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:{}");
    auto_method!(incr_product_stock() -> "UPDATE products SET stock = stock + 1 WHERE id = $1" --cache-key-tmpl="atto.product:{}" --incr);
    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);
}