Work on stocks database
This commit is contained in:
parent
2f0a43c8ef
commit
755363c23f
15
Cargo.lock
generated
15
Cargo.lock
generated
@ -11,6 +11,7 @@ dependencies = [
|
||||
"channels",
|
||||
"config",
|
||||
"dotenv",
|
||||
"fake",
|
||||
"futures 0.3.25",
|
||||
"gumdrop",
|
||||
"json",
|
||||
@ -23,6 +24,7 @@ dependencies = [
|
||||
"sqlx",
|
||||
"sqlx-core",
|
||||
"tarpc",
|
||||
"testx",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
@ -1253,6 +1255,15 @@ dependencies = [
|
||||
"tracing-subscriber",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "db-utils"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"model",
|
||||
"sqlx",
|
||||
"sqlx-core",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dbg"
|
||||
version = "1.0.4"
|
||||
@ -4092,8 +4103,10 @@ dependencies = [
|
||||
"channels",
|
||||
"chrono",
|
||||
"config",
|
||||
"db-utils",
|
||||
"derive_more",
|
||||
"dotenv",
|
||||
"fake",
|
||||
"futures 0.3.25",
|
||||
"model",
|
||||
"opentelemetry 0.17.0",
|
||||
@ -4104,11 +4117,13 @@ dependencies = [
|
||||
"sqlx",
|
||||
"sqlx-core",
|
||||
"tarpc",
|
||||
"testx",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tracing",
|
||||
"tracing-opentelemetry",
|
||||
"tracing-subscriber",
|
||||
"uuid 1.2.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
|
@ -5,6 +5,7 @@ members = [
|
||||
"crates/channels",
|
||||
"crates/config",
|
||||
"crates/testx",
|
||||
"crates/db-utils",
|
||||
# actors
|
||||
"crates/account_manager",
|
||||
"crates/cart_manager",
|
||||
|
@ -30,3 +30,7 @@ tokio = { version = "1.21.2", features = ['full'] }
|
||||
tracing = { version = "0.1.6" }
|
||||
tracing-opentelemetry = { version = "0.17.4" }
|
||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||
|
||||
[dev-dependencies]
|
||||
fake = { version = "2.5.0" }
|
||||
testx = { path = "../testx" }
|
||||
|
@ -234,7 +234,7 @@ mod tests {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn create_account() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
@ -266,7 +266,7 @@ mod tests {
|
||||
assert_eq!(account, expected);
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn all_accounts() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
@ -280,7 +280,7 @@ mod tests {
|
||||
assert!(v.len() >= 3);
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn update_account_without_pass() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
@ -326,7 +326,7 @@ mod tests {
|
||||
assert_eq!(updated_account, expected);
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn update_account_with_pass() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
@ -373,7 +373,7 @@ mod tests {
|
||||
assert_eq!(updated_account, expected);
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn find() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
@ -390,7 +390,7 @@ mod tests {
|
||||
assert_eq!(account, res);
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn find_identity_email() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
@ -408,7 +408,7 @@ mod tests {
|
||||
assert_eq!(account, res);
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn find_identity_login() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
|
@ -215,7 +215,7 @@ mod test {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[actix::test]
|
||||
#[tokio::test]
|
||||
async fn full_check() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
|
9
crates/db-utils/Cargo.toml
Normal file
9
crates/db-utils/Cargo.toml
Normal file
@ -0,0 +1,9 @@
|
||||
[package]
|
||||
name = "db-utils"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
[dependencies]
|
||||
model = { path = "../model" }
|
||||
sqlx = { version = "0.6.2", features = ["migrate", "runtime-actix-rustls", "all-types", "postgres"] }
|
||||
sqlx-core = { version = "0.6.2", features = [] }
|
97
crates/db-utils/src/lib.rs
Normal file
97
crates/db-utils/src/lib.rs
Normal file
@ -0,0 +1,97 @@
|
||||
use sqlx::Arguments;
|
||||
|
||||
pub struct MultiLoad<'transaction, 'transaction2, 'header, 'condition, T> {
|
||||
pool: &'transaction mut sqlx::Transaction<'transaction2, sqlx::Postgres>,
|
||||
header: &'header str,
|
||||
condition: &'condition str,
|
||||
sort: Option<String>,
|
||||
size: Option<usize>,
|
||||
__phantom: std::marker::PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'transaction, 'transaction2, 'header, 'condition, T>
|
||||
MultiLoad<'transaction, 'transaction2, 'header, 'condition, T>
|
||||
where
|
||||
T: for<'r> sqlx::FromRow<'r, sqlx::postgres::PgRow> + Send + Unpin,
|
||||
{
|
||||
pub fn new(
|
||||
pool: &'transaction mut sqlx::Transaction<'transaction2, sqlx::Postgres>,
|
||||
header: &'header str,
|
||||
condition: &'condition str,
|
||||
) -> Self {
|
||||
Self {
|
||||
pool,
|
||||
header,
|
||||
condition,
|
||||
sort: None,
|
||||
size: None,
|
||||
__phantom: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn with_sorting<S: Into<String>>(mut self, order: S) -> Self {
|
||||
self.sort = Some(order.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_size(mut self, size: usize) -> Self {
|
||||
self.size = Some(size);
|
||||
self
|
||||
}
|
||||
|
||||
pub async fn load<'query, Error, ErrorFn, Ids>(
|
||||
&mut self,
|
||||
len: usize,
|
||||
items: Ids,
|
||||
on_error: ErrorFn,
|
||||
) -> Result<Vec<T>, Error>
|
||||
where
|
||||
Ids: Iterator<Item = model::RecordId>,
|
||||
ErrorFn: Fn(sqlx::Error) -> Error,
|
||||
{
|
||||
let mut res = Vec::new();
|
||||
let size = self.size.unwrap_or(20).min(200);
|
||||
|
||||
for ids in items.fold(
|
||||
Vec::<Vec<model::RecordId>>::with_capacity(len),
|
||||
|mut v, id| {
|
||||
let last_len = v.last().map(|v| v.len());
|
||||
if last_len == Some(size) || last_len.is_none() {
|
||||
v.push(Vec::with_capacity(size));
|
||||
}
|
||||
v.last_mut().unwrap().push(id);
|
||||
v
|
||||
},
|
||||
) {
|
||||
let query: String = self.header.into();
|
||||
let mut query = ids.iter().enumerate().fold(query, |mut q, (idx, _id)| {
|
||||
if idx != 0 {
|
||||
q.push_str(" OR");
|
||||
}
|
||||
q.push_str(&format!(" {} ${}", self.condition, idx + 1));
|
||||
q
|
||||
});
|
||||
if let Some(s) = self.sort.as_deref() {
|
||||
query.push_str("\nORDER BY ");
|
||||
query.push_str(s);
|
||||
query.push(' ');
|
||||
}
|
||||
let q = sqlx::query_as_with(
|
||||
query.as_str(),
|
||||
ids.into_iter()
|
||||
.fold(sqlx::postgres::PgArguments::default(), |mut args, id| {
|
||||
args.add(id);
|
||||
args
|
||||
}),
|
||||
);
|
||||
|
||||
let records: Vec<T> = match q.fetch_all(&mut *self.pool).await {
|
||||
Ok(rec) => rec,
|
||||
Err(e) => return Err(on_error(e)),
|
||||
};
|
||||
res.extend(records);
|
||||
}
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
}
|
@ -11,6 +11,7 @@ path = "./src/main.rs"
|
||||
channels = { path = "../channels" }
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
config = { path = "../config" }
|
||||
db-utils = { path = "../db-utils" }
|
||||
derive_more = { version = "0.99", features = [] }
|
||||
dotenv = { version = "0.15.0" }
|
||||
futures = { version = "0.3.25" }
|
||||
@ -28,3 +29,8 @@ tokio = { version = "1.21.2", features = ['full'] }
|
||||
tracing = { version = "0.1.6" }
|
||||
tracing-opentelemetry = { version = "0.17.4" }
|
||||
tracing-subscriber = { version = "0.3.16", features = ["env-filter"] }
|
||||
uuid = { version = "1.2.1" }
|
||||
|
||||
[dev-dependencies]
|
||||
fake = { version = "2.5.0" }
|
||||
testx = { path = "../testx" }
|
||||
|
@ -4,6 +4,7 @@ use sqlx_core::postgres::Postgres;
|
||||
|
||||
mod photos;
|
||||
mod product_photos;
|
||||
mod product_variants;
|
||||
mod products;
|
||||
mod stocks;
|
||||
|
||||
|
55
crates/stock_manager/src/db/product_variants.rs
Normal file
55
crates/stock_manager/src/db/product_variants.rs
Normal file
@ -0,0 +1,55 @@
|
||||
use model::v2::*;
|
||||
|
||||
#[derive(Debug, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("")]
|
||||
CreateProductVariant,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CreateProductVariant {
|
||||
pub product_id: ProductId,
|
||||
pub name: ProductName,
|
||||
pub short_description: ProductShortDesc,
|
||||
pub long_description: ProductLongDesc,
|
||||
pub price: Price,
|
||||
}
|
||||
|
||||
impl CreateProductVariant {
|
||||
pub async fn run(
|
||||
self,
|
||||
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<ProductVariant> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO product_variants (
|
||||
product_id,
|
||||
name,
|
||||
short_description,
|
||||
long_description,
|
||||
price
|
||||
) VALUES ($1, $2, $3, $4, $5)
|
||||
RETURNINGS id,
|
||||
product_id,
|
||||
name,
|
||||
short_description,
|
||||
long_description,
|
||||
price
|
||||
"#,
|
||||
)
|
||||
.bind(self.product_id)
|
||||
.bind(self.name)
|
||||
.bind(self.short_description)
|
||||
.bind(self.long_description)
|
||||
.bind(self.price)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("{}", e);
|
||||
dbg!(e);
|
||||
Error::CreateProductVariant
|
||||
})
|
||||
}
|
||||
}
|
@ -1 +1,377 @@
|
||||
use model::v2::*;
|
||||
use model::ShoppingCartId;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Unable to load all products")]
|
||||
All,
|
||||
#[error("Unable to create product")]
|
||||
Create,
|
||||
#[error("Unable to update product")]
|
||||
Update(ProductId),
|
||||
#[error("Unable to delete product")]
|
||||
Delete(ProductId),
|
||||
#[error("Unable to find products for shopping cart")]
|
||||
ShoppingCartProducts(ShoppingCartId),
|
||||
#[error("Product with id {0} can't be found")]
|
||||
Single(ProductId),
|
||||
#[error("Failed to load products for given ids")]
|
||||
FindProducts,
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AllProducts {
|
||||
limit: i32,
|
||||
offset: i32,
|
||||
}
|
||||
|
||||
impl AllProducts {
|
||||
pub async fn run<'e, E>(self, pool: E) -> Result<Vec<model::Product>>
|
||||
where
|
||||
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
|
||||
{
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT id,
|
||||
name,
|
||||
category,
|
||||
deliver_days_flag
|
||||
FROM products
|
||||
ORDER BY id
|
||||
LIMIT $1 OFFSET $2
|
||||
"#,
|
||||
)
|
||||
.bind(self.limit.max(1).min(200))
|
||||
.bind(self.offset.max(0))
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("{e:?}");
|
||||
Error::All
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FindProduct {
|
||||
pub product_id: ProductId,
|
||||
}
|
||||
|
||||
impl FindProduct {
|
||||
pub async fn run<'e, E>(self, pool: E) -> Result<model::Product>
|
||||
where
|
||||
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
|
||||
{
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT id,
|
||||
name,
|
||||
category,
|
||||
deliver_days_flag
|
||||
FROM products
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(self.product_id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("{e:?}");
|
||||
Error::Single(self.product_id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CreateProduct {
|
||||
pub name: ProductName,
|
||||
pub category: Option<ProductCategory>,
|
||||
pub deliver_days_flag: Days,
|
||||
}
|
||||
|
||||
impl CreateProduct {
|
||||
pub async fn run<'e, E>(self, pool: E) -> Result<model::Product>
|
||||
where
|
||||
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
|
||||
{
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO products (name, category, deliver_days_flag)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id,
|
||||
name,
|
||||
category,
|
||||
deliver_days_flag
|
||||
"#,
|
||||
)
|
||||
.bind(self.name)
|
||||
.bind(self.category)
|
||||
.bind(self.deliver_days_flag)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("{e:?}");
|
||||
dbg!(e);
|
||||
Error::Create
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UpdateProduct {
|
||||
pub id: ProductId,
|
||||
pub name: ProductName,
|
||||
pub category: Option<ProductCategory>,
|
||||
pub deliver_days_flag: Days,
|
||||
}
|
||||
|
||||
impl UpdateProduct {
|
||||
pub async fn run<'e, E>(self, pool: E) -> Result<Product>
|
||||
where
|
||||
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
|
||||
{
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
UPDATE products
|
||||
SET name = $2,
|
||||
category = $3,
|
||||
deliver_days_flag = $4
|
||||
WHERE id = $1
|
||||
RETURNING id,
|
||||
name,
|
||||
category,
|
||||
deliver_days_flag
|
||||
"#,
|
||||
)
|
||||
.bind(self.id)
|
||||
.bind(self.name)
|
||||
.bind(self.category)
|
||||
.bind(self.deliver_days_flag)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("{e:?}");
|
||||
dbg!(e);
|
||||
Error::Update(self.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DeleteProduct {
|
||||
pub product_id: ProductId,
|
||||
}
|
||||
|
||||
impl DeleteProduct {
|
||||
pub async fn run<'e, E>(self, pool: E) -> Result<Option<Product>>
|
||||
where
|
||||
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
|
||||
{
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
DELETE FROM products
|
||||
WHERE id = $1
|
||||
RETURNING id,
|
||||
name,
|
||||
category,
|
||||
deliver_days_flag
|
||||
"#,
|
||||
)
|
||||
.bind(self.product_id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("{e:?}");
|
||||
Error::Delete(self.product_id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ShoppingCartProducts {
|
||||
pub shopping_cart_id: ShoppingCartId,
|
||||
pub limit: i32,
|
||||
pub offset: i32,
|
||||
}
|
||||
|
||||
impl ShoppingCartProducts {
|
||||
pub async fn shopping_cart_products<'e, E>(self, pool: E) -> Result<Vec<Product>>
|
||||
where
|
||||
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
|
||||
{
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT products.id,
|
||||
products.name,
|
||||
products.category,
|
||||
products.deliver_days_flag
|
||||
FROM products
|
||||
INNER JOIN shopping_cart_items ON shopping_cart_items.product_id = products.id
|
||||
WHERE shopping_cart_id = $1
|
||||
ORDER BY products.id
|
||||
LIMIT $2 OFFSET $3
|
||||
"#,
|
||||
)
|
||||
.bind(self.shopping_cart_id)
|
||||
.bind(self.limit.min(1).max(200))
|
||||
.bind(self.offset.min(0))
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("{e:?}");
|
||||
Error::ShoppingCartProducts(self.shopping_cart_id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FindProducts {
|
||||
pub product_ids: Vec<ProductId>,
|
||||
}
|
||||
|
||||
impl FindProducts {
|
||||
pub async fn run(
|
||||
self,
|
||||
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
) -> Result<Vec<Product>> {
|
||||
db_utils::MultiLoad::new(
|
||||
pool,
|
||||
r#"
|
||||
SELECT id,
|
||||
name,
|
||||
category,
|
||||
deliver_days_flag
|
||||
FROM products
|
||||
WHERE
|
||||
"#,
|
||||
"products.id =",
|
||||
)
|
||||
.with_size(200)
|
||||
.load(
|
||||
self.product_ids.len(),
|
||||
self.product_ids.into_iter().map(|id| *id),
|
||||
|e| {
|
||||
tracing::error!("{e:?}");
|
||||
Error::FindProducts
|
||||
},
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use config::UpdateConfig;
|
||||
use model::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct NoOpts;
|
||||
|
||||
impl UpdateConfig for NoOpts {}
|
||||
|
||||
use super::*;
|
||||
|
||||
async fn test_product(
|
||||
t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
name: Option<String>,
|
||||
short_description: Option<String>,
|
||||
long_description: Option<String>,
|
||||
category: Option<ProductCategory>,
|
||||
price: Option<u32>,
|
||||
deliver_days_flag: Option<Days>,
|
||||
) -> Product {
|
||||
CreateProduct {
|
||||
name: ProductName::new(name.unwrap_or_else(|| format!("{}", Uuid::new_v4()))),
|
||||
category,
|
||||
deliver_days_flag: deliver_days_flag
|
||||
.unwrap_or_else(|| Days(vec![Day::Friday, Day::Sunday])),
|
||||
}
|
||||
.run(t)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
test_product(&mut t, None, None, None, None, None, None).await;
|
||||
|
||||
testx::db_rollback!(t);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn all() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
let p1 = test_product(&mut t, None, None, None, None, None, None).await;
|
||||
let p2 = test_product(&mut t, None, None, None, None, None, None).await;
|
||||
let p3 = test_product(&mut t, None, None, None, None, None, None).await;
|
||||
|
||||
let products = super::all(AllProducts, &mut t).await.unwrap();
|
||||
|
||||
testx::db_rollback!(t);
|
||||
assert_eq!(products, vec![p1, p2, p3]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn find() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
let p1 = test_product(&mut t, None, None, None, None, None, None).await;
|
||||
let p2 = test_product(&mut t, None, None, None, None, None, None).await;
|
||||
let p3 = test_product(&mut t, None, None, None, None, None, None).await;
|
||||
|
||||
let product = find_product(FindProduct { product_id: p2.id }, &mut t)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
testx::db_rollback!(t);
|
||||
assert_ne!(product, p1);
|
||||
assert_eq!(product, p2);
|
||||
assert_ne!(product, p3);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
let original = test_product(&mut t, None, None, None, None, None, None).await;
|
||||
let updated = UpdateProduct {
|
||||
id: original.id,
|
||||
name: ProductName::new("a9s0dja0sjd0jas09dj"),
|
||||
short_description: ProductShortDesc::new("ajs9d8ua9sdu9ahsd98has"),
|
||||
long_description: ProductLongDesc::new("hja89sdy9yha9sdy98ayusd9hya9sy8dh"),
|
||||
category: None,
|
||||
price: Price::from_u32(823794),
|
||||
deliver_days_flag: Day::Tuesday | Day::Saturday,
|
||||
}
|
||||
.run(&mut t)
|
||||
.await
|
||||
.unwrap();
|
||||
let reloaded = FindProduct {
|
||||
product_id: original.id,
|
||||
}
|
||||
.run(&mut t)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
testx::db_rollback!(t);
|
||||
assert_ne!(updated, original);
|
||||
assert_eq!(updated, reloaded);
|
||||
assert_eq!(
|
||||
updated,
|
||||
Product {
|
||||
id: original.id,
|
||||
name: ProductName::new("a9s0dja0sjd0jas09dj"),
|
||||
short_description: ProductShortDesc::new("ajs9d8ua9sdu9ahsd98has"),
|
||||
long_description: ProductLongDesc::new("hja89sdy9yha9sdy98ayusd9hya9sy8dh"),
|
||||
category: None,
|
||||
price: Price::from_u32(823794),
|
||||
deliver_days_flag: Day::Tuesday | Day::Saturday,
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -1 +1,338 @@
|
||||
use model::v2::*;
|
||||
|
||||
use crate::db::products::AllProducts;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, serde::Serialize, thiserror::Error)]
|
||||
pub enum Error {
|
||||
#[error("Unable to load all stocks")]
|
||||
All,
|
||||
#[error("Unable to create stock")]
|
||||
Create,
|
||||
#[error("Unable to update stock {0:?}")]
|
||||
Update(StockId),
|
||||
#[error("Unable to delete stock {0:?}")]
|
||||
Delete(StockId),
|
||||
#[error("Unable find stock for product")]
|
||||
ProductVariantStock,
|
||||
#[error("Stock {0:?} does not exists")]
|
||||
NotFound(StockId),
|
||||
}
|
||||
|
||||
pub type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AllStocks {
|
||||
pub limit: i32,
|
||||
pub offset: i32,
|
||||
}
|
||||
|
||||
impl AllStocks {
|
||||
pub async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Vec<Stock>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT id, product_variant_id, quantity, quantity_unit
|
||||
FROM stocks
|
||||
ORDER BY id ASC
|
||||
LIMIT $1 OFFSET $2
|
||||
"#,
|
||||
)
|
||||
.bind(self.limit)
|
||||
.bind(self.offset)
|
||||
.fetch_all(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("{e:?}");
|
||||
Error::All
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct FindStock {
|
||||
pub id: StockId,
|
||||
}
|
||||
|
||||
impl FindStock {
|
||||
pub async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Stock> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
SELECT id, product_variant_id, quantity, quantity_unit
|
||||
FROM stocks
|
||||
WHERE id = $1
|
||||
"#,
|
||||
)
|
||||
.bind(self.id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("{e:?}");
|
||||
dbg!(e);
|
||||
Error::NotFound(self.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct CreateStock {
|
||||
pub product_variant_id: ProductVariantId,
|
||||
pub quantity: Quantity,
|
||||
pub quantity_unit: QuantityUnit,
|
||||
}
|
||||
|
||||
impl CreateStock {
|
||||
pub async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Stock> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
INSERT INTO stocks (product_variant_id, quantity, quantity_unit)
|
||||
VALUES ($1, $2, $3)
|
||||
RETURNING id, product_variant_id, quantity, quantity_unit
|
||||
"#,
|
||||
)
|
||||
.bind(self.product_variant_id)
|
||||
.bind(self.quantity)
|
||||
.bind(self.quantity_unit)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("{e:?}");
|
||||
dbg!(e);
|
||||
Error::Create
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct UpdateStock {
|
||||
pub id: StockId,
|
||||
pub product_id: ProductId,
|
||||
pub quantity: Quantity,
|
||||
pub quantity_unit: QuantityUnit,
|
||||
}
|
||||
|
||||
impl UpdateStock {
|
||||
pub async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Stock> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
UPDATE stocks
|
||||
SET product_variant_id = $1,
|
||||
quantity = $2,
|
||||
quantity_unit = $3
|
||||
WHERE id = $4
|
||||
RETURNING id, product_variant_id, quantity, quantity_unit
|
||||
"#,
|
||||
)
|
||||
.bind(self.product_id)
|
||||
.bind(self.quantity)
|
||||
.bind(self.quantity_unit)
|
||||
.bind(self.id)
|
||||
.fetch_one(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("{e:?}");
|
||||
Error::Update(self.id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct DeleteStock {
|
||||
pub stock_id: StockId,
|
||||
}
|
||||
|
||||
impl DeleteStock {
|
||||
async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Option<Stock>> {
|
||||
sqlx::query_as(
|
||||
r#"
|
||||
DELETE FROM stocks
|
||||
WHERE id = $1
|
||||
RETURNING id, product_variant_id, quantity, quantity_unit
|
||||
"#,
|
||||
)
|
||||
.bind(self.stock_id)
|
||||
.fetch_optional(pool)
|
||||
.await
|
||||
.map_err(|e| {
|
||||
tracing::error!("{e:?}");
|
||||
Error::Delete(self.stock_id)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProductVariantsStock {
|
||||
pub product_variant_ids: Vec<ProductVariantId>,
|
||||
}
|
||||
|
||||
impl ProductVariantsStock {
|
||||
async fn run(self, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Result<Vec<Stock>> {
|
||||
db_utils::MultiLoad::new(
|
||||
pool,
|
||||
r#"
|
||||
SELECT id, product_variant_id, quantity, quantity_unit
|
||||
FROM stocks
|
||||
WHERE
|
||||
"#,
|
||||
" product_variant_id =",
|
||||
)
|
||||
.with_size(200)
|
||||
.load(
|
||||
self.product_variant_ids.len(),
|
||||
self.product_variant_ids.into_iter().map(|id| *id),
|
||||
|_e| Error::ProductVariantStock,
|
||||
)
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use config::UpdateConfig;
|
||||
use fake::faker::lorem::en as lorem;
|
||||
use fake::Fake;
|
||||
use model::*;
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct NoOpts;
|
||||
|
||||
impl UpdateConfig for NoOpts {}
|
||||
|
||||
use super::*;
|
||||
use crate::db::products::*;
|
||||
|
||||
async fn test_product(pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Product {
|
||||
CreateProduct {
|
||||
name: ProductName::new(format!("db stocks test product {}", Uuid::new_v4())),
|
||||
category: None,
|
||||
deliver_days_flag: Days(vec![Day::Friday, Day::Sunday]),
|
||||
}
|
||||
.run(pool)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
async fn test_stock(
|
||||
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
||||
product_variant_id: Option<ProductVariantId>,
|
||||
quantity: Option<Quantity>,
|
||||
quantity_unit: Option<QuantityUnit>,
|
||||
) -> Stock {
|
||||
let product_variant_id = match product_variant_id {
|
||||
Some(id) => id,
|
||||
_ => test_product(&mut *pool).await.id,
|
||||
};
|
||||
let quantity = quantity.unwrap_or_else(|| Quantity::from_u32(345));
|
||||
let quantity_unit = quantity_unit.unwrap_or(QuantityUnit::Piece);
|
||||
|
||||
CreateStock {
|
||||
product_variant_id,
|
||||
quantity_unit,
|
||||
quantity,
|
||||
}
|
||||
.run(&mut *pool)
|
||||
.await
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn create_stock() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
test_stock(&mut t, None, None, None).await;
|
||||
|
||||
testx::db_rollback!(t);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn products_stock() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
let first = test_stock(&mut t, None, None, None).await;
|
||||
let second = test_stock(&mut t, None, None, None).await;
|
||||
|
||||
let stocks: Vec<Stock> = ProductVariantsStock {
|
||||
product_variant_ids: vec![first.product_id, second.product_id],
|
||||
}
|
||||
.run(&mut t)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
testx::db_rollback!(t);
|
||||
assert_eq!(stocks, vec![first, second]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn all_stocks() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
let first = test_stock(&mut t, None, None, None).await;
|
||||
let second = test_stock(&mut t, None, None, None).await;
|
||||
|
||||
let stocks: Vec<Stock> = AllStocks {
|
||||
limit: 200,
|
||||
offset: 0,
|
||||
}
|
||||
.run(&mut t)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
testx::db_rollback!(t);
|
||||
assert_eq!(stocks, vec![first, second]);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn delete_stock() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
let first = test_stock(&mut t, None, None, None).await;
|
||||
let second = test_stock(&mut t, None, None, None).await;
|
||||
|
||||
let deleted: Option<Stock> = DeleteStock {
|
||||
stock_id: second.id,
|
||||
}
|
||||
.run(&mut t)
|
||||
.await
|
||||
.unwrap();
|
||||
let reloaded = super::find_stock(FindStock { id: second.id }, &mut t).await;
|
||||
|
||||
testx::db_rollback!(t);
|
||||
assert_eq!(deleted, Some(second));
|
||||
assert_ne!(deleted, Some(first));
|
||||
assert_eq!(reloaded, Err(crate::Error::Stock(super::Error::NotFound)));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn update_stock() {
|
||||
testx::db_t_ref!(t);
|
||||
|
||||
let first = test_stock(&mut t, None, None, None).await;
|
||||
let second = test_stock(&mut t, None, None, None).await;
|
||||
let another_product = test_product(&mut t).await;
|
||||
|
||||
let updated: Stock = UpdateStock {
|
||||
id: second.id,
|
||||
product_id: another_product.id,
|
||||
quantity: Quantity::from_u32(19191),
|
||||
quantity_unit: QuantityUnit::Gram,
|
||||
}
|
||||
.run(&mut t)
|
||||
.await
|
||||
.unwrap();
|
||||
let reloaded = super::find_stock(FindStock { id: second.id }, &mut t)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
testx::db_rollback!(t);
|
||||
assert_eq!(
|
||||
updated,
|
||||
Stock {
|
||||
id: second.id,
|
||||
product_id: another_product.id,
|
||||
quantity: Quantity::from_u32(19191),
|
||||
quantity_unit: QuantityUnit::Gram,
|
||||
}
|
||||
);
|
||||
assert_ne!(updated, second);
|
||||
assert_ne!(updated, first);
|
||||
assert_eq!(reloaded, updated);
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user