Add create order

This commit is contained in:
Adrian Woźniak 2022-04-20 16:09:09 +02:00
parent e7446e7df2
commit 75a68a317c
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
11 changed files with 294 additions and 55 deletions

View File

@ -1,7 +1,9 @@
use sqlx::PgPool;
use super::Result;
use crate::database::Database;
use crate::database::{
create_order_item, shopping_cart_set_state, CreateOrderItem, Database, ShoppingCartSetState,
};
use crate::db_async_handler;
use crate::model::*;
@ -41,12 +43,22 @@ FROM account_orders
})
}
pub mod create_order {
use crate::model::{ProductId, Quantity, QuantityUnit};
pub struct OrderItem {
pub product_id: ProductId,
pub quantity: Quantity,
pub quantity_unit: QuantityUnit,
}
}
#[derive(actix::Message)]
#[rtype(result = "Result<AccountOrder>")]
pub struct CreateAccountOrder {
pub buyer_id: AccountId,
pub status: OrderStatus,
pub order_id: Option<OrderId>,
pub items: Vec<create_order::OrderItem>,
pub shopping_cart_id: ShoppingCartId,
}
db_async_handler!(CreateAccountOrder, create_account_order, AccountOrder);
@ -55,22 +67,64 @@ pub(crate) async fn create_account_order(
msg: CreateAccountOrder,
db: PgPool,
) -> Result<AccountOrder> {
sqlx::query_as(
let mut t = db.begin().await?;
let order: AccountOrder = match sqlx::query_as(
r#"
INSERT INTO account_orders (buyer_id, status, order_id)
INSERT INTO account_orders (buyer_id, status)
VALUES ($1, $2, $3)
RETURNING id, buyer_id, status, order_id
RETURNING id, buyer_id, status
"#,
)
.bind(msg.buyer_id)
.bind(msg.status)
.bind(msg.order_id)
.fetch_one(&db)
.bind(OrderStatus::Confirmed)
.fetch_one(&mut t)
.await
.map_err(|e| {
{
Ok(order) => order,
Err(e) => {
log::error!("{e:?}");
super::Error::AccountOrder(Error::CantCreate)
})
t.rollback().await.ok();
return Err(super::Error::AccountOrder(Error::CantCreate));
}
};
for item in msg.items {
if let Err(e) = create_order_item(
CreateOrderItem {
product_id: item.product_id,
order_id: order.id,
quantity: item.quantity,
quantity_unit: item.quantity_unit,
},
&mut t,
)
.await
{
log::error!("{e:?}");
t.rollback().await.ok();
return Err(super::Error::AccountOrder(Error::CantCreate));
}
}
if let Err(e) = shopping_cart_set_state(
ShoppingCartSetState {
id: msg.shopping_cart_id,
state: ShoppingCartState::Closed,
},
&mut t,
)
.await
{
log::error!("{e:?}");
t.rollback().await.ok();
return Err(super::Error::AccountOrder(Error::CantCreate));
};
t.commit().await.ok();
Ok(order)
}
#[derive(actix::Message)]

View File

@ -26,7 +26,7 @@ db_async_handler!(AllOrderItems, all_order_items, Vec<OrderItem>);
pub(crate) async fn all_order_items(_msg: AllOrderItems, pool: PgPool) -> Result<Vec<OrderItem>> {
sqlx::query_as(
r#"
SELECT id, buyer_id, status
SELECT id, product_id, order_id, quantity, quantity_unit
FROM order_items
"#,
)
@ -41,23 +41,34 @@ FROM order_items
#[derive(actix::Message)]
#[rtype(result = "Result<OrderItem>")]
pub struct CreateOrderItem {
pub buyer_id: AccountId,
pub status: OrderStatus,
pub product_id: ProductId,
pub order_id: AccountOrderId,
pub quantity: Quantity,
pub quantity_unit: QuantityUnit,
}
db_async_handler!(CreateOrderItem, create_order_item, OrderItem);
db_async_handler!(CreateOrderItem, inner_create_order_item, OrderItem);
pub(crate) async fn create_order_item(msg: CreateOrderItem, db: PgPool) -> Result<OrderItem> {
async fn inner_create_order_item(msg: CreateOrderItem, db: PgPool) -> Result<OrderItem> {
create_order_item(msg, &db).await
}
pub(crate) async fn create_order_item<'e, E>(msg: CreateOrderItem, db: E) -> Result<OrderItem>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
sqlx::query_as(
r#"
INSERT INTO order_items (buyer_id, status)
VALUES ($1, $2)
RETURNING id, buyer_id, status
INSERT INTO order_items (product_id, order_id, quantity, quantity_unit)
VALUES ($1, $2, $3, $4)
RETURNING id, product_id, order_id, quantity, quantity_unit
"#,
)
.bind(msg.buyer_id)
.bind(msg.status)
.fetch_one(&db)
.bind(msg.product_id)
.bind(msg.order_id)
.bind(msg.quantity)
.bind(msg.quantity_unit)
.fetch_one(db)
.await
.map_err(|e| {
log::error!("{e:?}");
@ -76,7 +87,7 @@ db_async_handler!(FindOrderItem, find_order_item, OrderItem);
pub(crate) async fn find_order_item(msg: FindOrderItem, db: PgPool) -> Result<OrderItem> {
sqlx::query_as(
r#"
SELECT id, buyer_id, status
SELECT id, product_id, order_id, quantity, quantity_unit
FROM order_items
WHERE id = $1
"#,

View File

@ -60,6 +60,7 @@ FROM shopping_cart_items
#[rtype(result = "Result<Vec<ShoppingCartItem>>")]
pub struct AccountShoppingCartItems {
pub account_id: AccountId,
pub shopping_cart_id: Option<ShoppingCartId>,
}
db_async_handler!(
@ -72,7 +73,23 @@ pub(crate) async fn account_shopping_cart_items(
msg: AccountShoppingCartItems,
pool: PgPool,
) -> Result<Vec<ShoppingCartItem>> {
sqlx::query_as(
match msg.shopping_cart_id {
Some(shopping_cart_id) => sqlx::query_as(
r#"
SELECT shopping_cart_items.id as id,
shopping_cart_items.product_id as product_id,
shopping_cart_items.shopping_cart_id as shopping_cart_id,
shopping_cart_items.quantity as quantity,
shopping_cart_items.quantity_unit as quantity_unit
FROM shopping_cart_items
LEFT JOIN shopping_carts
ON shopping_carts.id = shopping_cart_id
WHERE shopping_carts.buyer_id = $1 AND shopping_carts.id = $2
"#,
)
.bind(msg.account_id)
.bind(shopping_cart_id),
None => sqlx::query_as(
r#"
SELECT shopping_cart_items.id as id,
shopping_cart_items.product_id as product_id,
@ -85,7 +102,8 @@ LEFT JOIN shopping_carts
WHERE shopping_carts.buyer_id = $1
"#,
)
.bind(msg.account_id)
.bind(msg.account_id),
}
.fetch_all(&pool)
.await
.map_err(|e| {

View File

@ -153,6 +153,51 @@ RETURNING id, buyer_id, payment_method, state
})
}
#[derive(actix::Message)]
#[rtype(result = "Result<ShoppingCart>")]
pub struct ShoppingCartSetState {
pub id: ShoppingCartId,
pub state: ShoppingCartState,
}
db_async_handler!(
ShoppingCartSetState,
inner_shopping_cart_set_state,
ShoppingCart
);
async fn inner_shopping_cart_set_state(
msg: ShoppingCartSetState,
pool: PgPool,
) -> Result<ShoppingCart> {
shopping_cart_set_state(msg, &pool).await
}
pub(crate) async fn shopping_cart_set_state<'e, E>(
msg: ShoppingCartSetState,
pool: E,
) -> Result<ShoppingCart>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
sqlx::query_as(
r#"
UPDATE shopping_carts
SET state = $2
WHERE id = $1
RETURNING id, buyer_id, payment_method, state
"#,
)
.bind(msg.id)
.bind(msg.state)
.fetch_one(pool)
.await
.map_err(|e| {
log::error!("{e:?}");
super::Error::ShoppingCart(Error::CantUpdate(msg.id))
})
}
#[derive(actix::Message)]
#[rtype(result = "Result<ShoppingCart>")]
pub struct FindShoppingCart {

View File

@ -1,9 +1,9 @@
use actix::Addr;
use actix_web::Message;
use sqlx_core::postgres::PgPool;
use actix::Message;
use crate::database::{Database, SharedDatabase, self};
use crate::model::{AccountOrder, OrderStatus, ShoppingCartId};
use crate::database::{self, SharedDatabase};
use crate::model::{
AccountId, AccountOrder, OrderStatus, ShoppingCart, ShoppingCartId, ShoppingCartItem,
};
#[macro_export]
macro_rules! order_async_handler {
@ -21,7 +21,14 @@ macro_rules! order_async_handler {
}
#[derive(Debug, thiserror::Error)]
pub enum Error {}
pub enum Error {
#[error("Database actor failed")]
DatabaseInternal,
#[error("Shopping cart does not exists")]
ShoppingCart,
#[error("Failed to create account order")]
CreateAccountOrder,
}
pub type Result<T> = std::result::Result<T, Error>;
@ -42,11 +49,73 @@ impl OrderManager {
#[derive(Message, Debug)]
#[rtype(result = "Result<AccountOrder>")]
pub struct CreateOrder {
pub account_id: AccountId,
pub shopping_cart_id: ShoppingCartId,
}
order_async_handler!(CreateOrder, create_order, AccountOrder);
pub(crate) async fn create_order(msg: CreateOrder, db: SharedDatabase) -> Result<AccountOrder> {
let cart = match db.send(database)
let cart: ShoppingCart = match db
.send(database::FindShoppingCart {
id: msg.shopping_cart_id,
})
.await
{
Ok(Ok(cart)) => cart,
Ok(Err(e)) => {
log::error!("{e}");
return Err(Error::ShoppingCart);
}
Err(e) => {
log::error!("{e:?}");
return Err(Error::DatabaseInternal);
}
};
let items: Vec<ShoppingCartItem> = match db
.send(database::AccountShoppingCartItems {
account_id: cart.buyer_id,
shopping_cart_id: Some(cart.id),
})
.await
{
Ok(Ok(items)) => items,
Ok(Err(e)) => {
log::error!("{e}");
return Err(Error::ShoppingCart);
}
Err(e) => {
log::error!("{e:?}");
return Err(Error::DatabaseInternal);
}
};
let order = match db
.send(database::CreateAccountOrder {
shopping_cart_id: cart.id,
buyer_id: msg.account_id,
items: items
.into_iter()
.map(|item| database::create_order::OrderItem {
product_id: item.product_id,
quantity: item.quantity,
quantity_unit: item.quantity_unit,
})
.collect(),
})
.await
{
Ok(Ok(order)) => order,
Ok(Err(e)) => {
log::error!("{e}");
return Err(Error::CreateAccountOrder);
}
Err(e) => {
log::error!("{e:?}");
return Err(Error::DatabaseInternal);
}
};
Ok(order)
}
pub fn change(current: OrderStatus, next: OrderStatus) -> Option<OrderStatus> {

View File

@ -1 +1 @@
use crate::model::OrderStatus;

View File

@ -13,7 +13,7 @@ use jemallocator::Jemalloc;
use password_hash::SaltString;
use validator::{validate_email, validate_length};
use crate::actors::{database, token_manager};
use crate::actors::{database, order_manager, token_manager};
use crate::logic::encrypt_password;
use crate::model::{Email, Login, PassHash, Password, Role};
@ -172,6 +172,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
let config = Arc::new(Config::load());
let db = database::Database::build(&opts.db_url()).await?.start();
let token_manager = token_manager::TokenManager::new(db.clone()).start();
let order_manager = order_manager::OrderManager::new(db.clone()).start();
HttpServer::new(move || {
App::new()
@ -185,6 +186,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
.app_data(Data::new(config.clone()))
.app_data(Data::new(db.clone()))
.app_data(Data::new(token_manager.clone()))
.app_data(Data::new(order_manager.clone()))
.configure(routes::configure)
// .default_service(web::to(HttpResponse::Ok))
})

View File

@ -390,7 +390,7 @@ impl PartialEq<PasswordConfirmation> for Password {
}
}
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Deref, Display, From)]
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Debug, Deref, Display, From)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct AccountId(RecordId);
@ -488,7 +488,7 @@ pub struct Stock {
pub quantity_unit: QuantityUnit,
}
#[derive(sqlx::Type, Serialize, Deserialize, Deref)]
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Debug, Deref)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct AccountOrderId(RecordId);
@ -506,7 +506,7 @@ pub struct AccountOrder {
pub order_id: Option<OrderId>,
}
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Deref)]
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Debug, Deref)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct OrderItemId(pub RecordId);
@ -515,12 +515,12 @@ pub struct OrderItemId(pub RecordId);
pub struct OrderItem {
pub id: OrderItemId,
pub product_id: ProductId,
pub order_id: OrderItemId,
pub order_id: AccountOrderId,
pub quantity: Quantity,
pub quantity_unit: QuantityUnit,
}
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Deref, Display, Debug)]
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Debug, Deref, Display)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct ShoppingCartId(pub RecordId);
@ -533,7 +533,7 @@ pub struct ShoppingCart {
pub state: ShoppingCartState,
}
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Deref, Display, Debug)]
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Debug, Deref, Display)]
#[sqlx(transparent)]
#[serde(transparent)]
pub struct ShoppingCartItemId(RecordId);

