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 sqlx::PgPool;
use super::Result; 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::db_async_handler;
use crate::model::*; 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)] #[derive(actix::Message)]
#[rtype(result = "Result<AccountOrder>")] #[rtype(result = "Result<AccountOrder>")]
pub struct CreateAccountOrder { pub struct CreateAccountOrder {
pub buyer_id: AccountId, pub buyer_id: AccountId,
pub status: OrderStatus, pub items: Vec<create_order::OrderItem>,
pub order_id: Option<OrderId>, pub shopping_cart_id: ShoppingCartId,
} }
db_async_handler!(CreateAccountOrder, create_account_order, AccountOrder); db_async_handler!(CreateAccountOrder, create_account_order, AccountOrder);
@ -55,22 +67,64 @@ pub(crate) async fn create_account_order(
msg: CreateAccountOrder, msg: CreateAccountOrder,
db: PgPool, db: PgPool,
) -> Result<AccountOrder> { ) -> Result<AccountOrder> {
sqlx::query_as( let mut t = db.begin().await?;
let order: AccountOrder = match sqlx::query_as(
r#" r#"
INSERT INTO account_orders (buyer_id, status, order_id) INSERT INTO account_orders (buyer_id, status)
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
RETURNING id, buyer_id, status, order_id RETURNING id, buyer_id, status
"#, "#,
) )
.bind(msg.buyer_id) .bind(msg.buyer_id)
.bind(msg.status) .bind(OrderStatus::Confirmed)
.bind(msg.order_id) .fetch_one(&mut t)
.fetch_one(&db)
.await .await
.map_err(|e| { {
Ok(order) => order,
Err(e) => {
log::error!("{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)] #[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>> { pub(crate) async fn all_order_items(_msg: AllOrderItems, pool: PgPool) -> Result<Vec<OrderItem>> {
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT id, buyer_id, status SELECT id, product_id, order_id, quantity, quantity_unit
FROM order_items FROM order_items
"#, "#,
) )
@ -41,23 +41,34 @@ FROM order_items
#[derive(actix::Message)] #[derive(actix::Message)]
#[rtype(result = "Result<OrderItem>")] #[rtype(result = "Result<OrderItem>")]
pub struct CreateOrderItem { pub struct CreateOrderItem {
pub buyer_id: AccountId, pub product_id: ProductId,
pub status: OrderStatus, 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( sqlx::query_as(
r#" r#"
INSERT INTO order_items (buyer_id, status) INSERT INTO order_items (product_id, order_id, quantity, quantity_unit)
VALUES ($1, $2) VALUES ($1, $2, $3, $4)
RETURNING id, buyer_id, status RETURNING id, product_id, order_id, quantity, quantity_unit
"#, "#,
) )
.bind(msg.buyer_id) .bind(msg.product_id)
.bind(msg.status) .bind(msg.order_id)
.fetch_one(&db) .bind(msg.quantity)
.bind(msg.quantity_unit)
.fetch_one(db)
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{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> { pub(crate) async fn find_order_item(msg: FindOrderItem, db: PgPool) -> Result<OrderItem> {
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT id, buyer_id, status SELECT id, product_id, order_id, quantity, quantity_unit
FROM order_items FROM order_items
WHERE id = $1 WHERE id = $1
"#, "#,

View File

@ -60,6 +60,7 @@ FROM shopping_cart_items
#[rtype(result = "Result<Vec<ShoppingCartItem>>")] #[rtype(result = "Result<Vec<ShoppingCartItem>>")]
pub struct AccountShoppingCartItems { pub struct AccountShoppingCartItems {
pub account_id: AccountId, pub account_id: AccountId,
pub shopping_cart_id: Option<ShoppingCartId>,
} }
db_async_handler!( db_async_handler!(
@ -72,7 +73,23 @@ pub(crate) async fn account_shopping_cart_items(
msg: AccountShoppingCartItems, msg: AccountShoppingCartItems,
pool: PgPool, pool: PgPool,
) -> Result<Vec<ShoppingCartItem>> { ) -> 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#" r#"
SELECT shopping_cart_items.id as id, SELECT shopping_cart_items.id as id,
shopping_cart_items.product_id as product_id, shopping_cart_items.product_id as product_id,
@ -85,7 +102,8 @@ LEFT JOIN shopping_carts
WHERE shopping_carts.buyer_id = $1 WHERE shopping_carts.buyer_id = $1
"#, "#,
) )
.bind(msg.account_id) .bind(msg.account_id),
}
.fetch_all(&pool) .fetch_all(&pool)
.await .await
.map_err(|e| { .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)] #[derive(actix::Message)]
#[rtype(result = "Result<ShoppingCart>")] #[rtype(result = "Result<ShoppingCart>")]
pub struct FindShoppingCart { pub struct FindShoppingCart {

View File

@ -1,9 +1,9 @@
use actix::Addr; use actix::Message;
use actix_web::Message;
use sqlx_core::postgres::PgPool;
use crate::database::{Database, SharedDatabase, self}; use crate::database::{self, SharedDatabase};
use crate::model::{AccountOrder, OrderStatus, ShoppingCartId}; use crate::model::{
AccountId, AccountOrder, OrderStatus, ShoppingCart, ShoppingCartId, ShoppingCartItem,
};
#[macro_export] #[macro_export]
macro_rules! order_async_handler { macro_rules! order_async_handler {
@ -21,7 +21,14 @@ macro_rules! order_async_handler {
} }
#[derive(Debug, thiserror::Error)] #[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>; pub type Result<T> = std::result::Result<T, Error>;
@ -42,11 +49,73 @@ impl OrderManager {
#[derive(Message, Debug)] #[derive(Message, Debug)]
#[rtype(result = "Result<AccountOrder>")] #[rtype(result = "Result<AccountOrder>")]
pub struct CreateOrder { pub struct CreateOrder {
pub account_id: AccountId,
pub shopping_cart_id: ShoppingCartId, pub shopping_cart_id: ShoppingCartId,
} }
order_async_handler!(CreateOrder, create_order, AccountOrder);
pub(crate) async fn create_order(msg: CreateOrder, db: SharedDatabase) -> Result<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> { 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 password_hash::SaltString;
use validator::{validate_email, validate_length}; 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::logic::encrypt_password;
use crate::model::{Email, Login, PassHash, Password, Role}; 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 config = Arc::new(Config::load());
let db = database::Database::build(&opts.db_url()).await?.start(); let db = database::Database::build(&opts.db_url()).await?.start();
let token_manager = token_manager::TokenManager::new(db.clone()).start(); let token_manager = token_manager::TokenManager::new(db.clone()).start();
let order_manager = order_manager::OrderManager::new(db.clone()).start();
HttpServer::new(move || { HttpServer::new(move || {
App::new() App::new()
@ -185,6 +186,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
.app_data(Data::new(config.clone())) .app_data(Data::new(config.clone()))
.app_data(Data::new(db.clone())) .app_data(Data::new(db.clone()))
.app_data(Data::new(token_manager.clone())) .app_data(Data::new(token_manager.clone()))
.app_data(Data::new(order_manager.clone()))
.configure(routes::configure) .configure(routes::configure)
// .default_service(web::to(HttpResponse::Ok)) // .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)] #[sqlx(transparent)]
#[serde(transparent)] #[serde(transparent)]
pub struct AccountId(RecordId); pub struct AccountId(RecordId);
@ -488,7 +488,7 @@ pub struct Stock {
pub quantity_unit: QuantityUnit, pub quantity_unit: QuantityUnit,
} }
#[derive(sqlx::Type, Serialize, Deserialize, Deref)] #[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Debug, Deref)]
#[sqlx(transparent)] #[sqlx(transparent)]
#[serde(transparent)] #[serde(transparent)]
pub struct AccountOrderId(RecordId); pub struct AccountOrderId(RecordId);
@ -506,7 +506,7 @@ pub struct AccountOrder {
pub order_id: Option<OrderId>, pub order_id: Option<OrderId>,
} }
#[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Deref)] #[derive(sqlx::Type, Serialize, Deserialize, Copy, Clone, Debug, Deref)]
#[sqlx(transparent)] #[sqlx(transparent)]
#[serde(transparent)] #[serde(transparent)]
pub struct OrderItemId(pub RecordId); pub struct OrderItemId(pub RecordId);
@ -515,12 +515,12 @@ pub struct OrderItemId(pub RecordId);
pub struct OrderItem { pub struct OrderItem {
pub id: OrderItemId, pub id: OrderItemId,
pub product_id: ProductId, pub product_id: ProductId,
pub order_id: OrderItemId, pub order_id: AccountOrderId,
pub quantity: Quantity, pub quantity: Quantity,
pub quantity_unit: QuantityUnit, 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)] #[sqlx(transparent)]
#[serde(transparent)] #[serde(transparent)]
pub struct ShoppingCartId(pub RecordId); pub struct ShoppingCartId(pub RecordId);
@ -533,7 +533,7 @@ pub struct ShoppingCart {
pub state: ShoppingCartState, 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)] #[sqlx(transparent)]
#[serde(transparent)] #[serde(transparent)]
pub struct ShoppingCartItemId(RecordId); pub struct ShoppingCartItemId(RecordId);

View File

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

View File

@ -18,6 +18,9 @@ pub enum Error {
RemoveItem, RemoveItem,
#[error("Failed to add shopping cart item")] #[error("Failed to add shopping cart item")]
AddItem, AddItem,
#[error("Failed to create order")]
AddOrder,
} }
pub(crate) fn configure(config: &mut ServiceConfig) { 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::actors::cart_manager::CartManager;
use crate::database::Database; use crate::database::Database;
use crate::model::{ use crate::model::{
AccountId, ProductId, Quantity, QuantityUnit, ShoppingCart, ShoppingCartItem, AccountId, ProductId, Quantity, QuantityUnit, ShoppingCart, ShoppingCartId, ShoppingCartItem,
ShoppingCartItemId, ShoppingCartItemId,
}; };
use crate::order_manager::OrderManager;
use crate::routes::public::api_v1::ShoppingCartError; use crate::routes::public::api_v1::ShoppingCartError;
use crate::routes::public::Error as PublicError; use crate::routes::public::Error as PublicError;
use crate::routes::{RequireUser, Result}; use crate::routes::{RequireUser, Result};
use crate::token_manager::TokenManager; use crate::token_manager::TokenManager;
use crate::{database, routes}; use crate::{database, order_manager, routes};
#[get("/shopping-cart")] #[get("/shopping-cart")]
async fn shopping_cart( async fn shopping_cart(
@ -68,6 +69,7 @@ async fn shopping_cart_items(
match db match db
.send(database::AccountShoppingCartItems { .send(database::AccountShoppingCartItems {
account_id: cart.buyer_id, account_id: cart.buyer_id,
shopping_cart_id: Some(cart.id),
}) })
.await .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) { pub(crate) fn configure(config: &mut ServiceConfig) {
config.service(scope("") 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")) .scope("customer_id role subject audience expiration_time not_before_time issued_at_time"))
.service(shopping_cart) .service(shopping_cart)
.service(shopping_cart_items) .service(shopping_cart_items)
.service(delete_cart_item)); .service(delete_cart_item)
.service(create_order));
} }