use model::*; use super::Result; use crate::{ create_order_item, db_async_handler, shopping_cart_set_state, CreateOrderItem, ShoppingCartSetState, }; #[derive(Debug, Copy, Clone, PartialEq, serde::Serialize, thiserror::Error)] #[serde(rename_all = "kebab-case", tag = "account-order")] pub enum Error { #[error("Can't create account order")] CantCreate, #[error("Can't find account order does to lack of identity")] NoIdentity, #[error("Account order does not exists")] NotExists, #[error("Failed to load all account orders")] All, } #[derive(actix::Message)] #[rtype(result = "Result>")] pub struct AllOrders; db_async_handler!(AllOrders, all_orders, Vec, inner_all_orders); pub(crate) async fn all_orders( _msg: AllOrders, t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result> { sqlx::query_as( r#" SELECT id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id FROM orders ORDER BY id DESC "#, ) .fetch_all(t) .await .map_err(|e| { tracing::error!("{e:?}"); super::Error::AccountOrder(Error::All) }) } pub mod create_order { use 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 CreateOrder { pub buyer_id: AccountId, pub items: Vec, pub shopping_cart_id: Option, pub checkout_notes: Option, pub delivery_address_id: OrderAddressId, } db_async_handler!(CreateOrder, create_order, Order, inner_create_order); pub(crate) async fn create_order( msg: CreateOrder, t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { let order: Order = match sqlx::query_as( r#" INSERT INTO orders (buyer_id, status, checkout_notes, address_id) VALUES ($1, $2, $3, $4) RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id "#, ) .bind(msg.buyer_id) .bind(OrderStatus::Confirmed) .bind(msg.checkout_notes.as_deref()) .bind(msg.delivery_address_id) .fetch_one(&mut *t) .await { Ok(order) => order, Err(e) => { tracing::error!("{e:?}"); dbg!(e); 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 { dbg!(e); tracing::error!("{e:?}"); return Err(super::Error::AccountOrder(Error::CantCreate)); } } if let Some(shopping_cart_id) = msg.shopping_cart_id { if let Err(e) = shopping_cart_set_state( ShoppingCartSetState { id: shopping_cart_id, state: ShoppingCartState::Closed, checkout_notes: msg.checkout_notes, }, t, ) .await { dbg!(e); tracing::error!("{e:?}"); return Err(super::Error::AccountOrder(Error::CantCreate)); }; } Ok(order) } #[derive(actix::Message)] #[rtype(result = "Result")] pub struct UpdateOrder { pub id: OrderId, pub buyer_id: AccountId, pub status: OrderStatus, pub order_id: Option, } db_async_handler!(UpdateOrder, update_order, Order, inner_update_order); pub(crate) async fn update_order( msg: UpdateOrder, t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { sqlx::query_as( r#" UPDATE orders SET buyer_id = $2 AND status = $3 AND order_id = $4 WHERE id = $1 RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id "#, ) .bind(msg.id) .bind(msg.buyer_id) .bind(msg.status) .bind(msg.order_id) .fetch_one(t) .await .map_err(|e| { tracing::error!("{e:?}"); super::Error::AccountOrder(Error::CantCreate) }) } #[derive(actix::Message)] #[rtype(result = "Result")] pub struct UpdateOrderByExt { pub order_ext_id: String, pub status: OrderStatus, } db_async_handler!( UpdateOrderByExt, update_order_by_ext, Order, inner_update_order_by_ext ); pub(crate) async fn update_order_by_ext( msg: UpdateOrderByExt, t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { sqlx::query_as( r#" UPDATE orders SET status = $2 WHERE order_ext_id = $1 :: UUID RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id "#, ) .bind(msg.order_ext_id) .bind(msg.status) .fetch_one(t) .await .map_err(|e| { tracing::error!("{e:?}"); dbg!(e); super::Error::AccountOrder(Error::CantCreate) }) } #[derive(actix::Message)] #[rtype(result = "Result")] pub struct FindOrder { pub id: OrderId, } db_async_handler!( FindOrder, find_account_order, Order, inner_find_account_order ); pub(crate) async fn find_account_order( msg: FindOrder, t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { sqlx::query_as( r#" SELECT id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id FROM orders WHERE id = $1 "#, ) .bind(msg.id) .fetch_one(t) .await .map_err(|e| { tracing::error!("{e:?}"); super::Error::AccountOrder(Error::NotExists) }) } #[derive(actix::Message)] #[rtype(result = "Result")] pub struct SetOrderServiceId { pub id: OrderId, pub service_order_id: String, } db_async_handler!( SetOrderServiceId, set_order_service_id, Order, inner_set_order_service_id ); pub(crate) async fn set_order_service_id( msg: SetOrderServiceId, t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> Result { sqlx::query_as( r#" UPDATE orders SET service_order_id = $2 WHERE id = $1 RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id "#, ) .bind(msg.id) .bind(msg.service_order_id) .fetch_one(t) .await .map_err(|e| { tracing::error!("{e:?}"); super::Error::AccountOrder(Error::NotExists) }) } #[cfg(test)] mod tests { use config::UpdateConfig; use fake::Fake; use model::*; use uuid::Uuid; pub struct NoOpts; impl UpdateConfig for NoOpts {} use crate::*; async fn test_order_address( t: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> model::OrderAddress { create_order_address( CreateOrderAddress { name: Default::default(), email: Default::default(), street: Default::default(), city: Default::default(), country: Default::default(), zip: Default::default(), phone: Default::default(), }, t, ) .await .unwrap() } async fn test_product(t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> Product { create_product( CreateProduct { name: ProductName::new(format!("a{}", Uuid::new_v4())), short_description: ProductShortDesc::new(format!("{}", Uuid::new_v4())), long_description: ProductLongDesc::new(format!("{}", Uuid::new_v4())), category: None, price: Price::from_u32(4687), deliver_days_flag: Days(vec![Day::Friday, Day::Sunday]), }, t, ) .await .unwrap() } async fn test_shopping_cart( t: &mut sqlx::Transaction<'_, sqlx::Postgres>, buyer_id: Option, state: ShoppingCartState, ) -> ShoppingCart { let buyer_id = match buyer_id { Some(id) => id, _ => test_account(&mut *t).await.id, }; sqlx::query(r#" UPDATE shopping_carts SET state = 'closed' WHERE buyer_id = $1 "#) .bind(buyer_id) .execute(&mut *t) .await .unwrap(); let cart = create_shopping_cart( CreateShoppingCart { buyer_id, payment_method: PaymentMethod::PaymentOnTheSpot, }, &mut *t, ) .await .unwrap(); update_shopping_cart( UpdateShoppingCart { id: cart.id, buyer_id: cart.buyer_id, payment_method: cart.payment_method, state, checkout_notes: None, }, &mut *t, ) .await .unwrap() } async fn test_account(t: &mut sqlx::Transaction<'_, sqlx::Postgres>) -> FullAccount { use fake::faker::internet::en; let login: String = en::Username().fake(); let email: String = en::FreeEmail().fake(); let hash: String = en::Password(10..20).fake(); create_account( CreateAccount { email: Email::new(email), login: Login::new(login), pass_hash: PassHash::new(hash), role: Role::Admin, }, t, ) .await .unwrap() } async fn test_empty_order_without_cart( t: &mut sqlx::Transaction<'_, sqlx::Postgres>, buyer_id: Option, address_id: OrderAddressId, ) -> Order { let buyer_id = match buyer_id { Some(id) => id, _ => test_account(t).await.id, }; super::create_order( CreateOrder { buyer_id, items: vec![], shopping_cart_id: None, checkout_notes: None, delivery_address_id: address_id, }, t, ) .await .unwrap() } #[actix::test] async fn empty_order_without_cart() { testx::db_t!(t); let address_id = test_order_address(&mut t).await.id; test_empty_order_without_cart(&mut t, None, address_id).await; testx::db_rollback!(t); } #[actix::test] async fn empty_order_with_cart() { testx::db_t!(t); let buyer_id = test_account(&mut t).await.id; let address_id = test_order_address(&mut t).await.id; let cart_id = test_shopping_cart(&mut t, Some(buyer_id), ShoppingCartState::Active) .await .id; super::create_order( CreateOrder { buyer_id, items: vec![], shopping_cart_id: Some(cart_id), checkout_notes: None, delivery_address_id: address_id, }, &mut t, ) .await .unwrap(); testx::db_rollback!(t); } #[actix::test] async fn non_empty_order_with_cart() { testx::db_t!(t); let buyer_id = test_account(&mut t).await.id; let address_id = test_order_address(&mut t).await.id; let cart_id = test_shopping_cart(&mut t, Some(buyer_id), ShoppingCartState::Active) .await .id; super::create_order( CreateOrder { buyer_id, items: vec![ create_order::OrderItem { product_id: test_product(&mut t).await.id, quantity: Default::default(), quantity_unit: QuantityUnit::Gram, }, create_order::OrderItem { product_id: test_product(&mut t).await.id, quantity: Default::default(), quantity_unit: QuantityUnit::Gram, }, ], shopping_cart_id: Some(cart_id), checkout_notes: None, delivery_address_id: address_id, }, &mut t, ) .await .unwrap(); testx::db_rollback!(t); } #[actix::test] async fn non_empty_order_without_cart() { testx::db_t!(t); let buyer_id = test_account(&mut t).await.id; let address_id = test_order_address(&mut t).await.id; super::create_order( CreateOrder { buyer_id, items: vec![ create_order::OrderItem { product_id: test_product(&mut t).await.id, quantity: Default::default(), quantity_unit: QuantityUnit::Gram, }, create_order::OrderItem { product_id: test_product(&mut t).await.id, quantity: Default::default(), quantity_unit: QuantityUnit::Gram, }, ], shopping_cart_id: None, checkout_notes: None, delivery_address_id: address_id, }, &mut t, ) .await .unwrap(); testx::db_rollback!(t); } #[actix::test] async fn update_by_ext() { testx::db_t!(t); let address_id = test_order_address(&mut t).await.id; let original = test_empty_order_without_cart(&mut t, None, address_id).await; let updated = super::update_order_by_ext( UpdateOrderByExt { order_ext_id: original.order_ext_id.clone().to_string(), status: OrderStatus::Payed, }, &mut t, ) .await .unwrap(); testx::db_rollback!(t); assert_ne!(updated, original); assert_eq!( updated, Order { id: original.id, buyer_id: original.buyer_id, status: OrderStatus::Payed, order_ext_id: original.order_ext_id, service_order_id: None, checkout_notes: None, address_id: original.address_id } ); } }