WIP: Place order for guess and account.

This commit is contained in:
Adrian Woźniak 2022-05-23 14:11:56 +02:00
parent 28e9736562
commit c1c97061eb
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
19 changed files with 609 additions and 193 deletions

View File

@ -90,7 +90,7 @@ impl actix::Actor for AccountManager {
pub struct MeResult { pub struct MeResult {
pub account: FullAccount, pub account: FullAccount,
pub addresses: Vec<model::Address>, pub addresses: Vec<model::AccountAddress>,
} }
#[derive(actix::Message, Debug)] #[derive(actix::Message, Debug)]

View File

@ -9,7 +9,7 @@ pub enum Error {
} }
#[derive(actix::Message)] #[derive(actix::Message)]
#[rtype(result = "Result<Vec<model::Address>>")] #[rtype(result = "Result<Vec<model::AccountAddress>>")]
pub struct AccountAddresses { pub struct AccountAddresses {
pub account_id: model::AccountId, pub account_id: model::AccountId,
} }
@ -17,17 +17,17 @@ pub struct AccountAddresses {
db_async_handler!( db_async_handler!(
AccountAddresses, AccountAddresses,
account_addresses, account_addresses,
Vec<model::Address>, Vec<model::AccountAddress>,
inner_account_addresses inner_account_addresses
); );
pub(crate) async fn account_addresses( pub(crate) async fn account_addresses(
msg: AccountAddresses, msg: AccountAddresses,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<Vec<model::Address>> { ) -> Result<Vec<model::AccountAddress>> {
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT id, name, email, street, city, country, zip, account_id SELECT id, name, email, street, city, country, zip, account_id, is_default
FROM account_addresses FROM account_addresses
WHERE account_id = $1 WHERE account_id = $1
"#, "#,
@ -39,7 +39,37 @@ WHERE account_id = $1
} }
#[derive(actix::Message)] #[derive(actix::Message)]
#[rtype(result = "Result<model::Address>")] #[rtype(result = "Result<model::AccountAddress>")]
pub struct DefaultAccountAddress {
pub account_id: model::AccountId,
}
db_async_handler!(
DefaultAccountAddress,
default_account_address,
model::AccountAddress,
inner_default_account_address
);
pub(crate) async fn default_account_address(
msg: DefaultAccountAddress,
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 is_default
"#,
)
.bind(msg.account_id)
.fetch_one(pool)
.await
.map_err(|_| Error::AccountAddresses.into())
}
#[derive(actix::Message)]
#[rtype(result = "Result<model::AccountAddress>")]
pub struct CreateAccountAddress { pub struct CreateAccountAddress {
pub name: model::Name, pub name: model::Name,
pub email: model::Email, pub email: model::Email,
@ -47,25 +77,41 @@ pub struct CreateAccountAddress {
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 account_id: model::AccountId, pub account_id: Option<model::AccountId>,
pub is_default: bool,
} }
db_async_handler!( db_async_handler!(
CreateAccountAddress, CreateAccountAddress,
create_address, create_address,
model::Address, model::AccountAddress,
inner_create_address inner_create_address
); );
pub(crate) async fn create_address( pub(crate) async fn create_address(
msg: CreateAccountAddress, msg: CreateAccountAddress,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<model::Address> { ) -> Result<model::AccountAddress> {
if msg.is_default {
if let Err(e) = sqlx::query(
r#"
UPDATE account_addresses
SET is_default = FALSE
WHERE account_id = $1
"#,
)
.bind(msg.account_id)
.fetch_all(&mut *pool)
.await
{
log::error!("{}", e);
}
}
sqlx::query_as( sqlx::query_as(
r#" r#"
INSERT INTO account_addresses ( name, email, street, city, country, zip, account_id ) INSERT INTO account_addresses ( name, email, street, city, country, zip, account_id )
VALUES ($1, $2, $3, $4, $5, $6, $7) VALUES ($1, $2, $3, $4, $5, $6, $7)
RETURNING id, name, email, street, city, country, zip, account_id RETURNING id, name, email, street, city, country, zip, account_id, is_default
"#, "#,
) )
.bind(msg.name) .bind(msg.name)
@ -81,7 +127,7 @@ RETURNING id, name, email, street, city, country, zip, account_id
} }
#[derive(actix::Message)] #[derive(actix::Message)]
#[rtype(result = "Result<model::Address>")] #[rtype(result = "Result<model::AccountAddress>")]
pub struct UpdateAccountAddress { pub struct UpdateAccountAddress {
pub id: model::AddressId, pub id: model::AddressId,
pub name: model::Name, pub name: model::Name,
@ -91,25 +137,26 @@ pub struct UpdateAccountAddress {
pub country: model::Country, pub country: model::Country,
pub zip: model::Zip, pub zip: model::Zip,
pub account_id: model::AccountId, pub account_id: model::AccountId,
pub is_default: bool,
} }
db_async_handler!( db_async_handler!(
UpdateAccountAddress, UpdateAccountAddress,
update_account_address, update_account_address,
model::Address, model::AccountAddress,
inner_update_account_address inner_update_account_address
); );
pub(crate) async fn update_account_address( pub(crate) async fn update_account_address(
msg: UpdateAccountAddress, msg: UpdateAccountAddress,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>, pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<model::Address> { ) -> Result<model::AccountAddress> {
sqlx::query_as( sqlx::query_as(
r#" r#"
UPDATE account_addresses UPDATE account_addresses
SET name = $2, email = $3, street = $4, city = $5, country = $6, zip = $7, account_id = $8 SET name = $2, email = $3, street = $4, city = $5, country = $6, zip = $7, account_id = $8, is_default = $9
WHERE id = $1 WHERE id = $1
RETURNING id, name, email, street, city, country, zip, account_id RETURNING id, name, email, street, city, country, zip, account_id, is_default
"#, "#,
) )
.bind(msg.id) .bind(msg.id)
@ -120,6 +167,7 @@ RETURNING id, name, email, street, city, country, zip, account_id
.bind(msg.country) .bind(msg.country)
.bind(msg.zip) .bind(msg.zip)
.bind(msg.account_id) .bind(msg.account_id)
.bind(msg.is_default)
.fetch_one(pool) .fetch_one(pool)
.await .await
.map_err(|_| Error::CreateAccountAddress.into()) .map_err(|_| Error::CreateAccountAddress.into())

View File

@ -3,10 +3,11 @@ use config::SharedAppConfig;
use sqlx::PgPool; use sqlx::PgPool;
use sqlx_core::arguments::Arguments; use sqlx_core::arguments::Arguments;
pub use crate::account_orders::*; pub use crate::account_addresses::*;
pub use crate::accounts::*; pub use crate::accounts::*;
pub use crate::addresses::*; pub use crate::order_addresses::*;
pub use crate::order_items::*; pub use crate::order_items::*;
pub use crate::orders::*;
pub use crate::photos::*; pub use crate::photos::*;
pub use crate::product_photos::*; pub use crate::product_photos::*;
pub use crate::products::*; pub use crate::products::*;
@ -15,10 +16,11 @@ pub use crate::shopping_carts::*;
pub use crate::stocks::*; pub use crate::stocks::*;
pub use crate::tokens::*; pub use crate::tokens::*;
pub mod account_orders; pub mod account_addresses;
pub mod accounts; pub mod accounts;
pub mod addresses; pub mod order_addresses;
pub mod order_items; pub mod order_items;
pub mod orders;
pub mod photos; pub mod photos;
pub mod product_photos; pub mod product_photos;
pub mod products; pub mod products;
@ -128,7 +130,7 @@ pub enum Error {
#[error("{0}")] #[error("{0}")]
Account(#[from] accounts::Error), Account(#[from] accounts::Error),
#[error("{0}")] #[error("{0}")]
AccountOrder(#[from] account_orders::Error), AccountOrder(#[from] orders::Error),
#[error("{0}")] #[error("{0}")]
Product(#[from] products::Error), Product(#[from] products::Error),
#[error("{0}")] #[error("{0}")]
@ -146,7 +148,9 @@ pub enum Error {
#[error("{0}")] #[error("{0}")]
ProductPhoto(#[from] product_photos::Error), ProductPhoto(#[from] product_photos::Error),
#[error("{0}")] #[error("{0}")]
AccountAddress(#[from] addresses::Error), AccountAddress(#[from] account_addresses::Error),
#[error("{0}")]
OrderAddress(#[from] order_addresses::Error),
#[error("Failed to start or finish transaction")] #[error("Failed to start or finish transaction")]
TransactionFailed, TransactionFailed,
} }

View File

@ -0,0 +1,130 @@
use crate::{db_async_handler, Result};
#[derive(Debug, Copy, Clone, serde::Serialize, thiserror::Error)]
pub enum Error {
#[error("Can't load account addresses")]
OrderAddress,
#[error("Failed to save account address")]
CreateOrderAddress,
}
#[derive(actix::Message)]
#[rtype(result = "Result<model::OrderAddress>")]
pub struct OrderAddress {
pub order_id: model::OrderId,
}
db_async_handler!(
OrderAddress,
order_address,
model::OrderAddress,
inner_order_address
);
pub(crate) async fn order_address(
msg: OrderAddress,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<model::OrderAddress> {
sqlx::query_as(
r#"
SELECT
order_addresses.id,
order_addresses.name,
order_addresses.email,
order_addresses.street,
order_addresses.city,
order_addresses.country,
order_addresses.zip
FROM order_addresses
INNER JOIN orders ON orders.address_id = order_addresses.id
WHERE orders.id = $1
"#,
)
.bind(msg.order_id)
.fetch_one(pool)
.await
.map_err(|_| Error::OrderAddress.into())
}
#[derive(actix::Message)]
#[rtype(result = "Result<model::OrderAddress>")]
pub struct CreateOrderAddress {
pub name: model::Name,
pub email: model::Email,
pub street: model::Street,
pub city: model::City,
pub country: model::Country,
pub zip: model::Zip,
}
db_async_handler!(
CreateOrderAddress,
create_order_address,
model::OrderAddress,
inner_create_order_address
);
pub(crate) async fn create_order_address(
msg: CreateOrderAddress,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<model::OrderAddress> {
sqlx::query_as(
r#"
INSERT INTO order_addresses ( name, email, street, city, country, zip )
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING id, name, email, street, city, country, zip
"#,
)
.bind(msg.name)
.bind(msg.email)
.bind(msg.street)
.bind(msg.city)
.bind(msg.country)
.bind(msg.zip)
.fetch_one(pool)
.await
.map_err(|_| Error::CreateOrderAddress.into())
}
#[derive(actix::Message)]
#[rtype(result = "Result<model::OrderAddress>")]
pub struct UpdateOrderAddress {
pub id: model::OrderAddressId,
pub name: model::Name,
pub email: model::Email,
pub street: model::Street,
pub city: model::City,
pub country: model::Country,
pub zip: model::Zip,
}
db_async_handler!(
UpdateOrderAddress,
update_account_address,
model::OrderAddress,
inner_update_account_address
);
pub(crate) async fn update_account_address(
msg: UpdateOrderAddress,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<model::OrderAddress> {
sqlx::query_as(
r#"
UPDATE order_addresses
SET name = $2, email = $3, street = $4, city = $5, country = $6, zip = $7
WHERE id = $1
RETURNING id, name, email, street, city, country, zip
"#,
)
.bind(msg.id)
.bind(msg.name)
.bind(msg.email)
.bind(msg.street)
.bind(msg.city)
.bind(msg.country)
.bind(msg.zip)
.fetch_one(pool)
.await
.map_err(|_| Error::CreateOrderAddress.into())
}

View File

@ -47,7 +47,7 @@ ORDER BY id DESC
#[rtype(result = "Result<OrderItem>")] #[rtype(result = "Result<OrderItem>")]
pub struct CreateOrderItem { pub struct CreateOrderItem {
pub product_id: ProductId, pub product_id: ProductId,
pub order_id: AccountOrderId, pub order_id: OrderId,
pub quantity: Quantity, pub quantity: Quantity,
pub quantity_unit: QuantityUnit, pub quantity_unit: QuantityUnit,
} }
@ -109,7 +109,7 @@ WHERE id = $1
#[derive(actix::Message)] #[derive(actix::Message)]
#[rtype(result = "Result<Vec<OrderItem>>")] #[rtype(result = "Result<Vec<OrderItem>>")]
pub struct OrderItems { pub struct OrderItems {
pub order_id: model::AccountOrderId, pub order_id: model::OrderId,
} }
db_async_handler!(OrderItems, order_items, Vec<OrderItem>); db_async_handler!(OrderItems, order_items, Vec<OrderItem>);

View File

@ -21,19 +21,16 @@ pub enum Error {
} }
#[derive(actix::Message)] #[derive(actix::Message)]
#[rtype(result = "Result<Vec<AccountOrder>>")] #[rtype(result = "Result<Vec<Order>>")]
pub struct AllAccountOrders; pub struct AllAccountOrders;
db_async_handler!(AllAccountOrders, all_account_orders, Vec<AccountOrder>); db_async_handler!(AllAccountOrders, all_orders, Vec<Order>);
pub(crate) async fn all_account_orders( pub(crate) async fn all_orders(_msg: AllAccountOrders, pool: PgPool) -> Result<Vec<Order>> {
_msg: AllAccountOrders,
pool: PgPool,
) -> Result<Vec<AccountOrder>> {
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT id, buyer_id, status, order_ext_id, service_order_id, checkout_notes SELECT id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id
FROM account_orders FROM orders
ORDER BY id DESC ORDER BY id DESC
"#, "#,
) )
@ -56,7 +53,7 @@ pub mod create_order {
} }
#[derive(actix::Message)] #[derive(actix::Message)]
#[rtype(result = "Result<AccountOrder>")] #[rtype(result = "Result<Order>")]
pub struct CreateAccountOrder { pub struct CreateAccountOrder {
pub buyer_id: AccountId, pub buyer_id: AccountId,
pub items: Vec<create_order::OrderItem>, pub items: Vec<create_order::OrderItem>,
@ -67,19 +64,19 @@ pub struct CreateAccountOrder {
db_async_handler!( db_async_handler!(
CreateAccountOrder, CreateAccountOrder,
create_account_order, create_account_order,
AccountOrder, Order,
inner_create_account_order inner_create_account_order
); );
pub(crate) async fn create_account_order( pub(crate) async fn create_account_order(
msg: CreateAccountOrder, msg: CreateAccountOrder,
t: &mut sqlx::Transaction<'_, sqlx::Postgres>, t: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<AccountOrder> { ) -> Result<Order> {
let order: AccountOrder = match sqlx::query_as( let order: Order = match sqlx::query_as(
r#" r#"
INSERT INTO account_orders (buyer_id, status) INSERT INTO orders (buyer_id, status)
VALUES ($1, $2, $3) VALUES ($1, $2, $3)
RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id
"#, "#,
) )
.bind(msg.buyer_id) .bind(msg.buyer_id)
@ -130,26 +127,23 @@ RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes
} }
#[derive(actix::Message)] #[derive(actix::Message)]
#[rtype(result = "Result<AccountOrder>")] #[rtype(result = "Result<Order>")]
pub struct UpdateAccountOrder { pub struct UpdateAccountOrder {
pub id: AccountOrderId, pub id: OrderId,
pub buyer_id: AccountId, pub buyer_id: AccountId,
pub status: OrderStatus, pub status: OrderStatus,
pub order_id: Option<OrderId>, pub order_id: Option<ExtOrderId>,
} }
db_async_handler!(UpdateAccountOrder, update_account_order, AccountOrder); db_async_handler!(UpdateAccountOrder, update_account_order, Order);
pub(crate) async fn update_account_order( pub(crate) async fn update_account_order(msg: UpdateAccountOrder, db: PgPool) -> Result<Order> {
msg: UpdateAccountOrder,
db: PgPool,
) -> Result<AccountOrder> {
sqlx::query_as( sqlx::query_as(
r#" r#"
UPDATE account_orders UPDATE orders
SET buyer_id = $2 AND status = $3 AND order_id = $4 SET buyer_id = $2 AND status = $3 AND order_id = $4
WHERE id = $1 WHERE id = $1
RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id
"#, "#,
) )
.bind(msg.id) .bind(msg.id)
@ -165,28 +159,24 @@ RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes
} }
#[derive(actix::Message)] #[derive(actix::Message)]
#[rtype(result = "Result<AccountOrder>")] #[rtype(result = "Result<Order>")]
pub struct UpdateAccountOrderByExt { pub struct UpdateAccountOrderByExt {
pub order_ext_id: String, pub order_ext_id: String,
pub status: OrderStatus, pub status: OrderStatus,
} }
db_async_handler!( db_async_handler!(UpdateAccountOrderByExt, update_account_order_by_ext, Order);
UpdateAccountOrderByExt,
update_account_order_by_ext,
AccountOrder
);
pub(crate) async fn update_account_order_by_ext( pub(crate) async fn update_account_order_by_ext(
msg: UpdateAccountOrderByExt, msg: UpdateAccountOrderByExt,
db: PgPool, db: PgPool,
) -> Result<AccountOrder> { ) -> Result<Order> {
sqlx::query_as( sqlx::query_as(
r#" r#"
UPDATE account_orders UPDATE orders
SET status = $2 SET status = $2
WHERE order_ext_id = $1 WHERE order_ext_id = $1
RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id
"#, "#,
) )
.bind(msg.order_ext_id) .bind(msg.order_ext_id)
@ -200,18 +190,18 @@ RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes
} }
#[derive(actix::Message)] #[derive(actix::Message)]
#[rtype(result = "Result<AccountOrder>")] #[rtype(result = "Result<Order>")]
pub struct FindAccountOrder { pub struct FindAccountOrder {
pub id: AccountOrderId, pub id: OrderId,
} }
db_async_handler!(FindAccountOrder, find_account_order, AccountOrder); db_async_handler!(FindAccountOrder, find_account_order, Order);
pub(crate) async fn find_account_order(msg: FindAccountOrder, db: PgPool) -> Result<AccountOrder> { pub(crate) async fn find_account_order(msg: FindAccountOrder, db: PgPool) -> Result<Order> {
sqlx::query_as( sqlx::query_as(
r#" r#"
SELECT id, buyer_id, status, order_ext_id, service_order_id, checkout_notes SELECT id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id
FROM account_orders FROM orders
WHERE id = $1 WHERE id = $1
"#, "#,
) )
@ -225,24 +215,21 @@ WHERE id = $1
} }
#[derive(actix::Message)] #[derive(actix::Message)]
#[rtype(result = "Result<AccountOrder>")] #[rtype(result = "Result<Order>")]
pub struct SetOrderServiceId { pub struct SetOrderServiceId {
pub id: AccountOrderId, pub id: OrderId,
pub service_order_id: String, pub service_order_id: String,
} }
db_async_handler!(SetOrderServiceId, set_order_service_id, AccountOrder); db_async_handler!(SetOrderServiceId, set_order_service_id, Order);
pub(crate) async fn set_order_service_id( pub(crate) async fn set_order_service_id(msg: SetOrderServiceId, db: PgPool) -> Result<Order> {
msg: SetOrderServiceId,
db: PgPool,
) -> Result<AccountOrder> {
sqlx::query_as( sqlx::query_as(
r#" r#"
UPDATE account_orders UPDATE orders
SET service_order_id = $2 SET service_order_id = $2
WHERE id = $1 WHERE id = $1
RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes, address_id
"#, "#,
) )
.bind(msg.id) .bind(msg.id)

View File

@ -1,7 +1,7 @@
use actix::Message; use actix::Message;
use config::SharedAppConfig; use config::SharedAppConfig;
use database_manager::{query_db, SharedDatabase}; use database_manager::{query_db, SharedDatabase};
use model::{AccountId, AccountOrder, OrderStatus, ShoppingCart, ShoppingCartId, ShoppingCartItem}; use model::{AccountId, Order, OrderStatus, ShoppingCart, ShoppingCartItem};
#[macro_export] #[macro_export]
macro_rules! order_async_handler { macro_rules! order_async_handler {
@ -19,6 +19,27 @@ macro_rules! order_async_handler {
}; };
} }
#[macro_export]
macro_rules! query_order {
($order_manager: expr, $msg: expr, $fail: expr) => {
$crate::query_order!($order_manager, $msg, $fail, $fail)
};
($order_manager: expr, $msg: expr, $db_fail: expr, $act_fail: expr) => {
match $order_manager.send($msg).await {
Ok(Ok(r)) => Ok(r),
Ok(Err(e)) => {
log::error!("{e}");
Err($db_fail)
}
Err(e) => {
log::error!("{e:?}");
Err($act_fail)
}
}
};
}
#[derive(Debug, Copy, Clone, serde::Serialize, thiserror::Error)] #[derive(Debug, Copy, Clone, serde::Serialize, thiserror::Error)]
#[serde(rename_all = "kebab-case", tag = "order")] #[serde(rename_all = "kebab-case", tag = "order")]
pub enum Error { pub enum Error {
@ -28,6 +49,12 @@ pub enum Error {
ShoppingCart, ShoppingCart,
#[error("Failed to create account order")] #[error("Failed to create account order")]
CreateAccountOrder, CreateAccountOrder,
#[error("Account does not have address")]
NoAddress,
#[error("Invalid account address")]
InvalidAccountAddress,
#[error("Invalid order address")]
InvalidOrderAddress,
} }
pub type Result<T> = std::result::Result<T, Error>; pub type Result<T> = std::result::Result<T, Error>;
@ -47,24 +74,34 @@ impl OrderManager {
} }
} }
#[derive(Message, Debug)] #[derive(Debug)]
#[rtype(result = "Result<AccountOrder>")] pub struct CreateOrderAddress {
pub struct CreateOrder { pub name: model::Name,
pub account_id: AccountId, pub email: model::Email,
pub shopping_cart_id: ShoppingCartId, pub street: model::Street,
pub city: model::City,
pub country: model::Country,
pub zip: model::Zip,
} }
order_async_handler!(CreateOrder, create_order, AccountOrder); #[derive(Message, Debug)]
#[rtype(result = "Result<Order>")]
pub struct CreateAccountOrder {
pub account_id: AccountId,
pub create_address: Option<CreateOrderAddress>,
}
pub(crate) async fn create_order( order_async_handler!(CreateAccountOrder, create_account_order, Order);
msg: CreateOrder,
pub(crate) async fn create_account_order(
msg: CreateAccountOrder,
db: SharedDatabase, db: SharedDatabase,
_config: SharedAppConfig, _config: SharedAppConfig,
) -> Result<AccountOrder> { ) -> Result<Order> {
let cart: ShoppingCart = query_db!( let cart: ShoppingCart = query_db!(
db, db,
database_manager::FindShoppingCart { database_manager::EnsureActiveShoppingCart {
id: msg.shopping_cart_id, buyer_id: msg.account_id
}, },
Error::ShoppingCart, Error::ShoppingCart,
Error::DatabaseInternal Error::DatabaseInternal
@ -78,6 +115,44 @@ pub(crate) async fn create_order(
Error::ShoppingCart, Error::ShoppingCart,
Error::DatabaseInternal 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
)
};
query_db!(
db,
database_manager::CreateOrderAddress {
name: address.name,
email: address.email,
street: address.street,
city: address.city,
country: address.country,
zip: address.zip,
},
Error::InvalidOrderAddress
);
let order = query_db!( let order = query_db!(
db, db,
database_manager::CreateAccountOrder { database_manager::CreateAccountOrder {

View File

@ -151,7 +151,7 @@ impl From<Product> for pay_u::Product {
} }
pub struct CreatePaymentResult { pub struct CreatePaymentResult {
pub order: model::AccountOrder, pub order: model::Order,
pub items: Vec<model::OrderItem>, pub items: Vec<model::OrderItem>,
pub redirect_uri: String, pub redirect_uri: String,
} }
@ -215,7 +215,7 @@ pub(crate) async fn request_payment(
Error::UnavailableShoppingCart Error::UnavailableShoppingCart
); );
let db_order: model::AccountOrder = query_db!( let db_order: model::Order = query_db!(
db, db,
database_manager::CreateAccountOrder { database_manager::CreateAccountOrder {
buyer_id: msg.buyer_id, buyer_id: msg.buyer_id,

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, Address, Encrypt, PasswordConfirmation}; use model::{AccountAddress, AccountId, AccountState, Encrypt, PasswordConfirmation};
use token_manager::TokenManager; use token_manager::TokenManager;
use crate::routes::admin::Error; use crate::routes::admin::Error;
@ -112,7 +112,7 @@ pub async fn create_account(
role: payload.role, role: payload.role,
} }
); );
let addresses: Vec<Address> = admin_send_db!( let addresses: Vec<AccountAddress> = admin_send_db!(
db, db,
database_manager::AccountAddresses { database_manager::AccountAddresses {
account_id: account.id account_id: account.id

View File

@ -3,7 +3,7 @@ use actix_web::get;
use actix_web::web::{Data, Json, ServiceConfig}; use actix_web::web::{Data, Json, ServiceConfig};
use actix_web_httpauth::extractors::bearer::BearerAuth; use actix_web_httpauth::extractors::bearer::BearerAuth;
use database_manager::Database; use database_manager::Database;
use model::api::AccountOrders; use model::api::Orders;
use token_manager::TokenManager; use token_manager::TokenManager;
use crate::routes::RequireUser; use crate::routes::RequireUser;
@ -14,10 +14,10 @@ async fn orders(
credentials: BearerAuth, credentials: BearerAuth,
tm: Data<Addr<TokenManager>>, tm: Data<Addr<TokenManager>>,
db: Data<Addr<Database>>, db: Data<Addr<Database>>,
) -> routes::Result<Json<AccountOrders>> { ) -> routes::Result<Json<Orders>> {
credentials.require_admin(tm.into_inner()).await?; credentials.require_admin(tm.into_inner()).await?;
let orders: Vec<model::AccountOrder> = admin_send_db!(&db, database_manager::AllAccountOrders); let orders: Vec<model::Order> = admin_send_db!(&db, database_manager::AllAccountOrders);
let items: Vec<model::OrderItem> = admin_send_db!(db, database_manager::AllOrderItems); let items: Vec<model::OrderItem> = admin_send_db!(db, database_manager::AllOrderItems);
Ok(Json((orders, items).into())) Ok(Json((orders, items).into()))

View File

@ -6,6 +6,7 @@ use actix_web_httpauth::extractors::bearer::BearerAuth;
use cart_manager::{query_cart, CartManager}; use cart_manager::{query_cart, CartManager};
use database_manager::{query_db, Database}; use database_manager::{query_db, Database};
use model::api; use model::api;
use order_manager::{query_order, OrderManager};
use payment_manager::{query_pay, PaymentManager}; use payment_manager::{query_pay, PaymentManager};
use token_manager::TokenManager; use token_manager::TokenManager;
@ -234,6 +235,7 @@ pub(crate) async fn create_order(
tm: Data<Addr<TokenManager>>, tm: Data<Addr<TokenManager>>,
credentials: BearerAuth, credentials: BearerAuth,
payment: Data<Addr<PaymentManager>>, payment: Data<Addr<PaymentManager>>,
order: Data<Addr<OrderManager>>,
) -> routes::Result<HttpResponse> { ) -> routes::Result<HttpResponse> {
let account_id = credentials let account_id = credentials
.require_user(tm.into_inner()) .require_user(tm.into_inner())
@ -248,6 +250,7 @@ pub(crate) async fn create_order(
language, language,
charge_client, charge_client,
currency, currency,
address,
} = payload; } = payload;
let ip = match req.peer_addr() { let ip = match req.peer_addr() {
Some(ip) => ip, Some(ip) => ip,
@ -272,6 +275,32 @@ pub(crate) async fn create_order(
routes::Error::Public(PublicError::DatabaseConnection) routes::Error::Public(PublicError::DatabaseConnection)
); );
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,
},
),
},
PublicError::PlaceOrder,
PublicError::DatabaseConnection
)?;
Ok(HttpResponse::SeeOther() Ok(HttpResponse::SeeOther()
.append_header(("Location", redirect_uri.as_str())) .append_header(("Location", redirect_uri.as_str()))
.body(format!( .body(format!(

View File

@ -48,6 +48,8 @@ macro_rules! public_send_db {
pub enum Error { pub enum Error {
#[error("{0}")] #[error("{0}")]
ApiV1(#[from] api_v1::Error), ApiV1(#[from] api_v1::Error),
#[error("Failed to place order")]
PlaceOrder,
#[error("Internal server error")] #[error("Internal server error")]
DatabaseConnection, DatabaseConnection,
#[error("{0}")] #[error("{0}")]

View File

@ -0,0 +1,19 @@
ALTER TABLE account_orders
ALTER COLUMN buyer_id DROP NOT NULL;
CREATE TABLE order_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
);
ALTER TABLE account_orders
ADD COLUMN address_id INT REFERENCES order_addresses (id);
ALTER TABLE account_orders
RENAME TO orders;

View File

@ -0,0 +1,2 @@
ALTER TABLE account_addresses
ADD COLUMN is_default BOOLEAN NOT NULL DEFAULT false;

View File

@ -28,10 +28,10 @@ pub struct Account {
pub role: Role, pub role: Role,
pub customer_id: uuid::Uuid, pub customer_id: uuid::Uuid,
pub state: AccountState, pub state: AccountState,
pub addresses: Vec<Address>, pub addresses: Vec<AccountAddress>,
} }
impl From<(FullAccount, Vec<crate::Address>)> for Account { impl From<(FullAccount, Vec<crate::AccountAddress>)> for Account {
fn from( fn from(
( (
FullAccount { FullAccount {
@ -44,7 +44,7 @@ impl From<(FullAccount, Vec<crate::Address>)> for Account {
state, state,
}, },
addresses, addresses,
): (FullAccount, Vec<crate::Address>), ): (FullAccount, Vec<crate::AccountAddress>),
) -> Self { ) -> Self {
Self { Self {
id, id,
@ -59,7 +59,7 @@ impl From<(FullAccount, Vec<crate::Address>)> for Account {
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Address { pub struct AccountAddress {
pub id: AddressId, pub id: AddressId,
pub name: Name, pub name: Name,
pub email: Email, pub email: Email,
@ -68,11 +68,12 @@ pub struct Address {
pub country: Country, pub country: Country,
pub zip: Zip, pub zip: Zip,
pub account_id: AccountId, pub account_id: AccountId,
pub is_default: bool,
} }
impl From<crate::Address> for Address { impl From<crate::AccountAddress> for AccountAddress {
fn from( fn from(
crate::Address { crate::AccountAddress {
id, id,
name, name,
email, email,
@ -81,7 +82,8 @@ impl From<crate::Address> for Address {
country, country,
zip, zip,
account_id, account_id,
}: crate::Address, is_default,
}: crate::AccountAddress,
) -> Self { ) -> Self {
Self { Self {
id, id,
@ -92,6 +94,7 @@ impl From<crate::Address> for Address {
country, country,
zip, zip,
account_id, account_id,
is_default,
} }
} }
} }
@ -99,15 +102,15 @@ impl From<crate::Address> for Address {
#[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)]
pub struct AccountOrders(pub Vec<AccountOrder>); pub struct Orders(pub Vec<Order>);
impl From<(Vec<crate::AccountOrder>, Vec<crate::OrderItem>)> for AccountOrders { impl From<(Vec<crate::Order>, Vec<crate::OrderItem>)> for Orders {
fn from((orders, mut items): (Vec<crate::AccountOrder>, Vec<crate::OrderItem>)) -> Self { fn from((orders, mut items): (Vec<crate::Order>, Vec<crate::OrderItem>)) -> Self {
Self( Self(
orders orders
.into_iter() .into_iter()
.map( .map(
|crate::AccountOrder { |crate::Order {
id, id,
buyer_id, buyer_id,
status, status,
@ -115,14 +118,16 @@ impl From<(Vec<crate::AccountOrder>, Vec<crate::OrderItem>)> for AccountOrders {
order_ext_id: _, order_ext_id: _,
service_order_id: _, service_order_id: _,
checkout_notes, checkout_notes,
address_id,
}| { }| {
AccountOrder { Order {
id, id,
buyer_id, buyer_id,
status, status,
order_id, order_id,
items: items.drain_filter(|item| item.order_id == id).collect(), items: items.drain_filter(|item| item.order_id == id).collect(),
checkout_notes, checkout_notes,
address_id,
} }
}, },
) )
@ -131,10 +136,10 @@ impl From<(Vec<crate::AccountOrder>, Vec<crate::OrderItem>)> for AccountOrders {
} }
} }
impl From<(crate::AccountOrder, Vec<crate::OrderItem>)> for AccountOrder { impl From<(crate::Order, Vec<crate::OrderItem>)> for Order {
fn from( fn from(
( (
crate::AccountOrder { crate::Order {
id, id,
buyer_id, buyer_id,
status, status,
@ -142,30 +147,33 @@ impl From<(crate::AccountOrder, Vec<crate::OrderItem>)> for AccountOrder {
order_ext_id: _, order_ext_id: _,
service_order_id: _, service_order_id: _,
checkout_notes, checkout_notes,
address_id,
}, },
mut items, mut items,
): (crate::AccountOrder, Vec<crate::OrderItem>), ): (crate::Order, Vec<crate::OrderItem>),
) -> Self { ) -> Self {
AccountOrder { Order {
id, id,
buyer_id, buyer_id,
status, status,
order_id, order_id,
items: items.drain_filter(|item| item.order_id == id).collect(), items: items.drain_filter(|item| item.order_id == id).collect(),
checkout_notes, checkout_notes,
address_id,
} }
} }
} }
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct AccountOrder { pub struct Order {
pub id: crate::AccountOrderId, pub id: crate::OrderId,
pub buyer_id: crate::AccountId, pub buyer_id: crate::AccountId,
pub status: crate::OrderStatus, pub status: crate::OrderStatus,
pub order_id: Option<crate::OrderId>, pub order_id: Option<crate::ExtOrderId>,
pub items: Vec<crate::OrderItem>, pub items: Vec<crate::OrderItem>,
pub checkout_notes: Option<String>, pub checkout_notes: Option<String>,
pub address_id: OrderAddressId,
} }
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
@ -436,6 +444,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>,
} }
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
@ -482,6 +491,27 @@ pub struct SearchRequest {
pub lang: String, pub lang: String,
} }
#[derive(Serialize, Deserialize, Debug)]
pub struct CreateOrderAddress {
pub name: Name,
pub email: Email,
pub street: Street,
pub city: City,
pub country: Country,
pub zip: Zip,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct UpdateOrderAddress {
pub id: OrderAddressId,
pub name: Name,
pub email: Email,
pub street: Street,
pub city: City,
pub country: Country,
pub zip: Zip,
}
pub mod admin { pub mod admin {
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};

View File

@ -342,7 +342,7 @@ impl Login {
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Debug, Clone, Deref, DerefMut, From, Display)] #[derive(Serialize, Debug, Clone, Default, Deref, DerefMut, From, Display)]
#[serde(transparent)] #[serde(transparent)]
pub struct Email(String); pub struct Email(String);
@ -843,42 +843,51 @@ pub struct Stock {
#[cfg_attr(feature = "db", sqlx(transparent))] #[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Display, Deref)] #[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Display, Deref)]
#[serde(transparent)] #[serde(transparent)]
pub struct AccountOrderId(RecordId); pub struct OrderAddressId(RecordId);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Display, Deref)]
#[serde(transparent)]
pub struct OrderId(RecordId);
#[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(transparent))] #[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Display, Deref)] #[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Display, Deref)]
#[serde(transparent)] #[serde(transparent)]
pub struct OrderId(String); pub struct ExtOrderId(String);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct AccountOrder { pub struct Order {
pub id: AccountOrderId, pub id: OrderId,
pub buyer_id: AccountId, pub buyer_id: AccountId,
pub status: OrderStatus, pub status: OrderStatus,
pub order_id: Option<OrderId>, pub order_id: Option<ExtOrderId>,
pub order_ext_id: uuid::Uuid, pub order_ext_id: uuid::Uuid,
pub service_order_id: Option<String>, pub service_order_id: Option<String>,
pub checkout_notes: Option<String>, pub checkout_notes: Option<String>,
pub address_id: OrderAddressId,
} }
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize)] #[derive(Serialize, Deserialize)]
pub struct PublicAccountOrder { pub struct PublicAccountOrder {
pub id: AccountOrderId, pub id: OrderId,
pub buyer_id: AccountId, pub buyer_id: AccountId,
pub status: OrderStatus, pub status: OrderStatus,
pub order_id: Option<OrderId>, pub order_id: Option<ExtOrderId>,
pub checkout_notes: String, pub checkout_notes: String,
pub address_id: OrderAddressId,
} }
impl From<AccountOrder> for PublicAccountOrder { impl From<Order> for PublicAccountOrder {
fn from( fn from(
AccountOrder { Order {
id, id,
buyer_id, buyer_id,
status, status,
@ -886,7 +895,8 @@ impl From<AccountOrder> for PublicAccountOrder {
order_ext_id: _, order_ext_id: _,
service_order_id: _, service_order_id: _,
checkout_notes, checkout_notes,
}: AccountOrder, address_id,
}: Order,
) -> Self { ) -> Self {
Self { Self {
id, id,
@ -894,6 +904,7 @@ impl From<AccountOrder> for PublicAccountOrder {
status, status,
order_id, order_id,
checkout_notes: checkout_notes.unwrap_or_default(), checkout_notes: checkout_notes.unwrap_or_default(),
address_id,
} }
} }
} }
@ -910,7 +921,7 @@ pub struct OrderItemId(pub RecordId);
pub struct OrderItem { pub struct OrderItem {
pub id: OrderItemId, pub id: OrderItemId,
pub product_id: ProductId, pub product_id: ProductId,
pub order_id: AccountOrderId, pub order_id: OrderId,
pub quantity: Quantity, pub quantity: Quantity,
pub quantity_unit: QuantityUnit, pub quantity_unit: QuantityUnit,
} }
@ -1128,7 +1139,7 @@ pub struct AddressId(RecordId);
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, From, Display)] #[derive(Serialize, Deserialize, Debug, Clone, Default, Deref, DerefMut, From, Display)]
#[serde(transparent)] #[serde(transparent)]
pub struct Name(String); pub struct Name(String);
@ -1140,7 +1151,7 @@ impl Name {
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, From, Display)] #[derive(Serialize, Deserialize, Debug, Clone, Default, Deref, DerefMut, From, Display)]
#[serde(transparent)] #[serde(transparent)]
pub struct Street(String); pub struct Street(String);
@ -1152,7 +1163,7 @@ impl Street {
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, From, Display)] #[derive(Serialize, Deserialize, Debug, Clone, Default, Deref, DerefMut, From, Display)]
#[serde(transparent)] #[serde(transparent)]
pub struct City(String); pub struct City(String);
@ -1164,7 +1175,7 @@ impl City {
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, From, Display)] #[derive(Serialize, Deserialize, Debug, Clone, Default, Deref, DerefMut, From, Display)]
#[serde(transparent)] #[serde(transparent)]
pub struct Country(String); pub struct Country(String);
@ -1176,7 +1187,7 @@ impl Country {
#[cfg_attr(feature = "db", derive(sqlx::Type))] #[cfg_attr(feature = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))] #[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Debug, Clone, Deref, DerefMut, From, Display)] #[derive(Serialize, Deserialize, Debug, Clone, Default, Deref, DerefMut, From, Display)]
#[serde(transparent)] #[serde(transparent)]
pub struct Zip(String); pub struct Zip(String);
@ -1189,7 +1200,7 @@ impl Zip {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))] #[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))] #[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub struct Address { pub struct AccountAddress {
pub id: AddressId, pub id: AddressId,
pub name: Name, pub name: Name,
pub email: Email, pub email: Email,
@ -1198,4 +1209,18 @@ pub struct Address {
pub country: Country, pub country: Country,
pub zip: Zip, pub zip: Zip,
pub account_id: AccountId, pub account_id: AccountId,
pub is_default: bool,
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize, Debug)]
pub struct OrderAddress {
pub id: OrderAddressId,
pub name: Name,
pub email: Email,
pub street: Street,
pub city: City,
pub country: Country,
pub zip: Zip,
} }

View File

@ -117,3 +117,24 @@ pub async fn update_cart(
) )
.await .await
} }
pub async fn place_order(access_token: AccessTokenString) -> NetRes<String> {
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,
};
perform(
Request::new("/api/v1/order")
.method(Method::Post)
.header(Header::bearer(access_token.as_str()))
.json(&input)
.map_err(NetRes::Http)?,
)
.await
}

View File

@ -1,3 +1,5 @@
use std::str::FromStr;
use seed::prelude::*; use seed::prelude::*;
use seed::*; use seed::*;
@ -7,11 +9,28 @@ use crate::NetRes;
#[derive(Debug)] #[derive(Debug)]
pub enum CheckoutMsg { pub enum CheckoutMsg {
ProductsFetched(NetRes<model::api::Products>), ProductsFetched(NetRes<model::api::Products>),
AddressNameChanged(String),
AddressEmailChanged(String),
AddressStreetChanged(String),
AddressCityChanged(String),
AddressCountryChanged(String),
AddressZipChanged(String),
}
#[derive(Debug, Default)]
pub struct AddressForm {
pub name: model::Name,
pub email: model::Email,
pub street: model::Street,
pub city: model::City,
pub country: model::Country,
pub zip: model::Zip,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct CheckoutPage { pub struct CheckoutPage {
pub products: Products, pub products: Products,
pub address: AddressForm,
} }
pub fn init(_url: Url, orders: &mut impl Orders<crate::Msg>) -> CheckoutPage { pub fn init(_url: Url, orders: &mut impl Orders<crate::Msg>) -> CheckoutPage {
@ -22,6 +41,7 @@ pub fn init(_url: Url, orders: &mut impl Orders<crate::Msg>) -> CheckoutPage {
}); });
CheckoutPage { CheckoutPage {
products: Default::default(), products: Default::default(),
address: AddressForm::default(),
} }
} }
@ -38,6 +58,26 @@ 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) => {
model.address.name = model::Name::new(value);
}
CheckoutMsg::AddressEmailChanged(value) => {
if let Ok(value) = model::Email::from_str(&value) {
model.address.email = value;
}
}
CheckoutMsg::AddressStreetChanged(value) => {
model.address.street = model::Street::new(value);
}
CheckoutMsg::AddressCityChanged(value) => {
model.address.city = model::City::new(value);
}
CheckoutMsg::AddressCountryChanged(value) => {
model.address.country = model::Country::new(value);
}
CheckoutMsg::AddressZipChanged(value) => {
model.address.zip = model::Zip::new(value);
}
} }
} }
@ -182,7 +222,8 @@ mod right_side {
use seed::prelude::*; use seed::prelude::*;
use seed::*; use seed::*;
use crate::pages::public::checkout::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;
@ -204,11 +245,15 @@ mod right_side {
] ]
} }
fn contact(model: &crate::Model, _page: &CheckoutPage) -> Node<Msg> { fn contact(model: &crate::Model, page: &CheckoutPage) -> Node<Msg> {
if model.shared.me.is_some() { match &model.shared.me {
// TODO: Display user addresses Some(me) if me.addresses.is_empty() => contact_form(model, page),
return empty![]; Some(_me) => empty![],
None => contact_form(model, page),
} }
}
fn contact_form(model: &crate::Model, _page: &CheckoutPage) -> Node<Msg> {
div![ div![
C!["w-full mx-auto rounded-lg bg-white border border-gray-200 p-3 text-gray-800 font-light mb-6"], C!["w-full mx-auto rounded-lg bg-white border border-gray-200 p-3 text-gray-800 font-light mb-6"],
form![ form![
@ -219,19 +264,13 @@ mod right_side {
div![ div![
C!["mb-3"], C!["mb-3"],
div![ div![
input![ address_input(model, "client-name", "text", "Name", CheckoutMsg::AddressNameChanged),
C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"],
attrs![At::Id => "client-name", At::Placeholder => model.i18n.t("Name")]
]
], ],
], ],
div![ div![
C!["mb-3"], C!["mb-3"],
div![ div![
input![ address_input(model, "client-email", "email", "E-Mail", CheckoutMsg::AddressEmailChanged),
C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"],
attrs![At::Id => "client-email", At::Type => "email", At::Placeholder => model.i18n.t("E-Mail")]
]
], ],
], ],
div![ div![
@ -240,36 +279,47 @@ mod right_side {
], ],
div![ div![
C!["mb-3"], C!["mb-3"],
input![ address_input(model, "client-street", "text", "Street", CheckoutMsg::AddressStreetChanged),
C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"],
attrs![At::Id => "client-street", At::Placeholder => model.i18n.t("Street")]
]
], ],
div![ div![
C!["mb-3"], C!["mb-3"],
input![ address_input(model, "client-city", "text", "City", CheckoutMsg::AddressCityChanged),
C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"],
attrs![At::Id => "client-city", At::Placeholder => model.i18n.t("City")]
]
], ],
div![ div![
C!["mb-3 inline-block w-1/2 pr-1"], C!["mb-3 inline-block w-1/2 pr-1"],
input![ address_input(model, "client-country", "text", "Country", CheckoutMsg::AddressCountryChanged),
C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"],
attrs![At::Id => "client-country", At::Placeholder => model.i18n.t("Country")]
]
], ],
div![ div![
C!["mb-3 inline-block -mx-1 pl-1 w-1/2"], C!["mb-3 inline-block -mx-1 pl-1 w-1/2"],
input![ address_input(model, "client-zip", "text", "Zip", CheckoutMsg::AddressZipChanged),
C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"],
attrs![At::Id => "client-zip", At::Placeholder => model.i18n.t("Zip")]
]
], ],
] ]
] ]
} }
fn address_input<F>(
model: &crate::Model,
id: &str,
ty: &str,
label: &'static str,
msg: F,
) -> Node<Msg>
where
F: Clone + 'static + Fn(String) -> CheckoutMsg,
{
let handler = ev(Ev::Change, move |ev| {
ev.prevent_default();
let target = ev.target()?;
let input = seed::to_input(&target);
Some(crate::Msg::from(msg(input.value())))
});
input![
C!["w-full px-3 py-2 mb-1 border border-gray-200 rounded-md focus:outline-none focus:border-indigo-500 transition-colors"],
attrs![At::Id => id, At::Type => ty, At::Placeholder => model.i18n.t(label), At::Required => true],
handler,
]
}
fn pay_now(model: &crate::Model) -> Node<Msg> { fn pay_now(model: &crate::Model) -> Node<Msg> {
div![ div![
button![ button![
@ -292,13 +342,7 @@ mod right_side {
} }
fn pay_u(model: &crate::Model) -> Node<Msg> { fn pay_u(model: &crate::Model) -> Node<Msg> {
payment_input( payment_input(model, PaymentMethod::PayU, "pay_u", "PayU", pay_u_icon())
model,
model::PaymentMethod::PayU,
"pay_u",
"PayU",
pay_u_icon(),
)
} }
fn pay_u_icon() -> Node<Msg> { fn pay_u_icon() -> Node<Msg> {
@ -329,7 +373,7 @@ mod right_side {
fn pay_on_spot(model: &crate::Model) -> Node<Msg> { fn pay_on_spot(model: &crate::Model) -> Node<Msg> {
payment_input( payment_input(
model, model,
model::PaymentMethod::PaymentOnTheSpot, PaymentMethod::PaymentOnTheSpot,
"pay_on_spot", "pay_on_spot",
"Pay on spot", "Pay on spot",
pay_in_spot_icon(), pay_in_spot_icon(),
@ -397,7 +441,7 @@ mod right_side {
fn payment_input( fn payment_input(
model: &crate::Model, model: &crate::Model,
method: model::PaymentMethod, method: PaymentMethod,
name: &str, name: &str,
label: &'static str, label: &'static str,
icon: Node<Msg>, icon: Node<Msg>,

View File

@ -56,7 +56,6 @@ pub fn update(msg: RegisterMsg, model: &mut SignUpPage, orders: &mut impl Orders
let email = model.email.clone(); let email = model.email.clone();
let login = model.login.clone(); let login = model.login.clone();
let password = model.password.clone(); let password = model.password.clone();
let password_confirmation = model.password_confirmation.clone();
orders.perform_cmd(async move { orders.perform_cmd(async move {
crate::Msg::Public( crate::Msg::Public(
@ -65,7 +64,6 @@ pub fn update(msg: RegisterMsg, model: &mut SignUpPage, orders: &mut impl Orders
email, email,
login, login,
password, password,
password_confirmation,
}) })
.await, .await,
) )
@ -84,16 +82,15 @@ pub fn update(msg: RegisterMsg, model: &mut SignUpPage, orders: &mut impl Orders
pub fn view(model: &crate::Model, page: &SignUpPage) -> Node<crate::Msg> { pub fn view(model: &crate::Model, page: &SignUpPage) -> Node<crate::Msg> {
let home = Urls::new(&model.url).home(); let home = Urls::new(&model.url).home();
let logo = model let logo = model.logo.as_deref().map_or_else(
.logo || a![attrs![At::Href => home], "Logo"],
.as_deref() |src| {
.map(|src| {
a![ a![
attrs![At::Href => home], attrs![At::Href => home],
img![attrs![At::Src => src], C!["m-auto"]] img![attrs![At::Src => src], C!["m-auto"]]
] ]
}) },
.unwrap_or_else(|| a![attrs![At::Href => home], "Logo"]); );
let content = div![ let content = div![
C!["relative flex flex-col justify-center overflow-hidden"], C!["relative flex flex-col justify-center overflow-hidden"],
@ -129,51 +126,54 @@ fn sign_up_form(model: &crate::Model, _page: &SignUpPage) -> Node<RegisterMsg> {
RegisterMsg::Submit RegisterMsg::Submit
}), }),
div![ div![
label![attrs!["for" => "email"], C!["block text-sm text-indigo-800"], model.i18n.t("E-Mail")], label![attrs![At::For => "email"], C!["block text-sm text-indigo-800"], model.i18n.t("E-Mail")],
input![ input![
attrs!["type" => "email", "id" => "email"], attrs![At::Type => "email", At::Id => "email"],
C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"], C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"],
ev(Ev::Change, |ev| { ev(Ev::Change, |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::EmailChanged) ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::EmailChanged)
}) }),
] ]
], ],
div![ div![
label![attrs!["for" => "login"], C!["block text-sm text-indigo-800"], model.i18n.t("Login")], label![attrs![At::For => "login"], C!["block text-sm text-indigo-800"], model.i18n.t("Login")],
input![ input![
attrs!["type" => "text", "id" => "login"], attrs![At::Type => "text", At::Id => "login"],
C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"], C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"],
ev(Ev::Change, |ev| { ev(Ev::Change, |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::LoginChanged) ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::LoginChanged)
}) }),
] ]
], ],
div![ div![
C!["mt-4"], C!["mt-4"],
div![ div![
label![attrs!["for" => "password"], C!["block text-sm text-indigo-800"], model.i18n.t("Password")], label![attrs![At::For => "password"], C!["block text-sm text-indigo-800"], model.i18n.t("Password")],
input![attrs!["type" => "password", "id" => "password"], C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"]], input![
attrs![At::Type => "password", At::Id => "password"],
C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"],
ev(Ev::Change, |ev| { ev(Ev::Change, |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::PasswordChanged) ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::PasswordChanged)
}) })
]
], ],
div![ div![
label![ label![
attrs!["for" => "password-confirmation"], attrs![At::For => "password-confirmation"],
C!["block text-sm text-indigo-800"], C!["block text-sm text-indigo-800"],
model.i18n.t("Password confirmation") model.i18n.t("Password confirmation")
], ],
input![ input![
attrs!["type" => "password", "id" => "password-confirmation"], attrs![At::Type => "password", At::Id => "password-confirmation"],
C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"] C!["block w-full px-4 py-2 mt-2 text-indigo-700 bg-white border rounded-md focus:border-indigo-400 focus:ring-indigo-300 focus:outline-none focus:ring focus:ring-opacity-40"],
],
ev(Ev::Change, |ev| { ev(Ev::Change, |ev| {
ev.stop_propagation(); ev.stop_propagation();
ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::PasswordConfirmationChanged) ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::PasswordConfirmationChanged)
}) }),
]
], ],
div![ div![
C!["mt-6"], C!["mt-6"],