Add phone number and address handling

This commit is contained in:
eraden 2022-05-28 14:03:14 +02:00
parent c1c97061eb
commit 7617cb1064
13 changed files with 253 additions and 80 deletions

View File

@ -37,6 +37,41 @@ WHERE account_id = $1
.await .await
.map_err(|_| Error::AccountAddresses.into()) .map_err(|_| Error::AccountAddresses.into())
} }
////
#[derive(actix::Message)]
#[rtype(result = "Result<model::AccountAddress>")]
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<model::AccountAddress> {
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)] #[derive(actix::Message)]
#[rtype(result = "Result<model::AccountAddress>")] #[rtype(result = "Result<model::AccountAddress>")]

View File

@ -84,11 +84,18 @@ pub struct CreateOrderAddress {
pub zip: model::Zip, pub zip: model::Zip,
} }
#[derive(Debug)]
pub enum OrderAddressInput {
Address(CreateOrderAddress),
AccountAddress(model::AddressId),
DefaultAccountAddress,
}
#[derive(Message, Debug)] #[derive(Message, Debug)]
#[rtype(result = "Result<Order>")] #[rtype(result = "Result<Order>")]
pub struct CreateAccountOrder { pub struct CreateAccountOrder {
pub account_id: AccountId, pub account_id: AccountId,
pub create_address: Option<CreateOrderAddress>, pub order_address: OrderAddressInput,
} }
order_async_handler!(CreateAccountOrder, create_account_order, Order); order_async_handler!(CreateAccountOrder, create_account_order, Order);
@ -115,7 +122,9 @@ pub(crate) async fn create_account_order(
Error::ShoppingCart, Error::ShoppingCart,
Error::DatabaseInternal Error::DatabaseInternal
); );
let address: model::AccountAddress = if let Some(input) = msg.create_address {
let address: model::AccountAddress = match msg.order_address {
OrderAddressInput::Address(input) => {
query_db!( query_db!(
db, db,
database_manager::CreateAccountAddress { database_manager::CreateAccountAddress {
@ -125,12 +134,23 @@ pub(crate) async fn create_account_order(
city: input.city, city: input.city,
country: input.country, country: input.country,
zip: input.zip, zip: input.zip,
account_id: None, account_id: Some(cart.buyer_id),
is_default: true, is_default: true,
}, },
Error::InvalidAccountAddress Error::InvalidAccountAddress
) )
} else { }
OrderAddressInput::AccountAddress(address_id) => {
query_db!(
db,
database_manager::FindAccountAddress {
address_id,
account_id: cart.buyer_id
},
Error::NoAddress
)
}
OrderAddressInput::DefaultAccountAddress => {
query_db!( query_db!(
db, db,
database_manager::DefaultAccountAddress { database_manager::DefaultAccountAddress {
@ -138,6 +158,7 @@ pub(crate) async fn create_account_order(
}, },
Error::NoAddress Error::NoAddress
) )
}
}; };
query_db!( query_db!(

View File

@ -51,6 +51,12 @@ pub enum Error {
Token(token_manager::Error), Token(token_manager::Error),
} }
impl From<public::api_v1::Error> for Error {
fn from(e: public::api_v1::Error) -> Self {
Self::Public(public::Error::ApiV1(e))
}
}
impl From<V1ShoppingCartError> for Error { impl From<V1ShoppingCartError> for Error {
fn from(sv1: V1ShoppingCartError) -> Self { fn from(sv1: V1ShoppingCartError) -> Self {
Self::Public(PublicError::ApiV1(V1Error::ShoppingCart(sv1))) Self::Public(PublicError::ApiV1(V1Error::ShoppingCart(sv1)))

View File

@ -21,6 +21,9 @@ pub enum Error {
#[error("Failed to create order")] #[error("Failed to create order")]
AddOrder, AddOrder,
#[error("Can't place order. Client IP is unknown")]
NoIp,
} }
pub(crate) fn configure(config: &mut ServiceConfig) { pub(crate) fn configure(config: &mut ServiceConfig) {

View File

@ -236,7 +236,7 @@ pub(crate) async fn create_order(
credentials: BearerAuth, credentials: BearerAuth,
payment: Data<Addr<PaymentManager>>, payment: Data<Addr<PaymentManager>>,
order: Data<Addr<OrderManager>>, order: Data<Addr<OrderManager>>,
) -> routes::Result<HttpResponse> { ) -> routes::Result<Json<api::PlaceOrderResult>> {
let account_id = credentials let account_id = credentials
.require_user(tm.into_inner()) .require_user(tm.into_inner())
.await? .await?
@ -254,7 +254,7 @@ pub(crate) async fn create_order(
} = payload; } = payload;
let ip = match req.peer_addr() { let ip = match req.peer_addr() {
Some(ip) => ip, Some(ip) => ip,
_ => return Ok(HttpResponse::BadRequest().body("No IP")), _ => return Err(super::Error::NoIp.into()),
}; };
let payment_manager::CreatePaymentResult { redirect_uri, .. } = query_pay!( let payment_manager::CreatePaymentResult { redirect_uri, .. } = query_pay!(
@ -275,37 +275,43 @@ pub(crate) async fn create_order(
routes::Error::Public(PublicError::DatabaseConnection) 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,
order_manager::CreateAccountOrder { order_manager::CreateAccountOrder {
account_id, account_id,
create_address: address.map( order_address,
|model::api::CreateOrderAddress {
name,
email,
street,
city,
country,
zip,
}| order_manager::CreateOrderAddress {
name,
email,
street,
city,
country,
zip,
},
),
}, },
PublicError::PlaceOrder, PublicError::PlaceOrder,
PublicError::DatabaseConnection PublicError::DatabaseConnection
)?; )?;
Ok(HttpResponse::SeeOther() Ok(Json(api::PlaceOrderResult {
.append_header(("Location", redirect_uri.as_str())) redirect_uri,
.body(format!( order_id: order.id,
"<a href=\"{redirect_uri}\">Go to {redirect_uri}</a>" }))
)))
} }
pub(crate) fn configure(config: &mut ServiceConfig) { pub(crate) fn configure(config: &mut ServiceConfig) {

View File

@ -0,0 +1,2 @@
ALTER TABLE account_addresses
ADD COLUMN phone text NOT NULL DEFAULT '';

View File

@ -427,6 +427,13 @@ pub struct CreateAccountInput {
pub password: Password, pub password: Password,
} }
#[derive(Serialize, Deserialize, Debug)]
pub enum OrderAddressInput {
Address(CreateOrderAddress),
AccountAddress(AddressId),
DefaultAccountAddress,
}
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct CreateOrderInput { pub struct CreateOrderInput {
/// Required customer e-mail /// Required customer e-mail
@ -444,7 +451,7 @@ pub struct CreateOrderInput {
pub charge_client: bool, pub charge_client: bool,
/// User currency /// User currency
pub currency: String, pub currency: String,
pub address: Option<CreateOrderAddress>, pub address: OrderAddressInput,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -512,6 +519,12 @@ pub struct UpdateOrderAddress {
pub zip: Zip, pub zip: Zip,
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct PlaceOrderResult {
pub redirect_uri: String,
pub order_id: OrderId,
}
pub mod admin { pub mod admin {
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@ -6,6 +6,7 @@ use seed::fetch::{FetchError, Request};
pub mod admin; pub mod admin;
pub mod public; pub mod public;
#[must_use]
#[derive(Debug)] #[derive(Debug)]
pub enum NetRes<S> { pub enum NetRes<S> {
Success(S), Success(S),

View File

@ -1,4 +1,5 @@
use model::{AccessTokenString, RefreshTokenString}; use model::api::OrderAddressInput;
use model::{AccessTokenString, AddressId, RefreshTokenString};
use seed::fetch::{Header, Method, Request}; use seed::fetch::{Header, Method, Request};
use crate::api::perform; use crate::api::perform;
@ -118,16 +119,28 @@ pub async fn update_cart(
.await .await
} }
pub async fn place_order(access_token: AccessTokenString) -> NetRes<String> { 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<AddressId>,
) -> NetRes<model::api::PlaceOrderResult> {
let input = model::api::CreateOrderInput { let input = model::api::CreateOrderInput {
email: "".to_string(), email,
phone: "".to_string(), phone,
first_name: "".to_string(), first_name,
last_name: "".to_string(), last_name,
language: "".to_string(), language,
charge_client: false, charge_client,
currency: "".to_string(), currency,
address: None, address: address_id
.map(OrderAddressInput::AccountAddress)
.unwrap_or(OrderAddressInput::DefaultAccountAddress),
}; };
perform( perform(
Request::new("/api/v1/order") Request::new("/api/v1/order")

View File

@ -52,6 +52,10 @@ impl I18n {
.map(Into::into) .map(Into::into)
.unwrap_or_else(|| key.clone()) .unwrap_or_else(|| key.clone())
} }
pub fn current_language(&self) -> &str {
self.lang.as_str()
}
} }
pub struct Scope<'store, 'lang> { pub struct Scope<'store, 'lang> {

View File

@ -178,8 +178,7 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
pages::public::shopping_cart::update(msg, page, &mut orders.proxy(Into::into)) pages::public::shopping_cart::update(msg, page, &mut orders.proxy(Into::into))
} }
Msg::Public(pages::public::PublicMsg::Checkout(msg)) => { Msg::Public(pages::public::PublicMsg::Checkout(msg)) => {
let page = fetch_page!(public model, Checkout); pages::public::checkout::update(msg, model, &mut orders.proxy(Into::into))
pages::public::checkout::update(msg, page, &mut orders.proxy(Into::into))
} }
// Admin // Admin
Msg::Admin(pages::admin::Msg::Landing(msg)) => { Msg::Admin(pages::admin::Msg::Landing(msg)) => {

View File

@ -6,15 +6,24 @@ use crate::{shopping_cart, I18n, Page};
#[derive(Debug)] #[derive(Debug)]
pub struct Model { pub struct Model {
/// URL object for path constructor
pub url: Url, pub url: Url,
/// Access token
pub token: Option<String>, pub token: Option<String>,
/// Current SPA page
pub page: Page, pub page: Page,
/// Logo url form favicon href
pub logo: Option<String>, pub logo: Option<String>,
/// Shared data
pub shared: crate::shared::Model, pub shared: crate::shared::Model,
/// Translations
pub i18n: I18n, pub i18n: I18n,
/// Shopping cart information
pub cart: shopping_cart::ShoppingCart, pub cart: shopping_cart::ShoppingCart,
/// Application config
pub config: Config, pub config: Config,
/// Debug only modal
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
pub debug_modal: bool, pub debug_modal: bool,
} }

View File

@ -1,30 +1,37 @@
use std::str::FromStr; use std::str::FromStr;
use model::AccessTokenString;
use seed::prelude::*; use seed::prelude::*;
use seed::*; use seed::*;
use crate::model::Products; use crate::model::Products;
use crate::NetRes; use crate::{fetch_page, NetRes};
#[derive(Debug)] #[derive(Debug)]
pub enum CheckoutMsg { pub enum CheckoutMsg {
ProductsFetched(NetRes<model::api::Products>), ProductsFetched(NetRes<model::api::Products>),
AddressNameChanged(String), AddressFirstNameChanged(String),
AddressLastNameChanged(String),
AddressEmailChanged(String), AddressEmailChanged(String),
AddressStreetChanged(String), AddressStreetChanged(String),
AddressCityChanged(String), AddressCityChanged(String),
AddressCountryChanged(String), AddressCountryChanged(String),
AddressZipChanged(String), AddressZipChanged(String),
AddressPhoneChanged(String),
PlaceOrder,
OrderPlaced(NetRes<model::api::PlaceOrderResult>),
} }
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct AddressForm { pub struct AddressForm {
pub name: model::Name, pub first_name: model::Name,
pub email: model::Email, pub last_name: model::Name,
pub street: model::Street, pub street: model::Street,
pub city: model::City, pub city: model::City,
pub country: model::Country, pub country: model::Country,
pub zip: model::Zip, pub zip: model::Zip,
pub email: model::Email,
pub phone: String,
} }
#[derive(Debug)] #[derive(Debug)]
@ -47,10 +54,11 @@ pub fn init(_url: Url, orders: &mut impl Orders<crate::Msg>) -> CheckoutPage {
pub fn page_changed(_url: Url, _model: &mut CheckoutPage) {} pub fn page_changed(_url: Url, _model: &mut CheckoutPage) {}
pub fn update(msg: CheckoutMsg, model: &mut CheckoutPage, _orders: &mut impl Orders<crate::Msg>) { pub fn update(msg: CheckoutMsg, model: &mut crate::Model, orders: &mut impl Orders<crate::Msg>) {
match msg { match msg {
CheckoutMsg::ProductsFetched(NetRes::Success(products)) => { 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)) => { CheckoutMsg::ProductsFetched(NetRes::Error(e)) => {
seed::error!("fetch product 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)) => { CheckoutMsg::ProductsFetched(NetRes::Http(e)) => {
seed::error!("fetch product http", e); seed::error!("fetch product http", e);
} }
CheckoutMsg::AddressNameChanged(value) => { CheckoutMsg::AddressFirstNameChanged(value) => {
model.address.name = model::Name::new(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) => { CheckoutMsg::AddressEmailChanged(value) => {
if let Ok(value) = model::Email::from_str(&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) => { 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) => { 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) => { 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) => { 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 seed::*;
use crate::pages::public::checkout::{CheckoutMsg, CheckoutPage}; use crate::pages::public::checkout::{CheckoutMsg, CheckoutPage};
use crate::pages::public::sign_up::RegisterMsg;
use crate::shopping_cart::CartMsg; use crate::shopping_cart::CartMsg;
use crate::Msg; 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!["text-gray-600 font-semibold text-sm mb-2 ml-1"], model.i18n.t("Contact")],
], ],
div![ div![
C!["mb-3"], C!["mb-3 inline-block w-1/2 pr-1"],
div![ address_input(model, "client-first-name", "text", "First name", CheckoutMsg::AddressFirstNameChanged),
address_input(model, "client-name", "text", "Name", CheckoutMsg::AddressNameChanged),
], ],
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![ div![
C!["mb-3"], C!["mb-3"],
@ -273,6 +328,12 @@ mod right_side {
address_input(model, "client-email", "email", "E-Mail", CheckoutMsg::AddressEmailChanged), 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![ div![
C!["mb-3"], C!["mb-3"],
div![C!["text-gray-600 font-semibold text-sm mb-2 ml-1"], model.i18n.t("Address")], div![C!["text-gray-600 font-semibold text-sm mb-2 ml-1"], model.i18n.t("Address")],