View File

@ -104,14 +104,14 @@ impl Responder for Error {
msg: format!("{}", self),
}),
},
Error::Public(PublicError::ApiV1(V1Error::AddItem | V1Error::RemoveItem)) => {
HttpResponse::BadRequest()
Error::Public(PublicError::ApiV1(
V1Error::AddItem | V1Error::RemoveItem | V1Error::AddOrder,
)) => HttpResponse::BadRequest()
.content_type("application/json")
.json(ReqFailure {
success: false,
msg: format!("{}", self),
})
}
}),
}
}
}

View File

@ -18,6 +18,9 @@ pub enum Error {
RemoveItem,
#[error("Failed to add shopping cart item")]
AddItem,
#[error("Failed to create order")]
AddOrder,
}
pub(crate) fn configure(config: &mut ServiceConfig) {

View File

@ -7,14 +7,15 @@ use crate::actors::cart_manager;
use crate::actors::cart_manager::CartManager;
use crate::database::Database;
use crate::model::{
AccountId, ProductId, Quantity, QuantityUnit, ShoppingCart, ShoppingCartItem,
AccountId, ProductId, Quantity, QuantityUnit, ShoppingCart, ShoppingCartId, ShoppingCartItem,
ShoppingCartItemId,
};
use crate::order_manager::OrderManager;
use crate::routes::public::api_v1::ShoppingCartError;
use crate::routes::public::Error as PublicError;
use crate::routes::{RequireUser, Result};
use crate::token_manager::TokenManager;
use crate::{database, routes};
use crate::{database, order_manager, routes};
#[get("/shopping-cart")]
async fn shopping_cart(
@ -68,6 +69,7 @@ async fn shopping_cart_items(
match db
.send(database::AccountShoppingCartItems {
account_id: cart.buyer_id,
shopping_cart_id: Some(cart.id),
})
.await
{
@ -186,7 +188,41 @@ async fn delete_cart_item(
}
}
pub(crate) async fn create_order() {}
#[derive(serde::Deserialize)]
pub struct CreateOrderInput {
pub shopping_cart_id: ShoppingCartId,
}
#[post("/order")]
pub(crate) async fn create_order(
Json(payload): Json<CreateOrderInput>,
tm: Data<Addr<TokenManager>>,
credentials: BearerAuth,
om: Data<Addr<OrderManager>>,
) -> routes::Result<HttpResponse> {
let (token, _) = credentials.require_user(tm.into_inner()).await?;
let order = match om
.send(order_manager::CreateOrder {
account_id: AccountId::from(token.subject),
shopping_cart_id: payload.shopping_cart_id,
})
.await
{
Ok(Ok(order)) => order,
Ok(Err(e)) => {
log::error!("{e}");
return Err(routes::Error::Public(PublicError::ApiV1(
super::Error::AddOrder,
)));
}
Err(e) => {
log::error!("{e}");
return Err(routes::Error::Public(PublicError::DatabaseConnection));
}
};
Ok(HttpResponse::Created().json(order))
}
pub(crate) fn configure(config: &mut ServiceConfig) {
config.service(scope("")
@ -195,5 +231,6 @@ pub(crate) fn configure(config: &mut ServiceConfig) {
.scope("customer_id role subject audience expiration_time not_before_time issued_at_time"))
.service(shopping_cart)
.service(shopping_cart_items)
.service(delete_cart_item));
.service(delete_cart_item)
.service(create_order));
}