Add addresses
This commit is contained in:
parent
cafafb0f24
commit
0a3bc50d6c
16
Cargo.lock
generated
16
Cargo.lock
generated
@ -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"
|
||||
|
@ -11,6 +11,7 @@ members = [
|
||||
"api",
|
||||
"web",
|
||||
"shared/model",
|
||||
"actors/account_manager",
|
||||
"actors/cart_manager",
|
||||
"actors/database_manager",
|
||||
"actors/email_manager",
|
||||
|
20
actors/account_manager/Cargo.toml
Normal file
20
actors/account_manager/Cargo.toml
Normal 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 = [] }
|
10
actors/account_manager/src/lib.rs
Normal file
10
actors/account_manager/src/lib.rs
Normal 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 }
|
||||
}
|
||||
}
|
@ -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<T> = std::result::Result<T, Error>;
|
||||
@ -95,20 +92,20 @@ impl CartManager {
|
||||
}
|
||||
|
||||
#[derive(actix::Message, Debug)]
|
||||
#[rtype(result = "Result<Option<ShoppingCartItem>>")]
|
||||
#[rtype(result = "Result<Option<model::ShoppingCartItem>>")]
|
||||
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<ShoppingCartItem>);
|
||||
cart_async_handler!(ModifyItem, modify_item, Option<model::ShoppingCartItem>);
|
||||
|
||||
async fn modify_item(
|
||||
msg: ModifyItem,
|
||||
db: actix::Addr<Database>,
|
||||
) -> Result<Option<ShoppingCartItem>> {
|
||||
) -> Result<Option<model::ShoppingCartItem>> {
|
||||
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<Option<ShoppingCartItem>>")]
|
||||
#[rtype(result = "Result<Option<model::ShoppingCartItem>>")]
|
||||
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<ShoppingCartItem>);
|
||||
cart_async_handler!(
|
||||
RemoveProduct,
|
||||
remove_product,
|
||||
Option<model::ShoppingCartItem>
|
||||
);
|
||||
|
||||
pub(crate) async fn remove_product(
|
||||
msg: RemoveProduct,
|
||||
db: actix::Addr<Database>,
|
||||
) -> Result<Option<ShoppingCartItem>> {
|
||||
) -> Result<Option<model::ShoppingCartItem>> {
|
||||
Ok(query_db!(
|
||||
db,
|
||||
database_manager::RemoveCartItem {
|
||||
@ -196,39 +197,49 @@ pub(crate) async fn remove_product(
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(actix::Message, Debug)]
|
||||
#[rtype(result = "Result<Vec<ShoppingCartItem>>")]
|
||||
pub struct ModifyCart {
|
||||
pub buyer_id: AccountId,
|
||||
pub items: Vec<ModifyItem>,
|
||||
pub struct ModifyCartResult {
|
||||
pub cart_id: ShoppingCartId,
|
||||
pub items: Vec<model::ShoppingCartItem>,
|
||||
pub checkout_notes: String,
|
||||
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);
|
||||
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<model::ShoppingCart> = 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<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,
|
||||
})
|
||||
}
|
||||
|
126
actors/database_manager/src/addresses.rs
Normal file
126
actors/database_manager/src/addresses.rs
Normal 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())
|
||||
}
|
@ -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<T> = std::result::Result<T, Error>;
|
||||
|
@ -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)
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -124,6 +124,7 @@ pub struct UpdateShoppingCart {
|
||||
pub buyer_id: AccountId,
|
||||
pub payment_method: PaymentMethod,
|
||||
pub state: ShoppingCartState,
|
||||
pub checkout_notes: Option<String>,
|
||||
}
|
||||
|
||||
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| {
|
||||
|
@ -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<Address> = 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()),
|
||||
}))
|
||||
}
|
||||
|
||||
|
@ -142,18 +142,23 @@ async fn update_cart(
|
||||
)
|
||||
.collect();
|
||||
|
||||
let items: Vec<model::ShoppingCartItem> = 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<Addr<Database>>,
|
||||
tm: Data<Addr<TokenManager>>,
|
||||
credentials: BearerAuth,
|
||||
) -> routes::Result<Json<model::Account>> {
|
||||
) -> routes::Result<Json<model::api::Account>> {
|
||||
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")]
|
||||
|
11
migrations/20220519121203_account_addresses.sql
Normal file
11
migrations/20220519121203_account_addresses.sql
Normal 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)
|
||||
);
|
@ -20,6 +20,82 @@ pub struct Config {
|
||||
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))]
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[serde(transparent)]
|
||||
@ -388,11 +464,16 @@ pub struct UpdateItemOutput {
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct UpdateCartInput {
|
||||
pub items: Vec<UpdateItemInput>,
|
||||
pub notes: String,
|
||||
pub payment_method: Option<PaymentMethod>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub struct UpdateCartOutput {
|
||||
pub cart_id: ShoppingCartId,
|
||||
pub items: Vec<ShoppingCartItem>,
|
||||
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<RegisterError>,
|
||||
pub account: Option<Account>,
|
||||
pub account: Option<super::Account>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
|
@ -82,3 +82,39 @@ impl<T> fake::Dummy<T> for NonNegative {
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
@ -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: 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,
|
||||
}
|
||||
|
@ -4,19 +4,19 @@ use seed::fetch::{Header, Method, Request};
|
||||
use crate::api::perform;
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
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(
|
||||
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<model::A
|
||||
.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(
|
||||
Request::new("/api/v1/sign-in")
|
||||
.method(Method::Post)
|
||||
@ -35,7 +35,7 @@ pub async fn sign_in(input: model::api::SignInInput) -> super::NetRes<model::api
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn verify_token(access_token: AccessTokenString) -> super::NetRes<String> {
|
||||
pub async fn verify_token(access_token: AccessTokenString) -> NetRes<String> {
|
||||
perform(
|
||||
Request::new("/api/v1/token/verify")
|
||||
.method(Method::Post)
|
||||
@ -44,9 +44,7 @@ pub async fn verify_token(access_token: AccessTokenString) -> super::NetRes<Stri
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn refresh_token(
|
||||
access_token: RefreshTokenString,
|
||||
) -> super::NetRes<model::api::SessionOutput> {
|
||||
pub async fn refresh_token(access_token: RefreshTokenString) -> NetRes<model::api::SessionOutput> {
|
||||
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<model::api::SessionOutput> {
|
||||
pub async fn sign_up(input: model::api::CreateAccountInput) -> NetRes<model::api::SessionOutput> {
|
||||
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<crate::shopping_cart::Item>,
|
||||
notes: String,
|
||||
payment_method: Option<model::PaymentMethod>,
|
||||
) -> NetRes<model::api::UpdateCartOutput> {
|
||||
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
|
||||
}
|
||||
|
@ -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<Msg> {
|
||||
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"],
|
||||
payment_input(
|
||||
model,
|
||||
model::PaymentMethod::PayU,
|
||||
"pay_u",
|
||||
"PayU",
|
||||
pay_u_icon(),
|
||||
span![C!["ml-3"], model.i18n.t("PayU")]
|
||||
]
|
||||
],
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
fn pay_u_icon() -> Node<Msg> {
|
||||
@ -335,22 +327,13 @@ mod right_side {
|
||||
}
|
||||
|
||||
fn pay_on_spot(model: &crate::Model) -> Node<Msg> {
|
||||
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"],
|
||||
payment_input(
|
||||
model,
|
||||
model::PaymentMethod::PaymentOnTheSpot,
|
||||
"pay_on_spot",
|
||||
"Pay on spot",
|
||||
pay_in_spot_icon(),
|
||||
span![C!["ml-3"], model.i18n.t("Pay on spot")]
|
||||
]
|
||||
],
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
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)]
|
||||
]
|
||||
],
|
||||
]
|
||||
}
|
||||
}
|
||||
|
@ -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()
|
||||
]
|
||||
|
@ -10,7 +10,7 @@ pub mod view;
|
||||
#[derive(Debug)]
|
||||
pub enum SharedMsg {
|
||||
LoadMe,
|
||||
MeLoaded(NetRes<model::Account>),
|
||||
MeLoaded(NetRes<model::api::Account>),
|
||||
SignIn(model::api::SignInInput),
|
||||
SignedIn(NetRes<model::api::SessionOutput>),
|
||||
Notification(NotificationMsg),
|
||||
@ -27,7 +27,7 @@ pub struct Model {
|
||||
pub access_token: Option<model::AccessTokenString>,
|
||||
pub refresh_token: Option<model::RefreshTokenString>,
|
||||
pub exp: Option<chrono::NaiveDateTime>,
|
||||
pub me: Option<model::Account>,
|
||||
pub me: Option<model::api::Account>,
|
||||
pub notifications: Vec<notification::Notification>,
|
||||
}
|
||||
|
||||
|
@ -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"
|
||||
|
@ -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<model::api::UpdateCartOutput>),
|
||||
ChangeNotes(String),
|
||||
NotesChanged(String),
|
||||
PaymentChanged(model::PaymentMethod),
|
||||
}
|
||||
|
||||
impl From<CartMsg> for Msg {
|
||||
@ -36,16 +37,23 @@ pub type Items = indexmap::IndexMap<ProductId, Item>;
|
||||
|
||||
#[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<model::ShoppingCartId>,
|
||||
#[serde(rename = "is")]
|
||||
pub items: Items,
|
||||
#[serde(default)]
|
||||
#[serde(default, rename = "pm")]
|
||||
pub payment_method: Option<PaymentMethod>,
|
||||
#[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<Msg>) {
|
||||
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<Msg>) {
|
||||
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<Msg>) {
|
||||
fn sync_cart(model: &mut Model, orders: &mut impl Orders<Msg>) {
|
||||
if let Some(access_token) = model.shared.access_token.as_ref().cloned() {
|
||||
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::api::public::update_cart(access_token, items).await,
|
||||
crate::api::public::update_cart(access_token, items, notes, payment_method).await,
|
||||
))
|
||||
});
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user