371 lines
9.0 KiB
Rust
371 lines
9.0 KiB
Rust
use actix::Message;
|
|
use model::{ProductId, Quantity, QuantityUnit, Stock, StockId};
|
|
|
|
use crate::{MultiLoad, Result};
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq, 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")]
|
|
Update,
|
|
#[error("Unable to delete stock")]
|
|
Delete,
|
|
#[error("Unable find stock for product")]
|
|
ProductStock,
|
|
#[error("Stock does not exists")]
|
|
NotFound,
|
|
}
|
|
|
|
#[derive(Message)]
|
|
#[rtype(result = "Result<Vec<model::Stock>>")]
|
|
pub struct AllStocks;
|
|
|
|
crate::db_async_handler!(AllStocks, all_stocks, Vec<Stock>, inner_all_stocks);
|
|
|
|
async fn all_stocks(
|
|
_msg: AllStocks,
|
|
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
|
) -> Result<Vec<Stock>> {
|
|
sqlx::query_as(
|
|
r#"
|
|
SELECT id, product_id, quantity, quantity_unit
|
|
FROM stocks
|
|
ORDER BY id ASC
|
|
"#,
|
|
)
|
|
.fetch_all(pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("{e:?}");
|
|
crate::Error::Stock(Error::All)
|
|
})
|
|
}
|
|
|
|
#[derive(Message)]
|
|
#[rtype(result = "Result<model::Stock>")]
|
|
pub struct FindStock {
|
|
pub id: StockId,
|
|
}
|
|
|
|
crate::db_async_handler!(FindStock, find_stock, Stock, inner_find_stock);
|
|
|
|
async fn find_stock(
|
|
msg: FindStock,
|
|
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
|
) -> Result<Stock> {
|
|
sqlx::query_as(
|
|
r#"
|
|
SELECT id, product_id, quantity, quantity_unit
|
|
FROM stocks
|
|
WHERE id = $1
|
|
"#,
|
|
)
|
|
.bind(msg.id)
|
|
.fetch_one(pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("{e:?}");
|
|
dbg!(e);
|
|
crate::Error::Stock(Error::NotFound)
|
|
})
|
|
}
|
|
|
|
#[derive(Message)]
|
|
#[rtype(result = "Result<model::Stock>")]
|
|
pub struct CreateStock {
|
|
pub product_id: ProductId,
|
|
pub quantity: Quantity,
|
|
pub quantity_unit: QuantityUnit,
|
|
}
|
|
|
|
crate::db_async_handler!(CreateStock, create_stock, Stock, inner_create_stock);
|
|
|
|
async fn create_stock(
|
|
msg: CreateStock,
|
|
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
|
) -> Result<Stock> {
|
|
sqlx::query_as(
|
|
r#"
|
|
INSERT INTO stocks (product_id, quantity, quantity_unit)
|
|
VALUES ($1, $2, $3)
|
|
RETURNING id, product_id, quantity, quantity_unit
|
|
"#,
|
|
)
|
|
.bind(msg.product_id)
|
|
.bind(msg.quantity)
|
|
.bind(msg.quantity_unit)
|
|
.fetch_one(pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("{e:?}");
|
|
dbg!(e);
|
|
crate::Error::Stock(Error::Create)
|
|
})
|
|
}
|
|
|
|
#[derive(Message)]
|
|
#[rtype(result = "Result<model::Stock>")]
|
|
pub struct UpdateStock {
|
|
pub id: StockId,
|
|
pub product_id: ProductId,
|
|
pub quantity: Quantity,
|
|
pub quantity_unit: QuantityUnit,
|
|
}
|
|
|
|
crate::db_async_handler!(UpdateStock, update_stock, Stock, inner_update_stock);
|
|
|
|
async fn update_stock(
|
|
msg: UpdateStock,
|
|
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
|
) -> Result<Stock> {
|
|
sqlx::query_as(
|
|
r#"
|
|
UPDATE stocks
|
|
SET product_id = $1,
|
|
quantity = $2,
|
|
quantity_unit = $3
|
|
WHERE id = $4
|
|
RETURNING id, product_id, quantity, quantity_unit
|
|
"#,
|
|
)
|
|
.bind(msg.product_id)
|
|
.bind(msg.quantity)
|
|
.bind(msg.quantity_unit)
|
|
.bind(msg.id)
|
|
.fetch_one(pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("{e:?}");
|
|
crate::Error::Stock(Error::Update)
|
|
})
|
|
}
|
|
|
|
#[derive(Message)]
|
|
#[rtype(result = "Result<Option<model::Stock>>")]
|
|
pub struct DeleteStock {
|
|
pub stock_id: StockId,
|
|
}
|
|
|
|
crate::db_async_handler!(
|
|
DeleteStock,
|
|
delete_stock,
|
|
Option<model::Stock>,
|
|
inner_delete_stock
|
|
);
|
|
|
|
async fn delete_stock(
|
|
msg: DeleteStock,
|
|
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
|
) -> Result<Option<Stock>> {
|
|
sqlx::query_as(
|
|
r#"
|
|
DELETE FROM stocks
|
|
WHERE id = $1
|
|
RETURNING id, product_id, quantity, quantity_unit
|
|
"#,
|
|
)
|
|
.bind(msg.stock_id)
|
|
.fetch_optional(pool)
|
|
.await
|
|
.map_err(|e| {
|
|
tracing::error!("{e:?}");
|
|
crate::Error::Stock(Error::Delete)
|
|
})
|
|
}
|
|
|
|
#[derive(Message)]
|
|
#[rtype(result = "Result<Vec<model::Stock>>")]
|
|
pub struct ProductsStock {
|
|
pub product_ids: Vec<ProductId>,
|
|
}
|
|
|
|
crate::db_async_handler!(
|
|
ProductsStock,
|
|
product_stock,
|
|
Vec<model::Stock>,
|
|
inner_product_stock
|
|
);
|
|
|
|
async fn product_stock(
|
|
msg: ProductsStock,
|
|
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
|
) -> Result<Vec<Stock>> {
|
|
Ok(MultiLoad::new(
|
|
pool,
|
|
r#"
|
|
SELECT id, product_id, quantity, quantity_unit
|
|
FROM stocks
|
|
WHERE
|
|
"#,
|
|
" product_id =",
|
|
)
|
|
.load(
|
|
msg.product_ids.len(),
|
|
msg.product_ids.into_iter().map(|id| *id),
|
|
|_e| crate::Error::Stock(Error::ProductStock),
|
|
)
|
|
.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 crate::*;
|
|
|
|
async fn test_product(pool: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Product {
|
|
create_product(
|
|
CreateProduct {
|
|
name: ProductName::new(format!("db stocks test product {}", Uuid::new_v4())),
|
|
short_description: ProductShortDesc::new(lorem::Paragraph(1..2).fake::<String>()),
|
|
long_description: ProductLongDesc::new(lorem::Paragraph(4..5).fake::<String>()),
|
|
category: None,
|
|
price: Price::from_u32(12321),
|
|
deliver_days_flag: Days(vec![Day::Friday, Day::Sunday]),
|
|
},
|
|
pool,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
async fn test_stock(
|
|
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
|
|
product_id: Option<ProductId>,
|
|
quantity: Option<Quantity>,
|
|
quantity_unit: Option<QuantityUnit>,
|
|
) -> Stock {
|
|
let product_id = match product_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_else(|| QuantityUnit::Piece);
|
|
|
|
super::create_stock(
|
|
CreateStock {
|
|
product_id,
|
|
quantity_unit,
|
|
quantity,
|
|
},
|
|
&mut *pool,
|
|
)
|
|
.await
|
|
.unwrap()
|
|
}
|
|
|
|
#[actix::test]
|
|
async fn create_stock() {
|
|
testx::db_t!(t);
|
|
|
|
test_stock(&mut t, None, None, None).await;
|
|
|
|
testx::db_rollback!(t);
|
|
}
|
|
|
|
#[actix::test]
|
|
async fn products_stock() {
|
|
testx::db_t!(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> = super::product_stock(
|
|
ProductsStock {
|
|
product_ids: vec![first.product_id, second.product_id],
|
|
},
|
|
&mut t,
|
|
)
|
|
.await
|
|
.unwrap();
|
|
|
|
testx::db_rollback!(t);
|
|
assert_eq!(stocks, vec![first, second]);
|
|
}
|
|
|
|
#[actix::test]
|
|
async fn all_stocks() {
|
|
testx::db_t!(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> = super::all_stocks(AllStocks, &mut t).await.unwrap();
|
|
|
|
testx::db_rollback!(t);
|
|
assert_eq!(stocks, vec![first, second]);
|
|
}
|
|
|
|
#[actix::test]
|
|
async fn delete_stock() {
|
|
testx::db_t!(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> = super::delete_stock(
|
|
DeleteStock {
|
|
stock_id: second.id,
|
|
},
|
|
&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)));
|
|
}
|
|
|
|
#[actix::test]
|
|
async fn update_stock() {
|
|
testx::db_t!(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 = super::update_stock(
|
|
UpdateStock {
|
|
id: second.id,
|
|
product_id: another_product.id,
|
|
quantity: Quantity::from_u32(19191),
|
|
quantity_unit: QuantityUnit::Gram,
|
|
},
|
|
&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);
|
|
}
|
|
}
|