use anyhow::{anyhow, Error, Result}; use chrono::prelude::*; use http::StatusCode; use http_api_problem::HttpApiProblem; use serde::{Deserialize, Serialize}; use serde_json::json; use sqlx::types::Json; use sqlx::{Done, Executor, Postgres}; use tracing::instrument; use url::Url; use super::ListParams; use crate::problem::forbidden_permission; #[derive(Debug, Serialize, Deserialize, Clone)] pub struct Merchandise { pub mod_name: String, pub local_form_id: u32, pub name: String, pub quantity: u32, pub form_type: u32, pub is_food: bool, pub price: u32, pub keywords: Vec, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct MerchandiseList { pub id: i32, pub shop_id: i32, pub owner_id: i32, pub form_list: Json>, pub created_at: NaiveDateTime, pub updated_at: NaiveDateTime, } #[derive(Debug, Serialize, Deserialize, Clone)] pub struct PostedMerchandiseList { pub shop_id: i32, pub owner_id: Option, pub form_list: Json>, } impl MerchandiseList { pub fn resource_name() -> &'static str { "merchandise_list" } pub fn pk(&self) -> i32 { self.id } pub fn url(&self, api_url: &Url) -> Result { Ok(api_url.join(&format!("{}s/{}", Self::resource_name(), self.pk()))?) } // TODO: this model will probably never need to be accessed through it's ID, should these methods be removed/unimplemented? #[instrument(level = "debug", skip(db))] pub async fn get(db: impl Executor<'_, Database = Postgres>, id: i32) -> Result { sqlx::query_as!( Self, r#"SELECT id, shop_id, owner_id, created_at, updated_at, form_list as "form_list: Json>" FROM merchandise_lists WHERE id = $1"#, id, ) .fetch_one(db) .await .map_err(Error::new) } #[instrument(level = "debug", skip(merchandise_list, db))] pub async fn create( merchandise_list: PostedMerchandiseList, db: impl Executor<'_, Database = Postgres>, ) -> Result { Ok(sqlx::query_as!( Self, r#"INSERT INTO merchandise_lists (shop_id, owner_id, form_list, created_at, updated_at) VALUES ($1, $2, $3, now(), now()) RETURNING id, shop_id, owner_id, created_at, updated_at, form_list as "form_list: Json>""#, merchandise_list.shop_id, merchandise_list.owner_id, serde_json::json!(merchandise_list.form_list), ) .fetch_one(db) .await?) } #[instrument(level = "debug", skip(db))] pub async fn delete( db: impl Executor<'_, Database = Postgres> + Copy, owner_id: i32, id: i32, ) -> Result { let merchandise_list = sqlx::query!("SELECT owner_id FROM merchandise_lists WHERE id = $1", id) .fetch_one(db) .await?; if merchandise_list.owner_id == owner_id { return Ok( sqlx::query!("DELETE FROM merchandise_lists WHERE id = $1", id) .execute(db) .await? .rows_affected(), ); } else { return Err(forbidden_permission()); } } #[instrument(level = "debug", skip(db))] pub async fn list( db: impl Executor<'_, Database = Postgres>, list_params: &ListParams, ) -> Result> { let result = if let Some(order_by) = list_params.get_order_by() { sqlx::query_as!( Self, r#"SELECT id, shop_id, owner_id, created_at, updated_at, form_list as "form_list: Json>" FROM merchandise_lists ORDER BY $1 LIMIT $2 OFFSET $3"#, order_by, list_params.limit.unwrap_or(10), list_params.offset.unwrap_or(0), ) .fetch_all(db) .await? } else { sqlx::query_as!( Self, r#"SELECT id, shop_id, owner_id, created_at, updated_at, form_list as "form_list: Json>" FROM merchandise_lists LIMIT $1 OFFSET $2"#, list_params.limit.unwrap_or(10), list_params.offset.unwrap_or(0), ) .fetch_all(db) .await? }; Ok(result) } #[instrument(level = "debug", skip(merchandise_list, db))] pub async fn update( merchandise_list: PostedMerchandiseList, db: impl Executor<'_, Database = Postgres> + Copy, owner_id: i32, id: i32, ) -> Result { let existing_merchandise_list = sqlx::query!("SELECT owner_id FROM merchandise_lists WHERE id = $1", id) .fetch_one(db) .await?; if existing_merchandise_list.owner_id == owner_id { Ok(sqlx::query_as!( Self, r#"UPDATE merchandise_lists SET form_list = $2, updated_at = now() WHERE id = $1 RETURNING id, shop_id, owner_id, created_at, updated_at, form_list as "form_list: Json>""#, id, serde_json::json!(merchandise_list.form_list), ) .fetch_one(db) .await?) } else { return Err(forbidden_permission()); } } #[instrument(level = "debug", skip(db))] pub async fn get_by_shop_id( db: impl Executor<'_, Database = Postgres>, shop_id: i32, ) -> Result { sqlx::query_as!( Self, r#"SELECT id, shop_id, owner_id, created_at, updated_at, form_list as "form_list: Json>" FROM merchandise_lists WHERE shop_id = $1"#, shop_id, ) .fetch_one(db) .await .map_err(Error::new) } #[instrument(level = "debug", skip(merchandise_list, db))] pub async fn update_by_shop_id( merchandise_list: PostedMerchandiseList, db: impl Executor<'_, Database = Postgres> + Copy, owner_id: i32, shop_id: i32, ) -> Result { let existing_merchandise_list = sqlx::query!( "SELECT owner_id FROM merchandise_lists WHERE shop_id = $1", shop_id ) .fetch_one(db) .await?; if existing_merchandise_list.owner_id == owner_id { Ok(sqlx::query_as!( Self, r#"UPDATE merchandise_lists SET form_list = $2, updated_at = now() WHERE shop_id = $1 RETURNING id, shop_id, owner_id, created_at, updated_at, form_list as "form_list: Json>""#, shop_id, serde_json::json!(merchandise_list.form_list), ) .fetch_one(db) .await?) } else { return Err(forbidden_permission()); } } #[instrument(level = "debug", skip(db))] pub async fn update_merchandise_quantity( db: impl Executor<'_, Database = Postgres>, shop_id: i32, mod_name: &str, local_form_id: i32, name: &str, form_type: i32, is_food: bool, price: i32, quantity_delta: i32, ) -> Result { let add_item = json!([{ "mod_name": mod_name, "local_form_id": local_form_id, "name": name, "quantity": quantity_delta, "form_type": form_type, "is_food": is_food, "price": price, }]); Ok(sqlx::query_as!( Self, r#"UPDATE merchandise_lists SET form_list = CASE WHEN elem_index IS NULL AND quantity IS NULL AND $4 > 0 THEN form_list || $5 WHEN elem_index IS NOT NULL AND quantity IS NOT NULL AND quantity::int + $4 = 0 THEN form_list - elem_index::int WHEN elem_index IS NOT NULL AND quantity IS NOT NULL THEN jsonb_set( form_list, array[elem_index::text, 'quantity'], to_jsonb(quantity::int + $4), true ) ELSE NULL END FROM ( SELECT pos - 1 as elem_index, elem->>'quantity' as quantity FROM merchandise_lists, jsonb_array_elements(form_list) with ordinality arr(elem, pos) WHERE shop_id = $1 AND elem->>'mod_name' = $2::text AND elem->>'local_form_id' = $3::text UNION ALL SELECT NULL as elem_index, NULL as quantity LIMIT 1 ) sub WHERE shop_id = $1 RETURNING merchandise_lists.id, merchandise_lists.shop_id, merchandise_lists.owner_id, merchandise_lists.created_at, merchandise_lists.updated_at, merchandise_lists.form_list as "form_list: Json>""#, shop_id, mod_name, &local_form_id.to_string(), quantity_delta, add_item, ) .fetch_one(db) .await .map_err(|error| { let anyhow_error = anyhow!(error); if let Some(db_error) = anyhow_error.downcast_ref::() { if db_error.code() == "23502" && db_error.column() == Some("form_list") { return anyhow!(HttpApiProblem::with_title_and_type_from_status( StatusCode::NOT_FOUND ) .set_detail(format!( "Cannot find merchandise to buy with mod_name: {} and local_form_id: {:#010X}", mod_name, local_form_id ))); } } anyhow_error })?) } }