diff --git a/Cargo.toml b/Cargo.toml index 825275b..d2ef3f2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,3 +1,11 @@ +#[package] +#name = "bazzar" +#version = "0.1.0" +#edition = "2021" +# +#[[bin]] +#name = "api" +#path = "./api/src/main.rs" [workspace] members = [ "api", diff --git a/actors/cart_manager/src/lib.rs b/actors/cart_manager/src/lib.rs index b4bb1e4..a0e549a 100644 --- a/actors/cart_manager/src/lib.rs +++ b/actors/cart_manager/src/lib.rs @@ -94,7 +94,7 @@ impl CartManager { } } -#[derive(actix::Message)] +#[derive(actix::Message, Debug)] #[rtype(result = "Result>")] pub struct ModifyItem { pub buyer_id: AccountId, @@ -196,7 +196,7 @@ pub(crate) async fn remove_product( )) } -#[derive(actix::Message)] +#[derive(actix::Message, Debug)] #[rtype(result = "Result>")] pub struct ModifyCart { pub buyer_id: AccountId, @@ -206,6 +206,7 @@ pub struct ModifyCart { cart_async_handler!(ModifyCart, modify_cart, Vec); async fn modify_cart(msg: ModifyCart, db: actix::Addr) -> Result> { + log::debug!("{:?}", msg); let _cart = query_db!( db, database_manager::EnsureActiveShoppingCart { @@ -222,6 +223,7 @@ async fn modify_cart(msg: ModifyCart, db: actix::Addr) -> Result) -> Result = query_db!( + let items: Vec = query_db!( db, database_manager::CartItems { shopping_cart_id: cart.id @@ -244,7 +246,10 @@ async fn modify_cart(msg: ModifyCart, db: actix::Addr) -> Result) -> Result Result> { sqlx::query_as( r#" -SELECT id, buyer_id, status, order_ext_id, service_order_id +SELECT id, buyer_id, status, order_ext_id, service_order_id, checkout_notes FROM account_orders ORDER BY id DESC "#, @@ -60,6 +60,7 @@ pub struct CreateAccountOrder { pub buyer_id: AccountId, pub items: Vec, pub shopping_cart_id: ShoppingCartId, + pub checkout_notes: Option, } db_async_handler!( @@ -77,7 +78,7 @@ pub(crate) async fn create_account_order( r#" INSERT INTO account_orders (buyer_id, status) VALUES ($1, $2, $3) -RETURNING id, buyer_id, status, order_ext_id, service_order_id +RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes "#, ) .bind(msg.buyer_id) @@ -113,6 +114,7 @@ RETURNING id, buyer_id, status, order_ext_id, service_order_id ShoppingCartSetState { id: msg.shopping_cart_id, state: ShoppingCartState::Closed, + checkout_notes: msg.checkout_notes, }, t, ) @@ -146,7 +148,7 @@ pub(crate) async fn update_account_order( UPDATE account_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 +RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes "#, ) .bind(msg.id) @@ -183,7 +185,7 @@ pub(crate) async fn update_account_order_by_ext( UPDATE account_orders SET status = $2 WHERE order_ext_id = $1 -RETURNING id, buyer_id, status, order_ext_id, service_order_id +RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes "#, ) .bind(msg.order_ext_id) @@ -207,7 +209,7 @@ db_async_handler!(FindAccountOrder, find_account_order, AccountOrder); pub(crate) async fn find_account_order(msg: FindAccountOrder, db: PgPool) -> Result { sqlx::query_as( r#" -SELECT id, buyer_id, status, order_ext_id, service_order_id +SELECT id, buyer_id, status, order_ext_id, service_order_id, checkout_notes FROM account_orders WHERE id = $1 "#, @@ -239,7 +241,7 @@ pub(crate) async fn set_order_service_id( UPDATE account_orders SET service_order_id = $2 WHERE id = $1 -RETURNING id, buyer_id, status, order_ext_id, service_order_id +RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes "#, ) .bind(msg.id) diff --git a/actors/database_manager/src/shopping_cart_items.rs b/actors/database_manager/src/shopping_cart_items.rs index 2b4b3bd..c839e5e 100644 --- a/actors/database_manager/src/shopping_cart_items.rs +++ b/actors/database_manager/src/shopping_cart_items.rs @@ -172,7 +172,7 @@ pub(crate) async fn update_shopping_cart_item( sqlx::query_as( r#" UPDATE shopping_cart_items -SET product_id = $2 AND shopping_cart_id = $3 AND quantity = $4 AND quantity_unit = $5 +SET product_id = $2, shopping_cart_id = $3, quantity = $4, quantity_unit = $5 WHERE id = $1 RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit "#, diff --git a/actors/database_manager/src/shopping_carts.rs b/actors/database_manager/src/shopping_carts.rs index 88f0c6d..062a5a2 100644 --- a/actors/database_manager/src/shopping_carts.rs +++ b/actors/database_manager/src/shopping_carts.rs @@ -30,7 +30,7 @@ pub(crate) async fn all_shopping_carts( ) -> Result> { sqlx::query_as( r#" -SELECT id, buyer_id, payment_method, state +SELECT id, buyer_id, payment_method, state, checkout_notes FROM shopping_carts "#, ) @@ -62,9 +62,9 @@ pub(crate) async fn account_shopping_carts( if let Some(state) = msg.state { sqlx::query_as( r#" -SELECT id, buyer_id, payment_method, state - FROM shopping_carts - WHERE buyer_id = $1 AND state = $2 +SELECT id, buyer_id, payment_method, state, checkout_notes +FROM shopping_carts +WHERE buyer_id = $1 AND state = $2 "#, ) .bind(msg.account_id) @@ -72,7 +72,7 @@ SELECT id, buyer_id, payment_method, state } else { sqlx::query_as( r#" -SELECT id, buyer_id, payment_method, state +SELECT id, buyer_id, payment_method, state, checkout_notes FROM shopping_carts WHERE buyer_id = $1 "#, @@ -104,7 +104,7 @@ pub(crate) async fn create_shopping_cart( r#" INSERT INTO shopping_carts (buyer_id, payment_method) VALUES ($1, $2) -RETURNING id, buyer_id, payment_method, state +RETURNING id, buyer_id, payment_method, state, checkout_notes "#, ) .bind(msg.buyer_id) @@ -137,7 +137,7 @@ pub(crate) async fn update_shopping_cart( UPDATE shopping_carts SET buyer_id = $2 AND payment_method = $2 AND state = $4 WHERE id = $1 -RETURNING id, buyer_id, payment_method, state +RETURNING id, buyer_id, payment_method, state, checkout_notes "#, ) .bind(msg.id) @@ -157,38 +157,31 @@ RETURNING id, buyer_id, payment_method, state pub struct ShoppingCartSetState { pub id: ShoppingCartId, pub state: ShoppingCartState, + pub checkout_notes: Option, } db_async_handler!( ShoppingCartSetState, - inner_shopping_cart_set_state, - ShoppingCart + shopping_cart_set_state, + ShoppingCart, + inner_shopping_cart_set_state ); -async fn inner_shopping_cart_set_state( +pub(crate) async fn shopping_cart_set_state( msg: ShoppingCartSetState, - pool: PgPool, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, ) -> 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 +SET state = $2, checkout_notes = $3 WHERE id = $1 -RETURNING id, buyer_id, payment_method, state +RETURNING id, buyer_id, payment_method, state, checkout_notes "#, ) .bind(msg.id) .bind(msg.state) + .bind(msg.checkout_notes) .fetch_one(pool) .await .map_err(|e| { @@ -208,7 +201,7 @@ db_async_handler!(FindShoppingCart, find_shopping_cart, ShoppingCart); pub(crate) async fn find_shopping_cart(msg: FindShoppingCart, db: PgPool) -> Result { sqlx::query_as( r#" -SELECT id, buyer_id, payment_method, state +SELECT id, buyer_id, payment_method, state, checkout_notes FROM shopping_carts WHERE id = $1 "#, @@ -245,7 +238,7 @@ INSERT INTO shopping_carts (buyer_id, state) VALUES ($1, 'active') ON CONFLICT DO NOTHING -RETURNING id, buyer_id, payment_method, state; +RETURNING id, buyer_id, payment_method, state, checkout_notes "#, ) .bind(msg.buyer_id) @@ -259,7 +252,7 @@ RETURNING id, buyer_id, payment_method, state; }; sqlx::query_as( r#" -SELECT id, buyer_id, payment_method, state +SELECT id, buyer_id, payment_method, state, checkout_notes FROM shopping_carts WHERE buyer_id = $1 AND state = 'active' "#, diff --git a/actors/order_manager/src/lib.rs b/actors/order_manager/src/lib.rs index 0670882..64c8882 100644 --- a/actors/order_manager/src/lib.rs +++ b/actors/order_manager/src/lib.rs @@ -90,6 +90,7 @@ pub(crate) async fn create_order( quantity_unit: item.quantity_unit, }) .collect(), + checkout_notes: cart.checkout_notes }, Error::CreateAccountOrder, Error::DatabaseInternal diff --git a/actors/payment_manager/src/lib.rs b/actors/payment_manager/src/lib.rs index bd570ba..4a78f25 100644 --- a/actors/payment_manager/src/lib.rs +++ b/actors/payment_manager/src/lib.rs @@ -231,6 +231,7 @@ pub(crate) async fn request_payment( }) .collect(), shopping_cart_id: cart.id, + checkout_notes: cart.checkout_notes, }, Error::CreateOrder ); diff --git a/api/i18n.toml b/api/i18n.toml new file mode 100644 index 0000000..6b0bb7e --- /dev/null +++ b/api/i18n.toml @@ -0,0 +1,4 @@ +available-locales = ["en", "pl"] +default-locale = "en" +fallback_language = "en" +load-path = "locales" diff --git a/api/locales/en.yml b/api/locales/en.yml new file mode 100644 index 0000000..e69de29 diff --git a/api/locales/pl.yml b/api/locales/pl.yml new file mode 100644 index 0000000..e69de29 diff --git a/api/src/main.rs b/api/src/main.rs index e4656d5..1b0d361 100644 --- a/api/src/main.rs +++ b/api/src/main.rs @@ -59,6 +59,7 @@ async fn server(opts: ServerOpts) -> Result<()> { let fs_manager = fs_manager::FsManager::build(app_config.clone()) .await .expect("Failed to initialize file system storage"); + let cart_manager = cart_manager::CartManager::new(db.clone()).start(); let addr = { let l = app_config.lock(); let w = l.web(); @@ -87,6 +88,7 @@ async fn server(opts: ServerOpts) -> Result<()> { .app_data(Data::new(payment_manager.clone())) .app_data(Data::new(search_manager.clone())) .app_data(Data::new(fs_manager.clone())) + .app_data(Data::new(cart_manager.clone())) .configure(routes::configure) .service({ let l = app_config.lock(); diff --git a/migrations/20220519112905_add_checkout_notes.sql b/migrations/20220519112905_add_checkout_notes.sql new file mode 100644 index 0000000..78ba1da --- /dev/null +++ b/migrations/20220519112905_add_checkout_notes.sql @@ -0,0 +1,2 @@ +ALTER TABLE shopping_carts +ADD checkout_notes TEXT; diff --git a/migrations/20220519113031_add_checkout_notes_order.sql b/migrations/20220519113031_add_checkout_notes_order.sql new file mode 100644 index 0000000..e8b06d0 --- /dev/null +++ b/migrations/20220519113031_add_checkout_notes_order.sql @@ -0,0 +1,2 @@ +ALTER TABLE account_orders + ADD checkout_notes TEXT; diff --git a/shared/model/src/api.rs b/shared/model/src/api.rs index 443cf56..b97116c 100644 --- a/shared/model/src/api.rs +++ b/shared/model/src/api.rs @@ -38,6 +38,7 @@ impl From<(Vec, Vec)> for AccountOrders { order_id, order_ext_id: _, service_order_id: _, + checkout_notes, }| { AccountOrder { id, @@ -45,6 +46,7 @@ impl From<(Vec, Vec)> for AccountOrders { status, order_id, items: items.drain_filter(|item| item.order_id == id).collect(), + checkout_notes, } }, ) @@ -63,6 +65,7 @@ impl From<(crate::AccountOrder, Vec)> for AccountOrder { order_id, order_ext_id: _, service_order_id: _, + checkout_notes, }, mut items, ): (crate::AccountOrder, Vec), @@ -73,6 +76,7 @@ impl From<(crate::AccountOrder, Vec)> for AccountOrder { status, order_id, items: items.drain_filter(|item| item.order_id == id).collect(), + checkout_notes, } } } @@ -85,6 +89,7 @@ pub struct AccountOrder { pub status: crate::OrderStatus, pub order_id: Option, pub items: Vec, + pub checkout_notes: Option, } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] @@ -125,6 +130,7 @@ pub struct ShoppingCart { pub payment_method: PaymentMethod, pub state: ShoppingCartState, pub items: Vec, + pub checkout_notes: String, } impl From<(crate::ShoppingCart, Vec)> for ShoppingCart { @@ -135,6 +141,7 @@ impl From<(crate::ShoppingCart, Vec)> for ShoppingCart buyer_id, payment_method, state, + checkout_notes, }, items, ): (crate::ShoppingCart, Vec), @@ -144,6 +151,7 @@ impl From<(crate::ShoppingCart, Vec)> for ShoppingCart buyer_id, payment_method, state, + checkout_notes: checkout_notes.unwrap_or_default(), items: items .into_iter() .map( diff --git a/shared/model/src/lib.rs b/shared/model/src/lib.rs index 15cdeb9..317f790 100644 --- a/shared/model/src/lib.rs +++ b/shared/model/src/lib.rs @@ -856,6 +856,7 @@ pub struct AccountOrder { pub order_id: Option, pub order_ext_id: uuid::Uuid, pub service_order_id: Option, + pub checkout_notes: Option, } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] @@ -866,6 +867,7 @@ pub struct PublicAccountOrder { pub buyer_id: AccountId, pub status: OrderStatus, pub order_id: Option, + pub checkout_notes: String, } impl From for PublicAccountOrder { @@ -877,6 +879,7 @@ impl From for PublicAccountOrder { order_id, order_ext_id: _, service_order_id: _, + checkout_notes, }: AccountOrder, ) -> Self { Self { @@ -884,6 +887,7 @@ impl From for PublicAccountOrder { buyer_id, status, order_id, + checkout_notes: checkout_notes.unwrap_or_default(), } } } @@ -914,12 +918,13 @@ pub struct ShoppingCartId(pub RecordId); #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))] -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct ShoppingCart { pub id: ShoppingCartId, pub buyer_id: AccountId, pub payment_method: PaymentMethod, pub state: ShoppingCartState, + pub checkout_notes: Option, } #[cfg_attr(feature = "dummy", derive(fake::Dummy))] @@ -930,7 +935,7 @@ pub struct ShoppingCart { pub struct ShoppingCartItemId(RecordId); #[cfg_attr(feature = "db", derive(sqlx::FromRow))] -#[derive(Serialize, Deserialize)] +#[derive(Serialize, Deserialize, Debug)] pub struct ShoppingCartItem { pub id: ShoppingCartItemId, pub product_id: ProductId, diff --git a/web/Cargo.toml b/web/Cargo.toml index eff4c88..ba77457 100644 --- a/web/Cargo.toml +++ b/web/Cargo.toml @@ -25,7 +25,7 @@ wasm-bindgen = { version = "0.2.80", features = ["default"] } web-sys = { version = "0.3.57", features = ["Navigator"] } js-sys = { version = "0.3.57", features = [] } -indexmap = { version = "1", features = ["serde-1"] } +indexmap = { version = "1", default-features = false, features = ["serde-1", "std"] } rusty-money = { version = "0.4.1", features = ["iso"] } diff --git a/web/src/pages/public/shopping_cart.rs b/web/src/pages/public/shopping_cart.rs index 0e5949f..9e9fa6d 100644 --- a/web/src/pages/public/shopping_cart.rs +++ b/web/src/pages/public/shopping_cart.rs @@ -80,6 +80,7 @@ mod summary_left { use seed::*; use crate::pages::public::shopping_cart::ShoppingCartPage; + use crate::shopping_cart::CartMsg; pub fn view(model: &crate::Model, page: &ShoppingCartPage) -> Node { div![ @@ -94,7 +95,16 @@ mod summary_left { ] ], div![C!["p-4"], p![C!["mb-4 italic"], model.i18n.t("If you have some information for the seller you can leave them in the box below")]], - textarea![C!["w-full h-24 p-2 bg-gray-100 rounded border-none"]] + textarea![ + C!["w-full h-24 p-2 bg-gray-100 rounded border-none"], + ev(Ev::Change, move |ev| { + ev.stop_propagation(); + let target = ev.target()?; + let input = seed::to_textarea(&target); + Some(crate::Msg::from(CartMsg::ChangeNotes(input.value()))) + }), + model.cart.checkout_notes.as_str() + ] ] } diff --git a/web/src/shopping_cart.rs b/web/src/shopping_cart.rs index 428c882..07cabb4 100644 --- a/web/src/shopping_cart.rs +++ b/web/src/shopping_cart.rs @@ -23,6 +23,7 @@ pub enum CartMsg { /// Send current non-empty cart to server Sync, SyncResult(NetRes), + ChangeNotes(String), } impl From for Msg { @@ -44,6 +45,8 @@ pub struct Item { pub struct ShoppingCart { pub cart_id: Option, pub items: Items, + #[serde(default)] + pub checkout_notes: String, #[serde(skip)] pub hover: bool, } @@ -70,6 +73,7 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders) { entry.quantity_unit = quantity_unit; } store_local(&model.cart); + sync_cart(model, orders); } CartMsg::ModifyItem { product_id, @@ -89,10 +93,12 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders) { entry.quantity_unit = quantity_unit; } store_local(&model.cart); + sync_cart(model, orders); } CartMsg::Remove(product_id) => { model.cart.items.remove(&product_id); store_local(&model.cart); + sync_cart(model, orders); } CartMsg::Hover => { model.cart.hover = true; @@ -100,18 +106,30 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders) { CartMsg::Leave => { model.cart.hover = false; } - CartMsg::Sync => { - if let Some(access_token) = model.shared.access_token.as_ref().cloned() { - let items: Vec = model.cart.items.values().map(Clone::clone).collect(); - orders.perform_cmd(async { - crate::Msg::from(CartMsg::SyncResult( - crate::api::public::update_cart(access_token, items).await, - )) - }); - } - } + CartMsg::Sync => sync_cart(model, orders), CartMsg::SyncResult(NetRes::Success(cart)) => { - // cart.items + let len = cart.items.len(); + model.cart.items = cart.items.into_iter().fold( + IndexMap::with_capacity(len), + |mut set, + model::api::ShoppingCartItem { + id: _, + product_id, + shopping_cart_id: _, + quantity, + quantity_unit, + }| { + set.insert( + product_id, + Item { + product_id, + quantity, + quantity_unit, + }, + ); + set + }, + ); } CartMsg::SyncResult(NetRes::Error(failure)) => { for msg in failure.errors { @@ -121,6 +139,21 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders) { CartMsg::SyncResult(NetRes::Http(_cart)) => { orders.send_msg(NotificationMsg::Error("Unable to sync cart".into()).into()); } + CartMsg::ChangeNotes(notes) => { + model.cart.checkout_notes = notes; + store_local(&model.cart); + } + } +} + +fn sync_cart(model: &mut Model, orders: &mut impl Orders) { + if let Some(access_token) = model.shared.access_token.as_ref().cloned() { + let items: Vec = model.cart.items.values().map(Clone::clone).collect(); + orders.perform_cmd(async { + crate::Msg::from(CartMsg::SyncResult( + crate::api::public::update_cart(access_token, items).await, + )) + }); } }