diff --git a/api/src/actors/database/account_orders.rs b/api/src/actors/database/account_orders.rs index b90139e..5ebbdf1 100644 --- a/api/src/actors/database/account_orders.rs +++ b/api/src/actors/database/account_orders.rs @@ -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")] pub struct CreateAccountOrder { pub buyer_id: AccountId, - pub status: OrderStatus, - pub order_id: Option, + pub items: Vec, + 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 { - 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:?}"); + 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:?}"); - super::Error::AccountOrder(Error::CantCreate) - }) + + t.rollback().await.ok(); + return Err(super::Error::AccountOrder(Error::CantCreate)); + }; + + t.commit().await.ok(); + + Ok(order) } #[derive(actix::Message)] diff --git a/api/src/actors/database/order_items.rs b/api/src/actors/database/order_items.rs index 3f97bcd..ecf4339 100644 --- a/api/src/actors/database/order_items.rs +++ b/api/src/actors/database/order_items.rs @@ -26,7 +26,7 @@ db_async_handler!(AllOrderItems, all_order_items, Vec); pub(crate) async fn all_order_items(_msg: AllOrderItems, pool: PgPool) -> Result> { 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")] 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 { +async fn inner_create_order_item(msg: CreateOrderItem, db: PgPool) -> Result { + create_order_item(msg, &db).await +} + +pub(crate) async fn create_order_item<'e, E>(msg: CreateOrderItem, db: E) -> Result +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 { sqlx::query_as( r#" -SELECT id, buyer_id, status +SELECT id, product_id, order_id, quantity, quantity_unit FROM order_items WHERE id = $1 "#, diff --git a/api/src/actors/database/shopping_cart_items.rs b/api/src/actors/database/shopping_cart_items.rs index 8113a15..672689f 100644 --- a/api/src/actors/database/shopping_cart_items.rs +++ b/api/src/actors/database/shopping_cart_items.rs @@ -60,6 +60,7 @@ FROM shopping_cart_items #[rtype(result = "Result>")] pub struct AccountShoppingCartItems { pub account_id: AccountId, + pub shopping_cart_id: Option, } db_async_handler!( @@ -72,8 +73,24 @@ pub(crate) async fn account_shopping_cart_items( msg: AccountShoppingCartItems, pool: PgPool, ) -> Result> { - sqlx::query_as( - r#" + 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, shopping_cart_items.shopping_cart_id as shopping_cart_id, @@ -84,8 +101,9 @@ LEFT JOIN shopping_carts ON shopping_carts.id = shopping_cart_id WHERE shopping_carts.buyer_id = $1 "#, - ) - .bind(msg.account_id) + ) + .bind(msg.account_id), + } .fetch_all(&pool) .await .map_err(|e| { diff --git a/api/src/actors/database/shopping_carts.rs b/api/src/actors/database/shopping_carts.rs index e541418..b2d2f92 100644 --- a/api/src/actors/database/shopping_carts.rs +++ b/api/src/actors/database/shopping_carts.rs @@ -153,6 +153,51 @@ RETURNING id, buyer_id, payment_method, state }) } +#[derive(actix::Message)] +#[rtype(result = "Result")] +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 { + shopping_cart_set_state(msg, &pool).await +} + +pub(crate) async fn shopping_cart_set_state<'e, E>( + msg: ShoppingCartSetState, + pool: E, +) -> Result +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")] pub struct FindShoppingCart { diff --git a/api/src/actors/order_manager.rs b/api/src/actors/order_manager.rs index bae3a1c..e081fdc 100644 --- a/api/src/actors/order_manager.rs +++ b/api/src/actors/order_manager.rs @@ -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 = std::result::Result; @@ -42,11 +49,73 @@ impl OrderManager { #[derive(Message, Debug)] #[rtype(result = "Result")] 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 { - 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 = 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 { diff --git a/api/src/logic/order_state.rs b/api/src/logic/order_state.rs index 8093113..8b13789 100644 --- a/api/src/logic/order_state.rs +++ b/api/src/logic/order_state.rs @@ -1 +1 @@ -use crate::model::OrderStatus; + diff --git a/api/src/main.rs b/api/src/main.rs index 4f60014..c3b047d 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -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)) }) diff --git a/api/src/model.rs b/api/src/model.rs index 2f7f022..5a959c4 100644 --- a/api/src/model.rs +++ b/api/src/model.rs @@ -390,7 +390,7 @@ impl PartialEq 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, } -#[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); diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs index d6b6333..8251ce7 100644 --- a/api/src/routes/mod.rs +++ b/api/src/routes/mod.rs @@ -104,14 +104,14 @@ impl Responder for Error { msg: format!("{}", self), }), }, - Error::Public(PublicError::ApiV1(V1Error::AddItem | V1Error::RemoveItem)) => { - HttpResponse::BadRequest() - .content_type("application/json") - .json(ReqFailure { - success: false, - msg: format!("{}", self), - }) - } + Error::Public(PublicError::ApiV1( + V1Error::AddItem | V1Error::RemoveItem | V1Error::AddOrder, + )) => HttpResponse::BadRequest() + .content_type("application/json") + .json(ReqFailure { + success: false, + msg: format!("{}", self), + }), } } } diff --git a/api/src/routes/public/api_v1.rs b/api/src/routes/public/api_v1.rs index 640cbad..0e1c990 100644 --- a/api/src/routes/public/api_v1.rs +++ b/api/src/routes/public/api_v1.rs @@ -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) { diff --git a/api/src/routes/public/api_v1/restricted.rs b/api/src/routes/public/api_v1/restricted.rs index 60c6833..177162c 100644 --- a/api/src/routes/public/api_v1/restricted.rs +++ b/api/src/routes/public/api_v1/restricted.rs @@ -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, + tm: Data>, + credentials: BearerAuth, + om: Data>, +) -> routes::Result { + 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)); }