From 7617cb106472661458af6fb92ef3169820fc79e9 Mon Sep 17 00:00:00 2001 From: eraden Date: Sat, 28 May 2022 14:03:14 +0200 Subject: [PATCH] Add phone number and address handling --- .../database_manager/src/account_addresses.rs | 35 +++++++ actors/order_manager/src/lib.rs | 69 ++++++++----- api/src/routes/mod.rs | 6 ++ api/src/routes/public/api_v1/mod.rs | 3 + api/src/routes/public/api_v1/restricted.rs | 56 ++++++----- .../20220528115140_add_phone_to_address.sql | 2 + shared/model/src/api.rs | 15 ++- web/src/api.rs | 1 + web/src/api/public.rs | 33 +++++-- web/src/i18n.rs | 4 + web/src/lib.rs | 3 +- web/src/model.rs | 9 ++ web/src/pages/public/checkout.rs | 97 +++++++++++++++---- 13 files changed, 253 insertions(+), 80 deletions(-) create mode 100644 migrations/20220528115140_add_phone_to_address.sql diff --git a/actors/database_manager/src/account_addresses.rs b/actors/database_manager/src/account_addresses.rs index c143e69..678e5a3 100644 --- a/actors/database_manager/src/account_addresses.rs +++ b/actors/database_manager/src/account_addresses.rs @@ -37,6 +37,41 @@ WHERE account_id = $1 .await .map_err(|_| Error::AccountAddresses.into()) } +//// + +#[derive(actix::Message)] +#[rtype(result = "Result")] +pub struct FindAccountAddress { + pub account_id: model::AccountId, + pub address_id: model::AddressId, +} + +db_async_handler!( + FindAccountAddress, + find_account_address, + model::AccountAddress, + inner_find_account_address +); + +pub(crate) async fn find_account_address( + msg: FindAccountAddress, + pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, +) -> Result { + sqlx::query_as( + r#" +SELECT id, name, email, street, city, country, zip, account_id, is_default +FROM account_addresses +WHERE account_id = $1 AND id = $2 + "#, + ) + .bind(msg.account_id) + .bind(msg.address_id) + .fetch_one(pool) + .await + .map_err(|_| Error::AccountAddresses.into()) +} + +///// #[derive(actix::Message)] #[rtype(result = "Result")] diff --git a/actors/order_manager/src/lib.rs b/actors/order_manager/src/lib.rs index 8dc019d..08d8765 100644 --- a/actors/order_manager/src/lib.rs +++ b/actors/order_manager/src/lib.rs @@ -84,11 +84,18 @@ pub struct CreateOrderAddress { pub zip: model::Zip, } +#[derive(Debug)] +pub enum OrderAddressInput { + Address(CreateOrderAddress), + AccountAddress(model::AddressId), + DefaultAccountAddress, +} + #[derive(Message, Debug)] #[rtype(result = "Result")] pub struct CreateAccountOrder { pub account_id: AccountId, - pub create_address: Option, + pub order_address: OrderAddressInput, } order_async_handler!(CreateAccountOrder, create_account_order, Order); @@ -115,29 +122,43 @@ pub(crate) async fn create_account_order( Error::ShoppingCart, Error::DatabaseInternal ); - let address: model::AccountAddress = if let Some(input) = msg.create_address { - query_db!( - db, - database_manager::CreateAccountAddress { - name: input.name, - email: input.email, - street: input.street, - city: input.city, - country: input.country, - zip: input.zip, - account_id: None, - is_default: true, - }, - Error::InvalidAccountAddress - ) - } else { - query_db!( - db, - database_manager::DefaultAccountAddress { - account_id: cart.buyer_id - }, - Error::NoAddress - ) + + let address: model::AccountAddress = match msg.order_address { + OrderAddressInput::Address(input) => { + query_db!( + db, + database_manager::CreateAccountAddress { + name: input.name, + email: input.email, + street: input.street, + city: input.city, + country: input.country, + zip: input.zip, + account_id: Some(cart.buyer_id), + is_default: true, + }, + Error::InvalidAccountAddress + ) + } + OrderAddressInput::AccountAddress(address_id) => { + query_db!( + db, + database_manager::FindAccountAddress { + address_id, + account_id: cart.buyer_id + }, + Error::NoAddress + ) + } + OrderAddressInput::DefaultAccountAddress => { + query_db!( + db, + database_manager::DefaultAccountAddress { + account_id: cart.buyer_id + }, + Error::NoAddress + ) + } }; query_db!( diff --git a/api/src/routes/mod.rs b/api/src/routes/mod.rs index d05d8b5..5108d75 100644 --- a/api/src/routes/mod.rs +++ b/api/src/routes/mod.rs @@ -51,6 +51,12 @@ pub enum Error { Token(token_manager::Error), } +impl From for Error { + fn from(e: public::api_v1::Error) -> Self { + Self::Public(public::Error::ApiV1(e)) + } +} + impl From for Error { fn from(sv1: V1ShoppingCartError) -> Self { Self::Public(PublicError::ApiV1(V1Error::ShoppingCart(sv1))) diff --git a/api/src/routes/public/api_v1/mod.rs b/api/src/routes/public/api_v1/mod.rs index 3238974..15d7902 100644 --- a/api/src/routes/public/api_v1/mod.rs +++ b/api/src/routes/public/api_v1/mod.rs @@ -21,6 +21,9 @@ pub enum Error { #[error("Failed to create order")] AddOrder, + + #[error("Can't place order. Client IP is unknown")] + NoIp, } 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 8a57bf5..6e24c69 100644 --- a/api/src/routes/public/api_v1/restricted.rs +++ b/api/src/routes/public/api_v1/restricted.rs @@ -236,7 +236,7 @@ pub(crate) async fn create_order( credentials: BearerAuth, payment: Data>, order: Data>, -) -> routes::Result { +) -> routes::Result> { let account_id = credentials .require_user(tm.into_inner()) .await? @@ -254,7 +254,7 @@ pub(crate) async fn create_order( } = payload; let ip = match req.peer_addr() { Some(ip) => ip, - _ => return Ok(HttpResponse::BadRequest().body("No IP")), + _ => return Err(super::Error::NoIp.into()), }; let payment_manager::CreatePaymentResult { redirect_uri, .. } = query_pay!( @@ -275,37 +275,43 @@ pub(crate) async fn create_order( routes::Error::Public(PublicError::DatabaseConnection) ); - query_order!( + let order_address = match address { + api::OrderAddressInput::DefaultAccountAddress => { + order_manager::OrderAddressInput::DefaultAccountAddress + } + api::OrderAddressInput::AccountAddress(id) => { + order_manager::OrderAddressInput::AccountAddress(id) + } + api::OrderAddressInput::Address(api::CreateOrderAddress { + name, + email, + street, + city, + country, + zip, + }) => order_manager::OrderAddressInput::Address(order_manager::CreateOrderAddress { + name, + email: email.clone(), + street, + city, + country, + zip, + }), + }; + let order: model::Order = query_order!( order, order_manager::CreateAccountOrder { account_id, - create_address: address.map( - |model::api::CreateOrderAddress { - name, - email, - street, - city, - country, - zip, - }| order_manager::CreateOrderAddress { - name, - email, - street, - city, - country, - zip, - }, - ), + order_address, }, PublicError::PlaceOrder, PublicError::DatabaseConnection )?; - Ok(HttpResponse::SeeOther() - .append_header(("Location", redirect_uri.as_str())) - .body(format!( - "Go to {redirect_uri}" - ))) + Ok(Json(api::PlaceOrderResult { + redirect_uri, + order_id: order.id, + })) } pub(crate) fn configure(config: &mut ServiceConfig) { diff --git a/migrations/20220528115140_add_phone_to_address.sql b/migrations/20220528115140_add_phone_to_address.sql new file mode 100644 index 0000000..24f7c74 --- /dev/null +++ b/migrations/20220528115140_add_phone_to_address.sql @@ -0,0 +1,2 @@ +ALTER TABLE account_addresses + ADD COLUMN phone text NOT NULL DEFAULT ''; diff --git a/shared/model/src/api.rs b/shared/model/src/api.rs index e1d9177..c8faaec 100644 --- a/shared/model/src/api.rs +++ b/shared/model/src/api.rs @@ -427,6 +427,13 @@ pub struct CreateAccountInput { pub password: Password, } +#[derive(Serialize, Deserialize, Debug)] +pub enum OrderAddressInput { + Address(CreateOrderAddress), + AccountAddress(AddressId), + DefaultAccountAddress, +} + #[derive(Serialize, Deserialize, Debug)] pub struct CreateOrderInput { /// Required customer e-mail @@ -444,7 +451,7 @@ pub struct CreateOrderInput { pub charge_client: bool, /// User currency pub currency: String, - pub address: Option, + pub address: OrderAddressInput, } #[derive(Serialize, Deserialize, Debug)] @@ -512,6 +519,12 @@ pub struct UpdateOrderAddress { pub zip: Zip, } +#[derive(Serialize, Deserialize, Debug)] +pub struct PlaceOrderResult { + pub redirect_uri: String, + pub order_id: OrderId, +} + pub mod admin { use serde::{Deserialize, Serialize}; diff --git a/web/src/api.rs b/web/src/api.rs index 54b0eb8..0dea044 100644 --- a/web/src/api.rs +++ b/web/src/api.rs @@ -6,6 +6,7 @@ use seed::fetch::{FetchError, Request}; pub mod admin; pub mod public; +#[must_use] #[derive(Debug)] pub enum NetRes { Success(S), diff --git a/web/src/api/public.rs b/web/src/api/public.rs index 20b00d7..7b8d14c 100644 --- a/web/src/api/public.rs +++ b/web/src/api/public.rs @@ -1,4 +1,5 @@ -use model::{AccessTokenString, RefreshTokenString}; +use model::api::OrderAddressInput; +use model::{AccessTokenString, AddressId, RefreshTokenString}; use seed::fetch::{Header, Method, Request}; use crate::api::perform; @@ -118,16 +119,28 @@ pub async fn update_cart( .await } -pub async fn place_order(access_token: AccessTokenString) -> NetRes { +pub async fn place_account_order( + access_token: AccessTokenString, + email: String, + phone: String, + first_name: String, + last_name: String, + language: String, + charge_client: bool, + currency: String, + address_id: Option, +) -> NetRes { let input = model::api::CreateOrderInput { - email: "".to_string(), - phone: "".to_string(), - first_name: "".to_string(), - last_name: "".to_string(), - language: "".to_string(), - charge_client: false, - currency: "".to_string(), - address: None, + email, + phone, + first_name, + last_name, + language, + charge_client, + currency, + address: address_id + .map(OrderAddressInput::AccountAddress) + .unwrap_or(OrderAddressInput::DefaultAccountAddress), }; perform( Request::new("/api/v1/order") diff --git a/web/src/i18n.rs b/web/src/i18n.rs index 9f750f2..00d0ef0 100644 --- a/web/src/i18n.rs +++ b/web/src/i18n.rs @@ -52,6 +52,10 @@ impl I18n { .map(Into::into) .unwrap_or_else(|| key.clone()) } + + pub fn current_language(&self) -> &str { + self.lang.as_str() + } } pub struct Scope<'store, 'lang> { diff --git a/web/src/lib.rs b/web/src/lib.rs index 258e06b..d21fd12 100644 --- a/web/src/lib.rs +++ b/web/src/lib.rs @@ -178,8 +178,7 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders) { pages::public::shopping_cart::update(msg, page, &mut orders.proxy(Into::into)) } Msg::Public(pages::public::PublicMsg::Checkout(msg)) => { - let page = fetch_page!(public model, Checkout); - pages::public::checkout::update(msg, page, &mut orders.proxy(Into::into)) + pages::public::checkout::update(msg, model, &mut orders.proxy(Into::into)) } // Admin Msg::Admin(pages::admin::Msg::Landing(msg)) => { diff --git a/web/src/model.rs b/web/src/model.rs index ac3f55c..f2cb62b 100644 --- a/web/src/model.rs +++ b/web/src/model.rs @@ -6,15 +6,24 @@ use crate::{shopping_cart, I18n, Page}; #[derive(Debug)] pub struct Model { + /// URL object for path constructor pub url: Url, + /// Access token pub token: Option, + /// Current SPA page pub page: Page, + /// Logo url form favicon href pub logo: Option, + /// Shared data pub shared: crate::shared::Model, + /// Translations pub i18n: I18n, + /// Shopping cart information pub cart: shopping_cart::ShoppingCart, + /// Application config pub config: Config, + /// Debug only modal #[cfg(debug_assertions)] pub debug_modal: bool, } diff --git a/web/src/pages/public/checkout.rs b/web/src/pages/public/checkout.rs index c3f920f..2d191c7 100644 --- a/web/src/pages/public/checkout.rs +++ b/web/src/pages/public/checkout.rs @@ -1,30 +1,37 @@ use std::str::FromStr; +use model::AccessTokenString; use seed::prelude::*; use seed::*; use crate::model::Products; -use crate::NetRes; +use crate::{fetch_page, NetRes}; #[derive(Debug)] pub enum CheckoutMsg { ProductsFetched(NetRes), - AddressNameChanged(String), + AddressFirstNameChanged(String), + AddressLastNameChanged(String), AddressEmailChanged(String), AddressStreetChanged(String), AddressCityChanged(String), AddressCountryChanged(String), AddressZipChanged(String), + AddressPhoneChanged(String), + PlaceOrder, + OrderPlaced(NetRes), } #[derive(Debug, Default)] pub struct AddressForm { - pub name: model::Name, - pub email: model::Email, + pub first_name: model::Name, + pub last_name: model::Name, pub street: model::Street, pub city: model::City, pub country: model::Country, pub zip: model::Zip, + pub email: model::Email, + pub phone: String, } #[derive(Debug)] @@ -47,10 +54,11 @@ pub fn init(_url: Url, orders: &mut impl Orders) -> CheckoutPage { pub fn page_changed(_url: Url, _model: &mut CheckoutPage) {} -pub fn update(msg: CheckoutMsg, model: &mut CheckoutPage, _orders: &mut impl Orders) { +pub fn update(msg: CheckoutMsg, model: &mut crate::Model, orders: &mut impl Orders) { match msg { CheckoutMsg::ProductsFetched(NetRes::Success(products)) => { - model.products.update(products.0); + let page = fetch_page!(public model, Checkout); + page.products.update(products.0); } CheckoutMsg::ProductsFetched(NetRes::Error(e)) => { seed::error!("fetch product error", e); @@ -58,26 +66,72 @@ pub fn update(msg: CheckoutMsg, model: &mut CheckoutPage, _orders: &mut impl Ord CheckoutMsg::ProductsFetched(NetRes::Http(e)) => { seed::error!("fetch product http", e); } - CheckoutMsg::AddressNameChanged(value) => { - model.address.name = model::Name::new(value); + CheckoutMsg::AddressFirstNameChanged(value) => { + let page = fetch_page!(public model, Checkout); + page.address.first_name = model::Name::new(value); + } + CheckoutMsg::AddressLastNameChanged(value) => { + let page = fetch_page!(public model, Checkout); + page.address.last_name = model::Name::new(value); } CheckoutMsg::AddressEmailChanged(value) => { if let Ok(value) = model::Email::from_str(&value) { - model.address.email = value; + let page = fetch_page!(public model, Checkout); + page.address.email = value; } } + CheckoutMsg::AddressPhoneChanged(phone) => { + let page = fetch_page!(public model, Checkout); + page.address.phone = phone; + } CheckoutMsg::AddressStreetChanged(value) => { - model.address.street = model::Street::new(value); + let page = fetch_page!(public model, Checkout); + page.address.street = model::Street::new(value); } CheckoutMsg::AddressCityChanged(value) => { - model.address.city = model::City::new(value); + let page = fetch_page!(public model, Checkout); + page.address.city = model::City::new(value); } CheckoutMsg::AddressCountryChanged(value) => { - model.address.country = model::Country::new(value); + let page = fetch_page!(public model, Checkout); + page.address.country = model::Country::new(value); } CheckoutMsg::AddressZipChanged(value) => { - model.address.zip = model::Zip::new(value); + let page = fetch_page!(public model, Checkout); + page.address.zip = model::Zip::new(value); } + CheckoutMsg::PlaceOrder => { + if let Some(access_token) = model.token.as_ref().cloned() { + let page = fetch_page!(public model, Checkout); + let email: String = String::from(page.address.email.as_str()); + let phone = page.address.phone.clone(); + + let first_name: String = String::from(page.address.first_name.as_str()); + let last_name: String = String::from(page.address.last_name.as_str()); + let language: String = model.i18n.current_language().to_string(); + let charge_client = false; + let currency = model.config.currency.name.to_string(); + let address_id = None; + + orders.perform_cmd(async move { + crate::api::public::place_account_order( + AccessTokenString::new(access_token), + email, + phone, + first_name, + last_name, + language, + charge_client, + currency, + address_id, + ) + .await + }); + } + } + CheckoutMsg::OrderPlaced(NetRes::Success(_o)) => {} + CheckoutMsg::OrderPlaced(NetRes::Error(_o)) => {} + CheckoutMsg::OrderPlaced(NetRes::Http(_o)) => {} } } @@ -223,7 +277,6 @@ mod right_side { use seed::*; use crate::pages::public::checkout::{CheckoutMsg, CheckoutPage}; - use crate::pages::public::sign_up::RegisterMsg; use crate::shopping_cart::CartMsg; use crate::Msg; @@ -262,10 +315,12 @@ mod right_side { div![C!["text-gray-600 font-semibold text-sm mb-2 ml-1"], model.i18n.t("Contact")], ], div![ - C!["mb-3"], - div![ - address_input(model, "client-name", "text", "Name", CheckoutMsg::AddressNameChanged), - ], + C!["mb-3 inline-block w-1/2 pr-1"], + address_input(model, "client-first-name", "text", "First name", CheckoutMsg::AddressFirstNameChanged), + ], + div![ + C!["mb-3 inline-block -mx-1 pl-1 w-1/2"], + address_input(model, "client-last-name", "text", "Last name", CheckoutMsg::AddressLastNameChanged), ], div![ C!["mb-3"], @@ -273,6 +328,12 @@ mod right_side { address_input(model, "client-email", "email", "E-Mail", CheckoutMsg::AddressEmailChanged), ], ], + div![ + C!["mb-3"], + div![ + address_input(model, "client-phone", "phone", "Phone number", CheckoutMsg::AddressPhoneChanged), + ], + ], div![ C!["mb-3"], div![C!["text-gray-600 font-semibold text-sm mb-2 ml-1"], model.i18n.t("Address")],