diff --git a/Cargo.lock b/Cargo.lock index 19befa5..bf14e4a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,22 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "account_manager" +version = "0.1.0" +dependencies = [ + "actix 0.13.0", + "actix-rt", + "chrono", + "config", + "database_manager", + "log", + "model", + "pretty_env_logger", + "thiserror", + "uuid 0.8.2", +] + [[package]] name = "actix" version = "0.12.0" diff --git a/Cargo.toml b/Cargo.toml index d2ef3f2..d5f2d91 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,6 +11,7 @@ members = [ "api", "web", "shared/model", + "actors/account_manager", "actors/cart_manager", "actors/database_manager", "actors/email_manager", diff --git a/actors/account_manager/Cargo.toml b/actors/account_manager/Cargo.toml new file mode 100644 index 0000000..01397ff --- /dev/null +++ b/actors/account_manager/Cargo.toml @@ -0,0 +1,20 @@ +[package] +name = "account_manager" +version = "0.1.0" +edition = "2021" + +[dependencies] +model = { path = "../../shared/model" } +config = { path = "../../shared/config" } +database_manager = { path = "../database_manager" } + +actix = { version = "0.13", features = [] } +actix-rt = { version = "2.7", features = [] } + +thiserror = { version = "1.0.31" } + +uuid = { version = "0.8", features = ["serde"] } +chrono = { version = "0.4", features = ["serde"] } + +log = { version = "0.4", features = [] } +pretty_env_logger = { version = "0.4", features = [] } diff --git a/actors/account_manager/src/lib.rs b/actors/account_manager/src/lib.rs new file mode 100644 index 0000000..9726b32 --- /dev/null +++ b/actors/account_manager/src/lib.rs @@ -0,0 +1,10 @@ +#[derive(Debug)] +pub struct AccountManager { + db: actix::Addr, +} + +impl AccountManager { + pub fn new(db: actix::Addr) -> Self { + Self { db } + } +} diff --git a/actors/cart_manager/src/lib.rs b/actors/cart_manager/src/lib.rs index a0e549a..4383a87 100644 --- a/actors/cart_manager/src/lib.rs +++ b/actors/cart_manager/src/lib.rs @@ -3,10 +3,7 @@ use std::collections::HashSet; use database_manager::{query_db, Database}; -use model::{ - AccountId, ProductId, Quantity, QuantityUnit, ShoppingCartId, ShoppingCartItem, - ShoppingCartItemId, ShoppingCartState, -}; +use model::{PaymentMethod, ShoppingCartId}; #[macro_export] macro_rules! cart_async_handler { @@ -75,7 +72,7 @@ pub enum Error { #[error("Failed to change quantity")] ChangeQuantity, #[error("Shopping cart item {0} does not exists")] - NotExists(ShoppingCartItemId), + NotExists(model::ShoppingCartItemId), } pub type Result = std::result::Result; @@ -95,20 +92,20 @@ impl CartManager { } #[derive(actix::Message, Debug)] -#[rtype(result = "Result>")] +#[rtype(result = "Result>")] pub struct ModifyItem { - pub buyer_id: AccountId, - pub product_id: ProductId, - pub quantity: Quantity, - pub quantity_unit: QuantityUnit, + pub buyer_id: model::AccountId, + pub product_id: model::ProductId, + pub quantity: model::Quantity, + pub quantity_unit: model::QuantityUnit, } -cart_async_handler!(ModifyItem, modify_item, Option); +cart_async_handler!(ModifyItem, modify_item, Option); async fn modify_item( msg: ModifyItem, db: actix::Addr, -) -> Result> { +) -> Result> { let _cart = query_db!( db, database_manager::EnsureActiveShoppingCart { @@ -120,7 +117,7 @@ async fn modify_item( db, database_manager::AccountShoppingCarts { account_id: msg.buyer_id, - state: Some(ShoppingCartState::Active), + state: Some(model::ShoppingCartState::Active), }, passthrough Error::Db, Error::CartNotAvailable @@ -173,18 +170,22 @@ async fn modify_item( } #[derive(actix::Message)] -#[rtype(result = "Result>")] +#[rtype(result = "Result>")] pub struct RemoveProduct { - pub shopping_cart_id: ShoppingCartId, - pub shopping_cart_item_id: ShoppingCartItemId, + pub shopping_cart_id: model::ShoppingCartId, + pub shopping_cart_item_id: model::ShoppingCartItemId, } -cart_async_handler!(RemoveProduct, remove_product, Option); +cart_async_handler!( + RemoveProduct, + remove_product, + Option +); pub(crate) async fn remove_product( msg: RemoveProduct, db: actix::Addr, -) -> Result> { +) -> Result> { Ok(query_db!( db, database_manager::RemoveCartItem { @@ -196,39 +197,49 @@ pub(crate) async fn remove_product( )) } -#[derive(actix::Message, Debug)] -#[rtype(result = "Result>")] -pub struct ModifyCart { - pub buyer_id: AccountId, - pub items: Vec, +pub struct ModifyCartResult { + pub cart_id: ShoppingCartId, + pub items: Vec, + pub checkout_notes: String, + pub payment_method: model::PaymentMethod, } -cart_async_handler!(ModifyCart, modify_cart, Vec); +#[derive(actix::Message, Debug)] +#[rtype(result = "Result")] +pub struct ModifyCart { + pub buyer_id: model::AccountId, + pub items: Vec, + pub checkout_notes: String, + pub payment_method: Option, +} -async fn modify_cart(msg: ModifyCart, db: actix::Addr) -> Result> { +cart_async_handler!(ModifyCart, modify_cart, ModifyCartResult); + +async fn modify_cart(msg: ModifyCart, db: actix::Addr) -> Result { log::debug!("{:?}", msg); - let _cart = query_db!( + let cart: model::ShoppingCart = query_db!( db, database_manager::EnsureActiveShoppingCart { buyer_id: msg.buyer_id, }, Error::ShoppingCartFailed ); - let mut carts: Vec = query_db!( + let cart: model::ShoppingCart = query_db!( db, - database_manager::AccountShoppingCarts { - account_id: msg.buyer_id, - state: Some(ShoppingCartState::Active), + database_manager::UpdateShoppingCart { + id: cart.id, + buyer_id: msg.buyer_id, + payment_method: msg.payment_method.unwrap_or(cart.payment_method), + state: model::ShoppingCartState::Active, + checkout_notes: if msg.checkout_notes.is_empty() { + None + } else { + Some(msg.checkout_notes) + } }, passthrough Error::Db, Error::CartNotAvailable ); - log::debug!("carts {:?}", carts); - let cart = if carts.is_empty() { - return Err(Error::CartNotAvailable); - } else { - carts.remove(0) - }; let existing = msg.items @@ -269,5 +280,10 @@ async fn modify_cart(msg: ModifyCart, db: actix::Addr) -> Result, + inner_account_addresses +); + +pub(crate) async fn account_addresses( + msg: AccountAddresses, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result> { + sqlx::query_as( + r#" +SELECT id, name, email, street, city, country, zip, account_id +FROM account_addresses +WHERE account_id = $1 + "#, + ) + .bind(msg.account_id) + .fetch_all(pool) + .await + .map_err(|_| Error::AccountAddresses.into()) +} + +#[derive(actix::Message)] +#[rtype(result = "Result")] +pub struct CreateAccountAddress { + pub name: model::Name, + pub email: model::Email, + pub street: model::Street, + pub city: model::City, + pub country: model::Country, + pub zip: model::Zip, + pub account_id: model::AccountId, +} + +db_async_handler!( + CreateAccountAddress, + create_address, + model::Address, + inner_create_address +); + +pub(crate) async fn create_address( + msg: CreateAccountAddress, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result { + sqlx::query_as( + r#" +INSERT INTO account_addresses ( name, email, street, city, country, zip, account_id ) +VALUES ($1, $2, $3, $4, $5, $6, $7) +RETURNING id, name, email, street, city, country, zip, account_id + "#, + ) + .bind(msg.name) + .bind(msg.email) + .bind(msg.street) + .bind(msg.city) + .bind(msg.country) + .bind(msg.zip) + .bind(msg.account_id) + .fetch_one(pool) + .await + .map_err(|_| Error::CreateAccountAddress.into()) +} + +#[derive(actix::Message)] +#[rtype(result = "Result")] +pub struct UpdateAccountAddress { + pub id: model::AddressId, + pub name: model::Name, + pub email: model::Email, + pub street: model::Street, + pub city: model::City, + pub country: model::Country, + pub zip: model::Zip, + pub account_id: model::AccountId, +} + +db_async_handler!( + UpdateAccountAddress, + update_account_address, + model::Address, + inner_update_account_address +); + +pub(crate) async fn update_account_address( + msg: UpdateAccountAddress, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result { + sqlx::query_as( + r#" +UPDATE account_addresses +SET name = $2, email = $3, street = $4, city = $5, country = $6, zip = $7, account_id = $8 +WHERE id = $1 +RETURNING id, name, email, street, city, country, zip, account_id + "#, + ) + .bind(msg.id) + .bind(msg.name) + .bind(msg.email) + .bind(msg.street) + .bind(msg.city) + .bind(msg.country) + .bind(msg.zip) + .bind(msg.account_id) + .fetch_one(pool) + .await + .map_err(|_| Error::CreateAccountAddress.into()) +} diff --git a/actors/database_manager/src/lib.rs b/actors/database_manager/src/lib.rs index 20c6dee..b3d3a1a 100644 --- a/actors/database_manager/src/lib.rs +++ b/actors/database_manager/src/lib.rs @@ -5,6 +5,7 @@ use sqlx_core::arguments::Arguments; pub use crate::account_orders::*; pub use crate::accounts::*; +pub use crate::addresses::*; pub use crate::order_items::*; pub use crate::photos::*; pub use crate::product_photos::*; @@ -16,6 +17,7 @@ pub use crate::tokens::*; pub mod account_orders; pub mod accounts; +pub mod addresses; pub mod order_items; pub mod photos; pub mod product_photos; @@ -138,6 +140,8 @@ pub enum Error { Photo(#[from] photos::Error), #[error("{0}")] ProductPhoto(#[from] product_photos::Error), + #[error("{0}")] + AccountAddress(#[from] addresses::Error), } pub type Result = std::result::Result; diff --git a/actors/database_manager/src/order_items.rs b/actors/database_manager/src/order_items.rs index 3f49db0..7ae285a 100644 --- a/actors/database_manager/src/order_items.rs +++ b/actors/database_manager/src/order_items.rs @@ -38,7 +38,7 @@ ORDER BY id DESC .await .map_err(|e| { log::error!("{e:?}"); - super::Error::OrderItem(Error::All) + super::Error::from(Error::All) }) } diff --git a/actors/database_manager/src/shopping_carts.rs b/actors/database_manager/src/shopping_carts.rs index 062a5a2..745f007 100644 --- a/actors/database_manager/src/shopping_carts.rs +++ b/actors/database_manager/src/shopping_carts.rs @@ -124,6 +124,7 @@ pub struct UpdateShoppingCart { pub buyer_id: AccountId, pub payment_method: PaymentMethod, pub state: ShoppingCartState, + pub checkout_notes: Option, } db_async_handler!(UpdateShoppingCart, update_shopping_cart, ShoppingCart); @@ -135,7 +136,7 @@ pub(crate) async fn update_shopping_cart( sqlx::query_as( r#" UPDATE shopping_carts -SET buyer_id = $2 AND payment_method = $2 AND state = $4 +SET buyer_id = $2, payment_method = $3, state = $4, checkout_notes = $5 WHERE id = $1 RETURNING id, buyer_id, payment_method, state, checkout_notes "#, @@ -144,6 +145,7 @@ RETURNING id, buyer_id, payment_method, state, checkout_notes .bind(msg.buyer_id) .bind(msg.payment_method) .bind(msg.state) + .bind(msg.checkout_notes) .fetch_one(&db) .await .map_err(|e| { diff --git a/api/src/routes/admin/api_v1/accounts.rs b/api/src/routes/admin/api_v1/accounts.rs index 54154a4..0c574aa 100644 --- a/api/src/routes/admin/api_v1/accounts.rs +++ b/api/src/routes/admin/api_v1/accounts.rs @@ -4,7 +4,7 @@ use actix_web::{get, patch, post, HttpResponse}; use actix_web_httpauth::extractors::bearer::BearerAuth; use config::SharedAppConfig; use database_manager::Database; -use model::{AccountId, AccountState, Encrypt, PasswordConfirmation}; +use model::{AccountId, AccountState, Address, Encrypt, PasswordConfirmation}; use token_manager::TokenManager; use crate::routes::admin::Error; @@ -112,9 +112,16 @@ pub async fn create_account( role: payload.role, } ); + let addresses: Vec
= admin_send_db!( + db, + database_manager::AccountAddresses { + account_id: account.id + } + ); + Ok(Json(model::api::admin::RegisterResponse { errors: vec![], - account: Some(account.into()), + account: Some((account, addresses).into()), })) } diff --git a/api/src/routes/public/api_v1/restricted.rs b/api/src/routes/public/api_v1/restricted.rs index 31715fa..64bdc7d 100644 --- a/api/src/routes/public/api_v1/restricted.rs +++ b/api/src/routes/public/api_v1/restricted.rs @@ -142,18 +142,23 @@ async fn update_cart( ) .collect(); - let items: Vec = query_cart!( + let res: cart_manager::ModifyCartResult = query_cart!( cart, cart_manager::ModifyCart { buyer_id: token.account_id(), - items + items, + checkout_notes: payload.notes, + payment_method: payload.payment_method, }, routes::Error::Public(super::Error::ModifyItem.into()), routes::Error::Public(PublicError::DatabaseConnection) ); Ok(Json(api::UpdateCartOutput { - items: items.into_iter().map(Into::into).collect(), + cart_id: res.cart_id, + items: res.items.into_iter().map(Into::into).collect(), + checkout_notes: res.checkout_notes, + payment_method: res.payment_method, })) } @@ -201,14 +206,16 @@ pub(crate) async fn me( db: Data>, tm: Data>, credentials: BearerAuth, -) -> routes::Result> { +) -> routes::Result> { let account_id: model::AccountId = credentials .require_user(tm.into_inner()) .await? .account_id(); let account: model::FullAccount = public_send_db!(owned, db, database_manager::FindAccount { account_id }); - Ok(Json(account.into())) + let addresses = public_send_db!(owned, db, database_manager::AccountAddresses { account_id }); + + Ok(Json((account, addresses).into())) } #[post("/order")] diff --git a/migrations/20220519121203_account_addresses.sql b/migrations/20220519121203_account_addresses.sql new file mode 100644 index 0000000..1ca4b9d --- /dev/null +++ b/migrations/20220519121203_account_addresses.sql @@ -0,0 +1,11 @@ +CREATE TABLE account_addresses +( + id serial not null primary key unique, + name text not null, + email text not null, + street text not null, + city text not null, + country text not null, + zip text not null, + account_id int references accounts (id) +); diff --git a/shared/model/src/api.rs b/shared/model/src/api.rs index b97116c..95dfeca 100644 --- a/shared/model/src/api.rs +++ b/shared/model/src/api.rs @@ -20,6 +20,82 @@ pub struct Config { pub shipping_methods: Vec, } +#[derive(Serialize, Deserialize, Debug)] +pub struct Account { + pub id: AccountId, + pub email: Email, + pub login: Login, + pub role: Role, + pub customer_id: uuid::Uuid, + pub state: AccountState, + pub addresses: Vec
, +} + +impl From<(FullAccount, Vec)> for Account { + fn from( + ( + FullAccount { + id, + email, + login, + pass_hash: _, + role, + customer_id, + state, + }, + addresses, + ): (FullAccount, Vec), + ) -> Self { + Self { + id, + email, + login, + role, + customer_id, + state, + addresses: addresses.into_iter().map(From::from).collect(), + } + } +} + +#[derive(Serialize, Deserialize, Debug)] +pub struct Address { + pub id: AddressId, + pub name: Name, + pub email: Email, + pub street: Street, + pub city: City, + pub country: Country, + pub zip: Zip, + pub account_id: AccountId, +} + +impl From for Address { + fn from( + crate::Address { + id, + name, + email, + street, + city, + country, + zip, + account_id, + }: crate::Address, + ) -> Self { + Self { + id, + name, + email, + street, + city, + country, + zip, + account_id, + } + } +} + #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[derive(Serialize, Deserialize, Debug)] #[serde(transparent)] @@ -388,11 +464,16 @@ pub struct UpdateItemOutput { #[derive(Serialize, Deserialize, Debug)] pub struct UpdateCartInput { pub items: Vec, + pub notes: String, + pub payment_method: Option, } #[derive(Serialize, Deserialize, Debug)] pub struct UpdateCartOutput { + pub cart_id: ShoppingCartId, pub items: Vec, + pub checkout_notes: String, + pub payment_method: PaymentMethod, } #[derive(Serialize, Deserialize, Debug)] @@ -419,7 +500,7 @@ pub mod admin { #[derive(Serialize, Deserialize, Debug, Default)] pub struct RegisterResponse { pub errors: Vec, - pub account: Option, + pub account: Option, } #[derive(Serialize, Deserialize, Debug)] diff --git a/shared/model/src/dummy.rs b/shared/model/src/dummy.rs index 852182c..4d59031 100644 --- a/shared/model/src/dummy.rs +++ b/shared/model/src/dummy.rs @@ -82,3 +82,39 @@ impl fake::Dummy for NonNegative { Self(price) } } + +impl fake::Dummy for crate::Name { + fn dummy_with_rng(_config: &T, _rng: &mut R) -> Self { + use fake::faker::name::raw::*; + use fake::locales::*; + Self(Name(EN).fake()) + } +} +impl fake::Dummy for crate::Street { + fn dummy_with_rng(_config: &T, _rng: &mut R) -> Self { + use fake::faker::address::raw::*; + use fake::locales::*; + Self(StreetName(EN).fake()) + } +} +impl fake::Dummy for crate::City { + fn dummy_with_rng(_config: &T, _rng: &mut R) -> Self { + use fake::faker::address::raw::*; + use fake::locales::*; + Self(CityName(EN).fake()) + } +} +impl fake::Dummy for crate::Country { + fn dummy_with_rng(_config: &T, _rng: &mut R) -> Self { + use fake::faker::address::raw::*; + use fake::locales::*; + Self(CountryName(EN).fake()) + } +} +impl fake::Dummy for crate::Zip { + fn dummy_with_rng(_config: &T, _rng: &mut R) -> Self { + use fake::faker::address::raw::*; + use fake::locales::*; + Self(ZipCode(EN).fake()) + } +} diff --git a/shared/model/src/lib.rs b/shared/model/src/lib.rs index 317f790..9d1dc21 100644 --- a/shared/model/src/lib.rs +++ b/shared/model/src/lib.rs @@ -208,13 +208,19 @@ impl QuantityUnit { #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] -#[derive(Copy, Clone, Debug, Hash, Display, Deserialize, Serialize)] +#[derive(Copy, Clone, Debug, Hash, PartialEq, Display, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum PaymentMethod { PayU, PaymentOnTheSpot, } +impl Default for PaymentMethod { + fn default() -> Self { + Self::PaymentOnTheSpot + } +} + #[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] @@ -1113,3 +1119,83 @@ pub enum ShippingMethod { /// Shop owner will ship product manually Manual, } + +#[cfg_attr(feature = "dummy", derive(fake::Dummy))] +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, Hash, Deref, Display, From)] +pub struct AddressId(RecordId); + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, From, Display)] +#[serde(transparent)] +pub struct Name(String); + +impl Name { + pub fn new>(s: S) -> Self { + Self(s.into()) + } +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, From, Display)] +#[serde(transparent)] +pub struct Street(String); + +impl Street { + pub fn new>(s: S) -> Self { + Self(s.into()) + } +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, From, Display)] +#[serde(transparent)] +pub struct City(String); + +impl City { + pub fn new>(s: S) -> Self { + Self(s.into()) + } +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, From, Display)] +#[serde(transparent)] +pub struct Country(String); + +impl Country { + pub fn new>(s: S) -> Self { + Self(s.into()) + } +} + +#[cfg_attr(feature = "db", derive(sqlx::Type))] +#[cfg_attr(feature = "db", sqlx(transparent))] +#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, From, Display)] +#[serde(transparent)] +pub struct Zip(String); + +impl Zip { + pub fn new>(s: S) -> Self { + Self(s.into()) + } +} + +#[cfg_attr(feature = "dummy", derive(fake::Dummy))] +#[cfg_attr(feature = "db", derive(sqlx::FromRow))] +#[derive(Serialize, Deserialize, Debug)] +pub struct Address { + pub id: AddressId, + pub name: Name, + pub email: Email, + pub street: Street, + pub city: City, + pub country: Country, + pub zip: Zip, + pub account_id: AccountId, +} diff --git a/web/src/api/public.rs b/web/src/api/public.rs index 15fd33c..55bdb88 100644 --- a/web/src/api/public.rs +++ b/web/src/api/public.rs @@ -4,19 +4,19 @@ use seed::fetch::{Header, Method, Request}; use crate::api::perform; use crate::NetRes; -pub async fn config() -> super::NetRes { +pub async fn config() -> NetRes { perform(Request::new("/config").method(Method::Get)).await } -pub async fn fetch_products() -> super::NetRes { +pub async fn fetch_products() -> NetRes { perform(Request::new("/api/v1/products").method(Method::Get)).await } -pub async fn fetch_product(product_id: model::ProductId) -> super::NetRes { +pub async fn fetch_product(product_id: model::ProductId) -> NetRes { perform(Request::new(format!("/api/v1/product/{}", product_id)).method(Method::Get)).await } -pub async fn fetch_me(access_token: AccessTokenString) -> super::NetRes { +pub async fn fetch_me(access_token: AccessTokenString) -> NetRes { perform( Request::new("/api/v1/me") .header(Header::bearer(access_token.as_str())) @@ -25,7 +25,7 @@ pub async fn fetch_me(access_token: AccessTokenString) -> super::NetRes super::NetRes { +pub async fn sign_in(input: model::api::SignInInput) -> NetRes { perform( Request::new("/api/v1/sign-in") .method(Method::Post) @@ -35,7 +35,7 @@ pub async fn sign_in(input: model::api::SignInInput) -> super::NetRes super::NetRes { +pub async fn verify_token(access_token: AccessTokenString) -> NetRes { perform( Request::new("/api/v1/token/verify") .method(Method::Post) @@ -44,9 +44,7 @@ pub async fn verify_token(access_token: AccessTokenString) -> super::NetRes super::NetRes { +pub async fn refresh_token(access_token: RefreshTokenString) -> NetRes { perform( Request::new("/api/v1/token/refresh") .method(Method::Post) @@ -55,14 +53,12 @@ pub async fn refresh_token( .await } -pub async fn sign_up( - input: model::api::CreateAccountInput, -) -> super::NetRes { +pub async fn sign_up(input: model::api::CreateAccountInput) -> NetRes { perform( Request::new("/api/v1/register") .method(Method::Post) .json(&input) - .map_err(crate::api::NetRes::Http)?, + .map_err(NetRes::Http)?, ) .await } @@ -83,7 +79,7 @@ pub async fn update_cart_item( .method(Method::Put) .header(Header::bearer(access_token.as_str())) .json(&input) - .map_err(crate::api::NetRes::Http)?, + .map_err(NetRes::Http)?, ) .await } @@ -91,8 +87,11 @@ pub async fn update_cart_item( pub async fn update_cart( access_token: AccessTokenString, items: Vec, + notes: String, + payment_method: Option, ) -> NetRes { let input = model::api::UpdateCartInput { + notes, items: items .into_iter() .map( @@ -107,13 +106,14 @@ pub async fn update_cart( }, ) .collect(), + payment_method, }; perform( Request::new("/api/v1/shopping-cart") .method(Method::Put) .header(Header::bearer(access_token.as_str())) .json(&input) - .map_err(crate::api::NetRes::Http)?, + .map_err(NetRes::Http)?, ) .await } diff --git a/web/src/pages/public/checkout.rs b/web/src/pages/public/checkout.rs index 904c2bb..01a45d3 100644 --- a/web/src/pages/public/checkout.rs +++ b/web/src/pages/public/checkout.rs @@ -183,6 +183,7 @@ mod right_side { use seed::*; use crate::pages::public::checkout::CheckoutPage; + use crate::shopping_cart::CartMsg; use crate::Msg; static SELECTED_PAYMENT_METHOD: &str = "payment-selected-method"; @@ -291,22 +292,13 @@ mod right_side { } fn pay_u(model: &crate::Model) -> Node { - div![ - C!["w-full p-3 border-b border-gray-200"], - label![ - C!["flex items-center cursor-pointer"], - attrs![At::For => "pay_u"], - input![ - C!["form-radio h-5 w-5 text-indigo-500"], - attrs![At::Id => "pay_u", At::Name => SELECTED_PAYMENT_METHOD, At::Type => "radio", At::Value => "pay_u"], - ], - span![ - C!["flex items-center"], - pay_u_icon(), - span![C!["ml-3"], model.i18n.t("PayU")] - ] - ], - ] + payment_input( + model, + model::PaymentMethod::PayU, + "pay_u", + "PayU", + pay_u_icon(), + ) } fn pay_u_icon() -> Node { @@ -335,22 +327,13 @@ mod right_side { } fn pay_on_spot(model: &crate::Model) -> Node { - div![ - C!["w-full p-3 border-b border-gray-200"], - label![ - C!["flex items-center cursor-pointer"], - attrs![At::For => "pay_on_spot"], - input![ - C!["form-radio h-5 w-5 text-indigo-500"], - attrs![At::Id => "pay_on_spot", At::Name => SELECTED_PAYMENT_METHOD, At::Type => "radio", At::Value => "pay_on_spot"], - ], - span![ - C!["flex items-center"], - pay_in_spot_icon(), - span![C!["ml-3"], model.i18n.t("Pay on spot")] - ] - ], - ] + payment_input( + model, + model::PaymentMethod::PaymentOnTheSpot, + "pay_on_spot", + "Pay on spot", + pay_in_spot_icon(), + ) } fn pay_in_spot_icon() -> Node { @@ -411,4 +394,39 @@ mod right_side { ] ] } + + fn payment_input( + model: &crate::Model, + method: model::PaymentMethod, + name: &str, + label: &'static str, + icon: Node, + ) -> Node { + div![ + C!["w-full p-3 border-b border-gray-200"], + label![ + C!["flex items-center cursor-pointer"], + attrs![At::For => name], + input![ + C!["form-radio h-5 w-5 text-indigo-500"], + attrs![ + At::Id => name, + At::Name => SELECTED_PAYMENT_METHOD, + At::Type => "radio", + At::Value => method, + ], + IF![model.cart.payment_method.unwrap_or_default() == method => attrs![At::Checked => true]], + ev(Ev::Change, move |ev| { + ev.stop_propagation(); + crate::Msg::from(CartMsg::PaymentChanged(method)) + }) + ], + span![ + C!["flex items-center"], + icon, + span![C!["ml-3"], model.i18n.t(label)] + ] + ], + ] + } } diff --git a/web/src/pages/public/shopping_cart.rs b/web/src/pages/public/shopping_cart.rs index 9e9fa6d..ed9cdaa 100644 --- a/web/src/pages/public/shopping_cart.rs +++ b/web/src/pages/public/shopping_cart.rs @@ -101,7 +101,7 @@ mod summary_left { ev.stop_propagation(); let target = ev.target()?; let input = seed::to_textarea(&target); - Some(crate::Msg::from(CartMsg::ChangeNotes(input.value()))) + Some(crate::Msg::from(CartMsg::NotesChanged(input.value()))) }), model.cart.checkout_notes.as_str() ] diff --git a/web/src/shared.rs b/web/src/shared.rs index 2f4383e..bef83e3 100644 --- a/web/src/shared.rs +++ b/web/src/shared.rs @@ -10,7 +10,7 @@ pub mod view; #[derive(Debug)] pub enum SharedMsg { LoadMe, - MeLoaded(NetRes), + MeLoaded(NetRes), SignIn(model::api::SignInInput), SignedIn(NetRes), Notification(NotificationMsg), @@ -27,7 +27,7 @@ pub struct Model { pub access_token: Option, pub refresh_token: Option, pub exp: Option, - pub me: Option, + pub me: Option, pub notifications: Vec, } diff --git a/web/src/shared/view.rs b/web/src/shared/view.rs index d6c7fe2..5a8801a 100644 --- a/web/src/shared/view.rs +++ b/web/src/shared/view.rs @@ -207,7 +207,7 @@ pub mod cart_dropdown { let sum = rusty_money::Money::from_minor(sum as i64, model.config.currency); div![ - C!["p-4 justify-center flex"], + C!["p-4 justify-center flex bg-white"], button![ C![ "text-base undefined hover:scale-110 focus:outline-none flex justify-center px-4 py-2 rounded font-bold cursor-pointer" diff --git a/web/src/shopping_cart.rs b/web/src/shopping_cart.rs index 07cabb4..9ac5fc1 100644 --- a/web/src/shopping_cart.rs +++ b/web/src/shopping_cart.rs @@ -1,4 +1,4 @@ -use model::{ProductId, Quantity, QuantityUnit}; +use model::{PaymentMethod, ProductId, Quantity, QuantityUnit}; use seed::prelude::*; use serde::{Deserialize, Serialize}; @@ -23,7 +23,8 @@ pub enum CartMsg { /// Send current non-empty cart to server Sync, SyncResult(NetRes), - ChangeNotes(String), + NotesChanged(String), + PaymentChanged(model::PaymentMethod), } impl From for Msg { @@ -36,16 +37,23 @@ pub type Items = indexmap::IndexMap; #[derive(Debug, Copy, Clone, Serialize, Deserialize)] pub struct Item { + #[serde(rename = "i")] pub product_id: ProductId, + #[serde(rename = "q")] pub quantity: Quantity, + #[serde(rename = "u")] pub quantity_unit: QuantityUnit, } #[derive(Debug, Default, Serialize, Deserialize)] pub struct ShoppingCart { + #[serde(rename = "i")] pub cart_id: Option, + #[serde(rename = "is")] pub items: Items, - #[serde(default)] + #[serde(default, rename = "pm")] + pub payment_method: Option, + #[serde(default, rename = "cn")] pub checkout_notes: String, #[serde(skip)] pub hover: bool, @@ -109,6 +117,9 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders) { CartMsg::Sync => sync_cart(model, orders), CartMsg::SyncResult(NetRes::Success(cart)) => { let len = cart.items.len(); + model.cart.cart_id = Some(cart.cart_id); + model.cart.checkout_notes = cart.checkout_notes; + model.cart.payment_method = Some(cart.payment_method); model.cart.items = cart.items.into_iter().fold( IndexMap::with_capacity(len), |mut set, @@ -139,9 +150,15 @@ 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) => { + CartMsg::NotesChanged(notes) => { model.cart.checkout_notes = notes; store_local(&model.cart); + sync_cart(model, orders); + } + CartMsg::PaymentChanged(method) => { + model.cart.payment_method = Some(method); + store_local(&model.cart); + sync_cart(model, orders); } } } @@ -149,9 +166,11 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders) { 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 { + let notes = model.cart.checkout_notes.clone(); + let payment_method = model.cart.payment_method; + orders.perform_cmd(async move { crate::Msg::from(CartMsg::SyncResult( - crate::api::public::update_cart(access_token, items).await, + crate::api::public::update_cart(access_token, items, notes, payment_method).await, )) }); }