Add addresses

This commit is contained in:
Adrian Woźniak 2022-05-19 16:13:27 +02:00
parent cafafb0f24
commit 0a3bc50d6c
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
21 changed files with 565 additions and 105 deletions

16
Cargo.lock generated
View File

@ -2,6 +2,22 @@
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 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]] [[package]]
name = "actix" name = "actix"
version = "0.12.0" version = "0.12.0"

View File

@ -11,6 +11,7 @@ members = [
"api", "api",
"web", "web",
"shared/model", "shared/model",
"actors/account_manager",
"actors/cart_manager", "actors/cart_manager",
"actors/database_manager", "actors/database_manager",
"actors/email_manager", "actors/email_manager",

View File

@ -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 = [] }

View File

@ -0,0 +1,10 @@
#[derive(Debug)]
pub struct AccountManager {
db: actix::Addr<database_manager::Database>,
}
impl AccountManager {
pub fn new(db: actix::Addr<database_manager::Database>) -> Self {
Self { db }
}
}

View File

@ -3,10 +3,7 @@
use std::collections::HashSet; use std::collections::HashSet;
use database_manager::{query_db, Database}; use database_manager::{query_db, Database};
use model::{ use model::{PaymentMethod, ShoppingCartId};
AccountId, ProductId, Quantity, QuantityUnit, ShoppingCartId, ShoppingCartItem,
ShoppingCartItemId, ShoppingCartState,
};
#[macro_export] #[macro_export]
macro_rules! cart_async_handler { macro_rules! cart_async_handler {
@ -75,7 +72,7 @@ pub enum Error {
#[error("Failed to change quantity")] #[error("Failed to change quantity")]
ChangeQuantity, ChangeQuantity,
#[error("Shopping cart item {0} does not exists")] #[error("Shopping cart item {0} does not exists")]
NotExists(ShoppingCartItemId), NotExists(model::ShoppingCartItemId),
} }
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
@ -95,20 +92,20 @@ impl CartManager {
} }
#[derive(actix::Message, Debug)] #[derive(actix::Message, Debug)]
#[rtype(result = "Result<Option<ShoppingCartItem>>")] #[rtype(result = "Result<Option<model::ShoppingCartItem>>")]
pub struct ModifyItem { pub struct ModifyItem {
pub buyer_id: AccountId, pub buyer_id: model::AccountId,
pub product_id: ProductId, pub product_id: model::ProductId,
pub quantity: Quantity, pub quantity: model::Quantity,
pub quantity_unit: QuantityUnit, pub quantity_unit: model::QuantityUnit,
} }
cart_async_handler!(ModifyItem, modify_item, Option<ShoppingCartItem>); cart_async_handler!(ModifyItem, modify_item, Option<model::ShoppingCartItem>);
async fn modify_item( async fn modify_item(
msg: ModifyItem, msg: ModifyItem,
db: actix::Addr<Database>, db: actix::Addr<Database>,
) -> Result<Option<ShoppingCartItem>> { ) -> Result<Option<model::ShoppingCartItem>> {
let _cart = query_db!( let _cart = query_db!(
db, db,
database_manager::EnsureActiveShoppingCart { database_manager::EnsureActiveShoppingCart {
@ -120,7 +117,7 @@ async fn modify_item(
db, db,
database_manager::AccountShoppingCarts { database_manager::AccountShoppingCarts {
account_id: msg.buyer_id, account_id: msg.buyer_id,
state: Some(ShoppingCartState::Active), state: Some(model::ShoppingCartState::Active),
}, },
passthrough Error::Db, passthrough Error::Db,
Error::CartNotAvailable Error::CartNotAvailable
@ -173,18 +170,22 @@ async fn modify_item(
} }
#[derive(actix::Message)] #[derive(actix::Message)]
#[rtype(result = "Result<Option<ShoppingCartItem>>")] #[rtype(result = "Result<Option<model::ShoppingCartItem>>")]
pub struct RemoveProduct { pub struct RemoveProduct {
pub shopping_cart_id: ShoppingCartId, pub shopping_cart_id: model::ShoppingCartId,
pub shopping_cart_item_id: ShoppingCartItemId, pub shopping_cart_item_id: model::ShoppingCartItemId,
} }
cart_async_handler!(RemoveProduct, remove_product, Option<ShoppingCartItem>); cart_async_handler!(
RemoveProduct,
remove_product,
Option<model::ShoppingCartItem>
);
pub(crate) async fn remove_product( pub(crate) async fn remove_product(
msg: RemoveProduct, msg: RemoveProduct,
db: actix::Addr<Database>, db: actix::Addr<Database>,
) -> Result<Option<ShoppingCartItem>> { ) -> Result<Option<model::ShoppingCartItem>> {
Ok(query_db!( Ok(query_db!(
db, db,
database_manager::RemoveCartItem { database_manager::RemoveCartItem {
@ -196,39 +197,49 @@ pub(crate) async fn remove_product(
)) ))
} }
#[derive(actix::Message, Debug)] pub struct ModifyCartResult {
#[rtype(result = "Result<Vec<ShoppingCartItem>>")] pub cart_id: ShoppingCartId,
pub struct ModifyCart { pub items: Vec<model::ShoppingCartItem>,
pub buyer_id: AccountId, pub checkout_notes: String,
pub items: Vec<ModifyItem>, pub payment_method: model::PaymentMethod,
} }
cart_async_handler!(ModifyCart, modify_cart, Vec<ShoppingCartItem>); #[derive(actix::Message, Debug)]
#[rtype(result = "Result<ModifyCartResult>")]
pub struct ModifyCart {
pub buyer_id: model::AccountId,
pub items: Vec<ModifyItem>,
pub checkout_notes: String,
pub payment_method: Option<PaymentMethod>,
}
async fn modify_cart(msg: ModifyCart, db: actix::Addr<Database>) -> Result<Vec<ShoppingCartItem>> { cart_async_handler!(ModifyCart, modify_cart, ModifyCartResult);
async fn modify_cart(msg: ModifyCart, db: actix::Addr<Database>) -> Result<ModifyCartResult> {
log::debug!("{:?}", msg); log::debug!("{:?}", msg);
let _cart = query_db!( let cart: model::ShoppingCart = query_db!(
db, db,
database_manager::EnsureActiveShoppingCart { database_manager::EnsureActiveShoppingCart {
buyer_id: msg.buyer_id, buyer_id: msg.buyer_id,
}, },
Error::ShoppingCartFailed Error::ShoppingCartFailed
); );
let mut carts: Vec<model::ShoppingCart> = query_db!( let cart: model::ShoppingCart = query_db!(
db, db,
database_manager::AccountShoppingCarts { database_manager::UpdateShoppingCart {
account_id: msg.buyer_id, id: cart.id,
state: Some(ShoppingCartState::Active), 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, passthrough Error::Db,
Error::CartNotAvailable Error::CartNotAvailable
); );
log::debug!("carts {:?}", carts);
let cart = if carts.is_empty() {
return Err(Error::CartNotAvailable);
} else {
carts.remove(0)
};
let existing = let existing =
msg.items msg.items
@ -269,5 +280,10 @@ async fn modify_cart(msg: ModifyCart, db: actix::Addr<Database>) -> Result<Vec<S
} }
} }
Ok(out) Ok(ModifyCartResult {
cart_id: cart.id,
items: out,
checkout_notes: cart.checkout_notes.unwrap_or_default(),
payment_method: cart.payment_method,
})
} }

View File

@ -0,0 +1,126 @@
use crate::{db_async_handler, Result};
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("Can't load account addresses")]
AccountAddresses,
#[error("Failed to save account address")]
CreateAccountAddress,
}
#[derive(actix::Message)]
#[rtype(result = "Result<Vec<model::Address>>")]
pub struct AccountAddresses {
pub account_id: model::AccountId,
}
db_async_handler!(
AccountAddresses,
account_addresses,
Vec<model::Address>,
inner_account_addresses
);
pub(crate) async fn account_addresses(
msg: AccountAddresses,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<model::Address>> {
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<model::Address>")]
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<model::Address> {
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<model::Address>")]
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<model::Address> {
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())
}

View File

@ -5,6 +5,7 @@ use sqlx_core::arguments::Arguments;
pub use crate::account_orders::*; pub use crate::account_orders::*;
pub use crate::accounts::*; pub use crate::accounts::*;
pub use crate::addresses::*;
pub use crate::order_items::*; pub use crate::order_items::*;
pub use crate::photos::*; pub use crate::photos::*;
pub use crate::product_photos::*; pub use crate::product_photos::*;
@ -16,6 +17,7 @@ pub use crate::tokens::*;
pub mod account_orders; pub mod account_orders;
pub mod accounts; pub mod accounts;
pub mod addresses;
pub mod order_items; pub mod order_items;
pub mod photos; pub mod photos;
pub mod product_photos; pub mod product_photos;
@ -138,6 +140,8 @@ pub enum Error {
Photo(#[from] photos::Error), Photo(#[from] photos::Error),
#[error("{0}")] #[error("{0}")]
ProductPhoto(#[from] product_photos::Error), ProductPhoto(#[from] product_photos::Error),
#[error("{0}")]
AccountAddress(#[from] addresses::Error),
} }
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;

View File

@ -38,7 +38,7 @@ ORDER BY id DESC
.await .await
.map_err(|e| { .map_err(|e| {
log::error!("{e:?}"); log::error!("{e:?}");
super::Error::OrderItem(Error::All) super::Error::from(Error::All)
}) })
} }

View File

@ -124,6 +124,7 @@ pub struct UpdateShoppingCart {
pub buyer_id: AccountId, pub buyer_id: AccountId,
pub payment_method: PaymentMethod, pub payment_method: PaymentMethod,
pub state: ShoppingCartState, pub state: ShoppingCartState,
pub checkout_notes: Option<String>,
} }
db_async_handler!(UpdateShoppingCart, update_shopping_cart, ShoppingCart); db_async_handler!(UpdateShoppingCart, update_shopping_cart, ShoppingCart);
@ -135,7 +136,7 @@ pub(crate) async fn update_shopping_cart(
sqlx::query_as( sqlx::query_as(
r#" r#"
UPDATE shopping_carts 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 WHERE id = $1
RETURNING id, buyer_id, payment_method, state, checkout_notes 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.buyer_id)
.bind(msg.payment_method) .bind(msg.payment_method)
.bind(msg.state) .bind(msg.state)
.bind(msg.checkout_notes)
.fetch_one(&db) .fetch_one(&db)
.await .await
.map_err(|e| { .map_err(|e| {

View File

@ -4,7 +4,7 @@ use actix_web::{get, patch, post, HttpResponse};
use actix_web_httpauth::extractors::bearer::BearerAuth; use actix_web_httpauth::extractors::bearer::BearerAuth;
use config::SharedAppConfig; use config::SharedAppConfig;
use database_manager::Database; use database_manager::Database;
use model::{AccountId, AccountState, Encrypt, PasswordConfirmation}; use model::{AccountId, AccountState, Address, Encrypt, PasswordConfirmation};
use token_manager::TokenManager; use token_manager::TokenManager;
use crate::routes::admin::Error; use crate::routes::admin::Error;
@ -112,9 +112,16 @@ pub async fn create_account(
role: payload.role, role: payload.role,
} }
); );
let addresses: Vec<Address> = admin_send_db!(
db,
database_manager::AccountAddresses {
account_id: account.id
}
);
Ok(Json(model::api::admin::RegisterResponse { Ok(Json(model::api::admin::RegisterResponse {
errors: vec![], errors: vec![],
account: Some(account.into()), account: Some((account, addresses).into()),
})) }))
} }

View File

@ -142,18 +142,23 @@ async fn update_cart(
) )
.collect(); .collect();
let items: Vec<model::ShoppingCartItem> = query_cart!( let res: cart_manager::ModifyCartResult = query_cart!(
cart, cart,
cart_manager::ModifyCart { cart_manager::ModifyCart {
buyer_id: token.account_id(), 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(super::Error::ModifyItem.into()),
routes::Error::Public(PublicError::DatabaseConnection) routes::Error::Public(PublicError::DatabaseConnection)
); );
Ok(Json(api::UpdateCartOutput { 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<Addr<Database>>, db: Data<Addr<Database>>,
tm: Data<Addr<TokenManager>>, tm: Data<Addr<TokenManager>>,
credentials: BearerAuth, credentials: BearerAuth,
) -> routes::Result<Json<model::Account>> { ) -> routes::Result<Json<model::api::Account>> {
let account_id: model::AccountId = credentials let account_id: model::AccountId = credentials
.require_user(tm.into_inner()) .require_user(tm.into_inner())
.await? .await?
.account_id(); .account_id();
let account: model::FullAccount = let account: model::FullAccount =
public_send_db!(owned, db, database_manager::FindAccount { account_id }); 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")] #[post("/order")]

View File

@ -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)
);

View File

@ -20,6 +20,82 @@ pub struct Config {
pub shipping_methods: Vec<ShippingMethod>, pub shipping_methods: Vec<ShippingMethod>,
} }
#[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<Address>,
}
impl From<(FullAccount, Vec<crate::Address>)> for Account {
fn from(
(
FullAccount {
id,
email,
login,
pass_hash: _,
role,
customer_id,
state,
},
addresses,
): (FullAccount, Vec<crate::Address>),
) -> 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<crate::Address> 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))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
#[serde(transparent)] #[serde(transparent)]
@ -388,11 +464,16 @@ pub struct UpdateItemOutput {
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct UpdateCartInput { pub struct UpdateCartInput {
pub items: Vec<UpdateItemInput>, pub items: Vec<UpdateItemInput>,
pub notes: String,
pub payment_method: Option<PaymentMethod>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct UpdateCartOutput { pub struct UpdateCartOutput {
pub cart_id: ShoppingCartId,
pub items: Vec<ShoppingCartItem>, pub items: Vec<ShoppingCartItem>,
pub checkout_notes: String,
pub payment_method: PaymentMethod,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -419,7 +500,7 @@ pub mod admin {
#[derive(Serialize, Deserialize, Debug, Default)] #[derive(Serialize, Deserialize, Debug, Default)]
pub struct RegisterResponse { pub struct RegisterResponse {
pub errors: Vec<RegisterError>, pub errors: Vec<RegisterError>,
pub account: Option<Account>, pub account: Option<super::Account>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]

View File

@ -82,3 +82,39 @@ impl<T> fake::Dummy<T> for NonNegative {
Self(price) Self(price)
} }
} }
impl<T> fake::Dummy<T> for crate::Name {
fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
use fake::faker::name::raw::*;
use fake::locales::*;
Self(Name(EN).fake())
}
}
impl<T> fake::Dummy<T> for crate::Street {
fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
use fake::faker::address::raw::*;
use fake::locales::*;
Self(StreetName(EN).fake())
}
}
impl<T> fake::Dummy<T> for crate::City {
fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
use fake::faker::address::raw::*;
use fake::locales::*;
Self(CityName(EN).fake())
}
}
impl<T> fake::Dummy<T> for crate::Country {
fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
use fake::faker::address::raw::*;
use fake::locales::*;
Self(CountryName(EN).fake())
}
}
impl<T> fake::Dummy<T> for crate::Zip {
fn dummy_with_rng<R: Rng + ?Sized>(_config: &T, _rng: &mut R) -> Self {
use fake::faker::address::raw::*;
use fake::locales::*;
Self(ZipCode(EN).fake())
}
}

View File

@ -208,13 +208,19 @@ impl QuantityUnit {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] #[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")] #[serde(rename_all = "snake_case")]
pub enum PaymentMethod { pub enum PaymentMethod {
PayU, PayU,
PaymentOnTheSpot, PaymentOnTheSpot,
} }
impl Default for PaymentMethod {
fn default() -> Self {
Self::PaymentOnTheSpot
}
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))] #[cfg_attr(feature = "db", sqlx(rename_all = "snake_case"))]
@ -1113,3 +1119,83 @@ pub enum ShippingMethod {
/// Shop owner will ship product manually /// Shop owner will ship product manually
Manual, 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: Into<String>>(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: Into<String>>(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: Into<String>>(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: Into<String>>(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: Into<String>>(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,
}

View File

@ -4,19 +4,19 @@ use seed::fetch::{Header, Method, Request};
use crate::api::perform; use crate::api::perform;
use crate::NetRes; use crate::NetRes;
pub async fn config() -> super::NetRes<model::api::Config> { pub async fn config() -> NetRes<model::api::Config> {
perform(Request::new("/config").method(Method::Get)).await perform(Request::new("/config").method(Method::Get)).await
} }
pub async fn fetch_products() -> super::NetRes<model::api::Products> { pub async fn fetch_products() -> NetRes<model::api::Products> {
perform(Request::new("/api/v1/products").method(Method::Get)).await perform(Request::new("/api/v1/products").method(Method::Get)).await
} }
pub async fn fetch_product(product_id: model::ProductId) -> super::NetRes<model::api::Product> { pub async fn fetch_product(product_id: model::ProductId) -> NetRes<model::api::Product> {
perform(Request::new(format!("/api/v1/product/{}", product_id)).method(Method::Get)).await perform(Request::new(format!("/api/v1/product/{}", product_id)).method(Method::Get)).await
} }
pub async fn fetch_me(access_token: AccessTokenString) -> super::NetRes<model::Account> { pub async fn fetch_me(access_token: AccessTokenString) -> NetRes<model::api::Account> {
perform( perform(
Request::new("/api/v1/me") Request::new("/api/v1/me")
.header(Header::bearer(access_token.as_str())) .header(Header::bearer(access_token.as_str()))
@ -25,7 +25,7 @@ pub async fn fetch_me(access_token: AccessTokenString) -> super::NetRes<model::A
.await .await
} }
pub async fn sign_in(input: model::api::SignInInput) -> super::NetRes<model::api::SessionOutput> { pub async fn sign_in(input: model::api::SignInInput) -> NetRes<model::api::SessionOutput> {
perform( perform(
Request::new("/api/v1/sign-in") Request::new("/api/v1/sign-in")
.method(Method::Post) .method(Method::Post)
@ -35,7 +35,7 @@ pub async fn sign_in(input: model::api::SignInInput) -> super::NetRes<model::api
.await .await
} }
pub async fn verify_token(access_token: AccessTokenString) -> super::NetRes<String> { pub async fn verify_token(access_token: AccessTokenString) -> NetRes<String> {
perform( perform(
Request::new("/api/v1/token/verify") Request::new("/api/v1/token/verify")
.method(Method::Post) .method(Method::Post)
@ -44,9 +44,7 @@ pub async fn verify_token(access_token: AccessTokenString) -> super::NetRes<Stri
.await .await
} }
pub async fn refresh_token( pub async fn refresh_token(access_token: RefreshTokenString) -> NetRes<model::api::SessionOutput> {
access_token: RefreshTokenString,
) -> super::NetRes<model::api::SessionOutput> {
perform( perform(
Request::new("/api/v1/token/refresh") Request::new("/api/v1/token/refresh")
.method(Method::Post) .method(Method::Post)
@ -55,14 +53,12 @@ pub async fn refresh_token(
.await .await
} }
pub async fn sign_up( pub async fn sign_up(input: model::api::CreateAccountInput) -> NetRes<model::api::SessionOutput> {
input: model::api::CreateAccountInput,
) -> super::NetRes<model::api::SessionOutput> {
perform( perform(
Request::new("/api/v1/register") Request::new("/api/v1/register")
.method(Method::Post) .method(Method::Post)
.json(&input) .json(&input)
.map_err(crate::api::NetRes::Http)?, .map_err(NetRes::Http)?,
) )
.await .await
} }
@ -83,7 +79,7 @@ pub async fn update_cart_item(
.method(Method::Put) .method(Method::Put)
.header(Header::bearer(access_token.as_str())) .header(Header::bearer(access_token.as_str()))
.json(&input) .json(&input)
.map_err(crate::api::NetRes::Http)?, .map_err(NetRes::Http)?,
) )
.await .await
} }
@ -91,8 +87,11 @@ pub async fn update_cart_item(
pub async fn update_cart( pub async fn update_cart(
access_token: AccessTokenString, access_token: AccessTokenString,
items: Vec<crate::shopping_cart::Item>, items: Vec<crate::shopping_cart::Item>,
notes: String,
payment_method: Option<model::PaymentMethod>,
) -> NetRes<model::api::UpdateCartOutput> { ) -> NetRes<model::api::UpdateCartOutput> {
let input = model::api::UpdateCartInput { let input = model::api::UpdateCartInput {
notes,
items: items items: items
.into_iter() .into_iter()
.map( .map(
@ -107,13 +106,14 @@ pub async fn update_cart(
}, },
) )
.collect(), .collect(),
payment_method,
}; };
perform( perform(
Request::new("/api/v1/shopping-cart") Request::new("/api/v1/shopping-cart")
.method(Method::Put) .method(Method::Put)
.header(Header::bearer(access_token.as_str())) .header(Header::bearer(access_token.as_str()))
.json(&input) .json(&input)
.map_err(crate::api::NetRes::Http)?, .map_err(NetRes::Http)?,
) )
.await .await
} }

View File

@ -183,6 +183,7 @@ mod right_side {
use seed::*; use seed::*;
use crate::pages::public::checkout::CheckoutPage; use crate::pages::public::checkout::CheckoutPage;
use crate::shopping_cart::CartMsg;
use crate::Msg; use crate::Msg;
static SELECTED_PAYMENT_METHOD: &str = "payment-selected-method"; static SELECTED_PAYMENT_METHOD: &str = "payment-selected-method";
@ -291,22 +292,13 @@ mod right_side {
} }
fn pay_u(model: &crate::Model) -> Node<Msg> { fn pay_u(model: &crate::Model) -> Node<Msg> {
div![ payment_input(
C!["w-full p-3 border-b border-gray-200"], model,
label![ model::PaymentMethod::PayU,
C!["flex items-center cursor-pointer"], "pay_u",
attrs![At::For => "pay_u"], "PayU",
input![ pay_u_icon(),
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")]
]
],
]
} }
fn pay_u_icon() -> Node<Msg> { fn pay_u_icon() -> Node<Msg> {
@ -335,22 +327,13 @@ mod right_side {
} }
fn pay_on_spot(model: &crate::Model) -> Node<Msg> { fn pay_on_spot(model: &crate::Model) -> Node<Msg> {
div![ payment_input(
C!["w-full p-3 border-b border-gray-200"], model,
label![ model::PaymentMethod::PaymentOnTheSpot,
C!["flex items-center cursor-pointer"], "pay_on_spot",
attrs![At::For => "pay_on_spot"], "Pay on spot",
input![ pay_in_spot_icon(),
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")]
]
],
]
} }
fn pay_in_spot_icon() -> Node<Msg> { fn pay_in_spot_icon() -> Node<Msg> {
@ -411,4 +394,39 @@ mod right_side {
] ]
] ]
} }
fn payment_input(
model: &crate::Model,
method: model::PaymentMethod,
name: &str,
label: &'static str,
icon: Node<Msg>,
) -> Node<Msg> {
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)]
]
],
]
}
} }

View File

@ -101,7 +101,7 @@ mod summary_left {
ev.stop_propagation(); ev.stop_propagation();
let target = ev.target()?; let target = ev.target()?;
let input = seed::to_textarea(&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() model.cart.checkout_notes.as_str()
] ]

View File

@ -10,7 +10,7 @@ pub mod view;
#[derive(Debug)] #[derive(Debug)]
pub enum SharedMsg { pub enum SharedMsg {
LoadMe, LoadMe,
MeLoaded(NetRes<model::Account>), MeLoaded(NetRes<model::api::Account>),
SignIn(model::api::SignInInput), SignIn(model::api::SignInInput),
SignedIn(NetRes<model::api::SessionOutput>), SignedIn(NetRes<model::api::SessionOutput>),
Notification(NotificationMsg), Notification(NotificationMsg),
@ -27,7 +27,7 @@ pub struct Model {
pub access_token: Option<model::AccessTokenString>, pub access_token: Option<model::AccessTokenString>,
pub refresh_token: Option<model::RefreshTokenString>, pub refresh_token: Option<model::RefreshTokenString>,
pub exp: Option<chrono::NaiveDateTime>, pub exp: Option<chrono::NaiveDateTime>,
pub me: Option<model::Account>, pub me: Option<model::api::Account>,
pub notifications: Vec<notification::Notification>, pub notifications: Vec<notification::Notification>,
} }

View File

@ -207,7 +207,7 @@ pub mod cart_dropdown {
let sum = rusty_money::Money::from_minor(sum as i64, model.config.currency); let sum = rusty_money::Money::from_minor(sum as i64, model.config.currency);
div![ div![
C!["p-4 justify-center flex"], C!["p-4 justify-center flex bg-white"],
button![ button![
C![ C![
"text-base undefined hover:scale-110 focus:outline-none flex justify-center px-4 py-2 rounded font-bold cursor-pointer" "text-base undefined hover:scale-110 focus:outline-none flex justify-center px-4 py-2 rounded font-bold cursor-pointer"

View File

@ -1,4 +1,4 @@
use model::{ProductId, Quantity, QuantityUnit}; use model::{PaymentMethod, ProductId, Quantity, QuantityUnit};
use seed::prelude::*; use seed::prelude::*;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
@ -23,7 +23,8 @@ pub enum CartMsg {
/// Send current non-empty cart to server /// Send current non-empty cart to server
Sync, Sync,
SyncResult(NetRes<model::api::UpdateCartOutput>), SyncResult(NetRes<model::api::UpdateCartOutput>),
ChangeNotes(String), NotesChanged(String),
PaymentChanged(model::PaymentMethod),
} }
impl From<CartMsg> for Msg { impl From<CartMsg> for Msg {
@ -36,16 +37,23 @@ pub type Items = indexmap::IndexMap<ProductId, Item>;
#[derive(Debug, Copy, Clone, Serialize, Deserialize)] #[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct Item { pub struct Item {
#[serde(rename = "i")]
pub product_id: ProductId, pub product_id: ProductId,
#[serde(rename = "q")]
pub quantity: Quantity, pub quantity: Quantity,
#[serde(rename = "u")]
pub quantity_unit: QuantityUnit, pub quantity_unit: QuantityUnit,
} }
#[derive(Debug, Default, Serialize, Deserialize)] #[derive(Debug, Default, Serialize, Deserialize)]
pub struct ShoppingCart { pub struct ShoppingCart {
#[serde(rename = "i")]
pub cart_id: Option<model::ShoppingCartId>, pub cart_id: Option<model::ShoppingCartId>,
#[serde(rename = "is")]
pub items: Items, pub items: Items,
#[serde(default)] #[serde(default, rename = "pm")]
pub payment_method: Option<PaymentMethod>,
#[serde(default, rename = "cn")]
pub checkout_notes: String, pub checkout_notes: String,
#[serde(skip)] #[serde(skip)]
pub hover: bool, pub hover: bool,
@ -109,6 +117,9 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
CartMsg::Sync => sync_cart(model, orders), CartMsg::Sync => sync_cart(model, orders),
CartMsg::SyncResult(NetRes::Success(cart)) => { CartMsg::SyncResult(NetRes::Success(cart)) => {
let len = cart.items.len(); 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( model.cart.items = cart.items.into_iter().fold(
IndexMap::with_capacity(len), IndexMap::with_capacity(len),
|mut set, |mut set,
@ -139,9 +150,15 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
CartMsg::SyncResult(NetRes::Http(_cart)) => { CartMsg::SyncResult(NetRes::Http(_cart)) => {
orders.send_msg(NotificationMsg::Error("Unable to sync cart".into()).into()); orders.send_msg(NotificationMsg::Error("Unable to sync cart".into()).into());
} }
CartMsg::ChangeNotes(notes) => { CartMsg::NotesChanged(notes) => {
model.cart.checkout_notes = notes; model.cart.checkout_notes = notes;
store_local(&model.cart); 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<Msg>) {
fn sync_cart(model: &mut Model, orders: &mut impl Orders<Msg>) { fn sync_cart(model: &mut Model, orders: &mut impl Orders<Msg>) {
if let Some(access_token) = model.shared.access_token.as_ref().cloned() { if let Some(access_token) = model.shared.access_token.as_ref().cloned() {
let items: Vec<Item> = model.cart.items.values().map(Clone::clone).collect(); let items: Vec<Item> = 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::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,
)) ))
}); });
} }