Check keywords on transaction, update shop gold

Shops now have a "type" and keywords of items they only deal with.

Also add vendors table in preparation for implementing the endpoint.
This commit is contained in:
Tyler Hallada 2021-02-12 00:35:19 -05:00
parent a64caa4081
commit 6b07ec7d07
4 changed files with 116 additions and 7 deletions

View File

@ -13,6 +13,11 @@ CREATE TABLE "shops" (
"name" VARCHAR(255) NOT NULL, "name" VARCHAR(255) NOT NULL,
"owner_id" INTEGER REFERENCES "owners"(id) NOT NULL, "owner_id" INTEGER REFERENCES "owners"(id) NOT NULL,
"description" TEXT, "description" TEXT,
"gold" INTEGER NOT NULL DEFAULT 0
CONSTRAINT "shop_gold_gt_zero" CHECK (gold >= 0),
"shop_type" VARCHAR(255) NOT NULL DEFAULT 'general_store',
"vendor_keywords" TEXT[] NOT NULL DEFAULT '{"VendorItemKey", "VendorNoSale"}',
"vendor_keywords_exclude" BOOLEAN NOT NULL DEFAULT true,
"created_at" timestamp(3) NOT NULL, "created_at" timestamp(3) NOT NULL,
"updated_at" timestamp(3) NOT NULL "updated_at" timestamp(3) NOT NULL
); );
@ -36,6 +41,14 @@ CREATE TABLE "merchandise_lists" (
"updated_at" timestamp(3) NOT NULL "updated_at" timestamp(3) NOT NULL
); );
CREATE INDEX "merchandise_lists_mod_name_and_local_form_id" ON "merchandise_lists" USING GIN (form_list jsonb_path_ops); CREATE INDEX "merchandise_lists_mod_name_and_local_form_id" ON "merchandise_lists" USING GIN (form_list jsonb_path_ops);
CREATE TABLE "vendors" (
"id" SERIAL PRIMARY KEY NOT NULL,
"shop_id" INTEGER REFERENCES "shops"(id) NOT NULL UNIQUE,
"owner_id" INTEGER REFERENCES "owners"(id) NOT NULL,
"name" VARCHAR(255) NOT NULL,
"body_preset" INTEGER NOT NULL
);
CREATE UNIQUE INDEX "vendors_unique_name_and_owner_id" ON "vendors" ("name", "owner_id", "shop_id");
CREATE TABLE "transactions" ( CREATE TABLE "transactions" (
"id" SERIAL PRIMARY KEY NOT NULL, "id" SERIAL PRIMARY KEY NOT NULL,
"shop_id" INTEGER REFERENCES "shops"(id) NOT NULL, "shop_id" INTEGER REFERENCES "shops"(id) NOT NULL,

View File

@ -3,4 +3,5 @@ DROP TABLE shops CASCADE;
DROP TABLE interior_ref_lists CASCADE; DROP TABLE interior_ref_lists CASCADE;
DROP TABLE merchandise_lists CASCADE; DROP TABLE merchandise_lists CASCADE;
DROP TABLE transactions CASCADE; DROP TABLE transactions CASCADE;
DROP TABLE vendors CASCADE;
DROP TABLE _sqlx_migrations CASCADE; DROP TABLE _sqlx_migrations CASCADE;

View File

@ -1,13 +1,14 @@
use anyhow::{anyhow, Result}; use anyhow::{anyhow, Result};
use http::StatusCode; use http::StatusCode;
use http_api_problem::HttpApiProblem;
use hyper::body::Bytes; use hyper::body::Bytes;
use mime::Mime; use mime::Mime;
use uuid::Uuid; use uuid::Uuid;
use warp::reply::{with_header, with_status}; use warp::reply::{with_header, with_status};
use warp::{Rejection, Reply}; use warp::{reject, Rejection, Reply};
use crate::caches::{CachedResponse, CACHES}; use crate::caches::{CachedResponse, CACHES};
use crate::models::{ListParams, MerchandiseList, PostedTransaction, Transaction}; use crate::models::{ListParams, MerchandiseList, PostedTransaction, Shop, Transaction};
use crate::problem::reject_anyhow; use crate::problem::reject_anyhow;
use crate::Environment; use crate::Environment;
@ -128,9 +129,22 @@ pub async fn create(
let saved_transaction = Transaction::create(transaction, &mut tx) let saved_transaction = Transaction::create(transaction, &mut tx)
.await .await
.map_err(reject_anyhow)?; .map_err(reject_anyhow)?;
let quantity_delta = match saved_transaction.is_sell { if !Shop::accepts_keywords(
true => saved_transaction.quantity, &mut tx,
false => saved_transaction.quantity * -1, saved_transaction.shop_id,
&saved_transaction.keywords,
)
.await
.map_err(reject_anyhow)?
{
return Err(reject::custom(
HttpApiProblem::with_title_and_type_from_status(StatusCode::BAD_REQUEST)
.set_detail("Shop does not accept that kind of merchandise"),
));
}
let (quantity_delta, shop_gold_delta) = match saved_transaction.is_sell {
true => (saved_transaction.quantity, saved_transaction.price * -1),
false => (saved_transaction.quantity * -1, saved_transaction.price),
}; };
let updated_merchandise_list = MerchandiseList::update_merchandise_quantity( let updated_merchandise_list = MerchandiseList::update_merchandise_quantity(
&mut tx, &mut tx,
@ -146,6 +160,9 @@ pub async fn create(
) )
.await .await
.map_err(reject_anyhow)?; .map_err(reject_anyhow)?;
Shop::update_gold(&mut tx, saved_transaction.shop_id, shop_gold_delta)
.await
.map_err(reject_anyhow)?;
tx.commit() tx.commit()
.await .await
.map_err(|error| reject_anyhow(anyhow!(error)))?; .map_err(|error| reject_anyhow(anyhow!(error)))?;
@ -184,10 +201,21 @@ pub async fn create(
CACHES.list_transactions_by_shop_id_bin.clear().await; CACHES.list_transactions_by_shop_id_bin.clear().await;
CACHES.list_merchandise_lists.clear().await; CACHES.list_merchandise_lists.clear().await;
CACHES.list_merchandise_lists_bin.clear().await; CACHES.list_merchandise_lists_bin.clear().await;
CACHES
.shop
.delete_response(updated_merchandise_list.shop_id)
.await;
CACHES
.shop_bin
.delete_response(updated_merchandise_list.shop_id)
.await;
CACHES.list_shops.clear().await;
CACHES.list_shops_bin.clear().await;
}); });
Ok(reply) Ok(reply)
} }
// Does NOT reverse the transaction side-effects!
pub async fn delete( pub async fn delete(
id: i32, id: i32,
api_key: Option<Uuid>, api_key: Option<Uuid>,

View File

@ -14,6 +14,10 @@ pub struct Shop {
pub name: String, pub name: String,
pub owner_id: i32, pub owner_id: i32,
pub description: Option<String>, pub description: Option<String>,
pub gold: i32,
pub shop_type: String,
pub vendor_keywords: Vec<String>,
pub vendor_keywords_exclude: bool,
pub created_at: NaiveDateTime, pub created_at: NaiveDateTime,
pub updated_at: NaiveDateTime, pub updated_at: NaiveDateTime,
} }
@ -23,6 +27,10 @@ pub struct PostedShop {
pub name: String, pub name: String,
pub owner_id: Option<i32>, pub owner_id: Option<i32>,
pub description: Option<String>, pub description: Option<String>,
pub gold: Option<i32>,
pub shop_type: Option<String>,
pub vendor_keywords: Option<Vec<String>>,
pub vendor_keywords_exclude: Option<bool>,
} }
impl Shop { impl Shop {
@ -54,12 +62,19 @@ impl Shop {
Ok(sqlx::query_as!( Ok(sqlx::query_as!(
Self, Self,
"INSERT INTO shops "INSERT INTO shops
(name, owner_id, description, created_at, updated_at) (name, owner_id, description, gold, shop_type, vendor_keywords,
VALUES ($1, $2, $3, now(), now()) vendor_keywords_exclude, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, now(), now())
RETURNING *", RETURNING *",
shop.name, shop.name,
shop.owner_id, shop.owner_id,
shop.description, shop.description,
shop.gold.unwrap_or(0),
shop.shop_type.unwrap_or("general_store".to_string()),
&shop
.vendor_keywords
.unwrap_or_else(|| vec!["VendorItemKey".to_string(), "VendorNoSale".to_string()]),
shop.vendor_keywords_exclude.unwrap_or(true),
) )
.fetch_one(db) .fetch_one(db)
.await?) .await?)
@ -134,6 +149,10 @@ impl Shop {
name = $2, name = $2,
owner_id = $3, owner_id = $3,
description = $4, description = $4,
gold = $5,
shop_type = $6,
vendor_keywords = $7,
vendor_keywords_exclude = $8,
updated_at = now() updated_at = now()
WHERE id = $1 WHERE id = $1
RETURNING *", RETURNING *",
@ -141,6 +160,10 @@ impl Shop {
shop.name, shop.name,
shop.owner_id, shop.owner_id,
shop.description, shop.description,
shop.gold,
shop.shop_type,
&shop.vendor_keywords.unwrap_or_else(|| vec![]),
shop.vendor_keywords_exclude,
) )
.fetch_one(db) .fetch_one(db)
.await?) .await?)
@ -148,4 +171,48 @@ impl Shop {
return Err(forbidden_permission()); return Err(forbidden_permission());
} }
} }
#[instrument(level = "debug", skip(db))]
pub async fn accepts_keywords(
db: impl Executor<'_, Database = Postgres>,
id: i32,
keywords: &[String],
) -> Result<bool> {
// Macro not available, see: https://github.com/launchbadge/sqlx/issues/428
Ok(sqlx::query_scalar(
"SELECT EXISTS (
SELECT 1 FROM shops
WHERE id = $1
AND ((
vendor_keywords_exclude = true AND
NOT vendor_keywords && $2
) OR (
vendor_keywords_exclude = false AND
vendor_keywords && $2
))
)",
)
.bind(id)
.bind(keywords)
.fetch_one(db)
.await?)
}
#[instrument(level = "debug", skip(db))]
pub async fn update_gold(
db: impl Executor<'_, Database = Postgres>,
id: i32,
gold_delta: i32,
) -> Result<()> {
sqlx::query!(
"UPDATE shops SET
gold = gold + $2
WHERE id = $1",
id,
gold_delta,
)
.execute(db)
.await?;
Ok(())
}
} }