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 account: FullAccount,
pub addresses: Vec<model::Address>,
pub addresses: Vec<model::AccountAddress>,
}
#[derive(actix::Message, Debug)]

View File

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

View File

@ -3,10 +3,11 @@ use config::SharedAppConfig;
use sqlx::PgPool;
use sqlx_core::arguments::Arguments;
pub use crate::account_orders::*;
pub use crate::account_addresses::*;
pub use crate::accounts::*;
pub use crate::addresses::*;
pub use crate::order_addresses::*;
pub use crate::order_items::*;
pub use crate::orders::*;
pub use crate::photos::*;
pub use crate::product_photos::*;
pub use crate::products::*;
@ -15,10 +16,11 @@ pub use crate::shopping_carts::*;
pub use crate::stocks::*;
pub use crate::tokens::*;
pub mod account_orders;
pub mod account_addresses;
pub mod accounts;
pub mod addresses;
pub mod order_addresses;
pub mod order_items;
pub mod orders;
pub mod photos;
pub mod product_photos;
pub mod products;
@ -128,7 +130,7 @@ pub enum Error {
#[error("{0}")]
Account(#[from] accounts::Error),
#[error("{0}")]
AccountOrder(#[from] account_orders::Error),
AccountOrder(#[from] orders::Error),
#[error("{0}")]
Product(#[from] products::Error),
#[error("{0}")]
@ -146,7 +148,9 @@ pub enum Error {
#[error("{0}")]
ProductPhoto(#[from] product_photos::Error),
#[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")]
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>")]
pub struct CreateOrderItem {
pub product_id: ProductId,
pub order_id: AccountOrderId,
pub order_id: OrderId,
pub quantity: Quantity,
pub quantity_unit: QuantityUnit,
}
@ -109,7 +109,7 @@ WHERE id = $1
#[derive(actix::Message)]
#[rtype(result = "Result<Vec<OrderItem>>")]
pub struct OrderItems {
pub order_id: model::AccountOrderId,
pub order_id: model::OrderId,
}
db_async_handler!(OrderItems, order_items, Vec<OrderItem>);

View File

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

View File

@ -1,7 +1,7 @@
use actix::Message;
use config::SharedAppConfig;
use database_manager::{query_db, SharedDatabase};
use model::{AccountId, AccountOrder, OrderStatus, ShoppingCart, ShoppingCartId, ShoppingCartItem};
use model::{AccountId, Order, OrderStatus, ShoppingCart, ShoppingCartItem};
#[macro_export]
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)]
#[serde(rename_all = "kebab-case", tag = "order")]
pub enum Error {
@ -28,6 +49,12 @@ pub enum Error {
ShoppingCart,
#[error("Failed to create account order")]
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>;
@ -47,24 +74,34 @@ impl OrderManager {
}
}
#[derive(Message, Debug)]
#[rtype(result = "Result<AccountOrder>")]
pub struct CreateOrder {
pub account_id: AccountId,
pub shopping_cart_id: ShoppingCartId,
#[derive(Debug)]
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,
}
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(
msg: CreateOrder,
order_async_handler!(CreateAccountOrder, create_account_order, Order);
pub(crate) async fn create_account_order(
msg: CreateAccountOrder,
db: SharedDatabase,
_config: SharedAppConfig,
) -> Result<AccountOrder> {
) -> Result<Order> {
let cart: ShoppingCart = query_db!(
db,
database_manager::FindShoppingCart {
id: msg.shopping_cart_id,
database_manager::EnsureActiveShoppingCart {
buyer_id: msg.account_id
},
Error::ShoppingCart,
Error::DatabaseInternal
@ -78,6 +115,44 @@ pub(crate) async fn create_order(
Error::ShoppingCart,
Error::DatabaseInternal
);
let address: model::AccountAddress = if let Some(input) = msg.create_address {
query_db!(
db,
database_manager::CreateAccountAddress {
name: input.name,
email: input.email,
street: input.street,
city: input.city,
country: input.country,
zip: input.zip,
account_id: None,
is_default: true,
},
Error::InvalidAccountAddress
)
} else {
query_db!(
db,
database_manager::DefaultAccountAddress {
account_id: cart.buyer_id
},
Error::NoAddress
)
};
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!(
db,
database_manager::CreateAccountOrder {

View File

@ -151,7 +151,7 @@ impl From<Product> for pay_u::Product {
}
pub struct CreatePaymentResult {
pub order: model::AccountOrder,
pub order: model::Order,
pub items: Vec<model::OrderItem>,
pub redirect_uri: String,
}
@ -215,7 +215,7 @@ pub(crate) async fn request_payment(
Error::UnavailableShoppingCart
);
let db_order: model::AccountOrder = query_db!(
let db_order: model::Order = query_db!(
db,
database_manager::CreateAccountOrder {
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 config::SharedAppConfig;
use database_manager::Database;
use model::{AccountId, AccountState, Address, Encrypt, PasswordConfirmation};
use model::{AccountAddress, AccountId, AccountState, Encrypt, PasswordConfirmation};
use token_manager::TokenManager;
use crate::routes::admin::Error;
@ -112,7 +112,7 @@ pub async fn create_account(
role: payload.role,
}
);
let addresses: Vec<Address> = admin_send_db!(
let addresses: Vec<AccountAddress> = admin_send_db!(
db,
database_manager::AccountAddresses {
account_id: account.id

View File

@ -3,7 +3,7 @@ use actix_web::get;
use actix_web::web::{Data, Json, ServiceConfig};
use actix_web_httpauth::extractors::bearer::BearerAuth;
use database_manager::Database;
use model::api::AccountOrders;
use model::api::Orders;
use token_manager::TokenManager;
use crate::routes::RequireUser;
@ -14,10 +14,10 @@ async fn orders(
credentials: BearerAuth,
tm: Data<Addr<TokenManager>>,
db: Data<Addr<Database>>,
) -> routes::Result<Json<AccountOrders>> {
) -> routes::Result<Json<Orders>> {
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);
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 database_manager::{query_db, Database};
use model::api;
use order_manager::{query_order, OrderManager};
use payment_manager::{query_pay, PaymentManager};
use token_manager::TokenManager;
@ -234,6 +235,7 @@ pub(crate) async fn create_order(
tm: Data<Addr<TokenManager>>,
credentials: BearerAuth,
payment: Data<Addr<PaymentManager>>,
order: Data<Addr<OrderManager>>,
) -> routes::Result<HttpResponse> {
let account_id = credentials
.require_user(tm.into_inner())
@ -248,6 +250,7 @@ pub(crate) async fn create_order(
language,
charge_client,
currency,
address,
} = payload;
let ip = match req.peer_addr() {
Some(ip) => ip,
@ -272,6 +275,32 @@ pub(crate) async fn create_order(
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()
.append_header(("Location", redirect_uri.as_str()))
.body(format!(

View File

@ -48,6 +48,8 @@ macro_rules! public_send_db {
pub enum Error {
#[error("{0}")]
ApiV1(#[from] api_v1::Error),
#[error("Failed to place order")]
PlaceOrder,
#[error("Internal server error")]
DatabaseConnection,
#[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 customer_id: uuid::Uuid,
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(
(
FullAccount {
@ -44,7 +44,7 @@ impl From<(FullAccount, Vec<crate::Address>)> for Account {
state,
},
addresses,
): (FullAccount, Vec<crate::Address>),
): (FullAccount, Vec<crate::AccountAddress>),
) -> Self {
Self {
id,
@ -59,7 +59,7 @@ impl From<(FullAccount, Vec<crate::Address>)> for Account {
}
#[derive(Serialize, Deserialize, Debug)]
pub struct Address {
pub struct AccountAddress {
pub id: AddressId,
pub name: Name,
pub email: Email,
@ -68,11 +68,12 @@ pub struct Address {
pub country: Country,
pub zip: Zip,
pub account_id: AccountId,
pub is_default: bool,
}
impl From<crate::Address> for Address {
impl From<crate::AccountAddress> for AccountAddress {
fn from(
crate::Address {
crate::AccountAddress {
id,
name,
email,
@ -81,7 +82,8 @@ impl From<crate::Address> for Address {
country,
zip,
account_id,
}: crate::Address,
is_default,
}: crate::AccountAddress,
) -> Self {
Self {
id,
@ -92,6 +94,7 @@ impl From<crate::Address> for Address {
country,
zip,
account_id,
is_default,
}
}
}
@ -99,15 +102,15 @@ impl From<crate::Address> for Address {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[derive(Serialize, Deserialize, Debug)]
#[serde(transparent)]
pub struct AccountOrders(pub Vec<AccountOrder>);
pub struct Orders(pub Vec<Order>);
impl From<(Vec<crate::AccountOrder>, Vec<crate::OrderItem>)> for AccountOrders {
fn from((orders, mut items): (Vec<crate::AccountOrder>, Vec<crate::OrderItem>)) -> Self {
impl From<(Vec<crate::Order>, Vec<crate::OrderItem>)> for Orders {
fn from((orders, mut items): (Vec<crate::Order>, Vec<crate::OrderItem>)) -> Self {
Self(
orders
.into_iter()
.map(
|crate::AccountOrder {
|crate::Order {
id,
buyer_id,
status,
@ -115,14 +118,16 @@ impl From<(Vec<crate::AccountOrder>, Vec<crate::OrderItem>)> for AccountOrders {
order_ext_id: _,
service_order_id: _,
checkout_notes,
address_id,
}| {
AccountOrder {
Order {
id,
buyer_id,
status,
order_id,
items: items.drain_filter(|item| item.order_id == id).collect(),
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(
(
crate::AccountOrder {
crate::Order {
id,
buyer_id,
status,
@ -142,30 +147,33 @@ impl From<(crate::AccountOrder, Vec<crate::OrderItem>)> for AccountOrder {
order_ext_id: _,
service_order_id: _,
checkout_notes,
address_id,
},
mut items,
): (crate::AccountOrder, Vec<crate::OrderItem>),
): (crate::Order, Vec<crate::OrderItem>),
) -> Self {
AccountOrder {
Order {
id,
buyer_id,
status,
order_id,
items: items.drain_filter(|item| item.order_id == id).collect(),
checkout_notes,
address_id,
}
}
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[derive(Serialize, Deserialize, Debug)]
pub struct AccountOrder {
pub id: crate::AccountOrderId,
pub struct Order {
pub id: crate::OrderId,
pub buyer_id: crate::AccountId,
pub status: crate::OrderStatus,
pub order_id: Option<crate::OrderId>,
pub order_id: Option<crate::ExtOrderId>,
pub items: Vec<crate::OrderItem>,
pub checkout_notes: Option<String>,
pub address_id: OrderAddressId,
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
@ -436,6 +444,7 @@ pub struct CreateOrderInput {
pub charge_client: bool,
/// User currency
pub currency: String,
pub address: Option<CreateOrderAddress>,
}
#[derive(Serialize, Deserialize, Debug)]
@ -482,6 +491,27 @@ pub struct SearchRequest {
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 {
use serde::{Deserialize, Serialize};

View File

@ -342,7 +342,7 @@ impl Login {
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[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)]
pub struct Email(String);
@ -843,42 +843,51 @@ pub struct Stock {
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Copy, Clone, Debug, PartialEq, Display, Deref)]
#[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 = "db", derive(sqlx::Type))]
#[cfg_attr(feature = "db", sqlx(transparent))]
#[derive(Serialize, Deserialize, Clone, Debug, PartialEq, Display, Deref)]
#[serde(transparent)]
pub struct OrderId(String);
pub struct ExtOrderId(String);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize)]
pub struct AccountOrder {
pub id: AccountOrderId,
pub struct Order {
pub id: OrderId,
pub buyer_id: AccountId,
pub status: OrderStatus,
pub order_id: Option<OrderId>,
pub order_id: Option<ExtOrderId>,
pub order_ext_id: uuid::Uuid,
pub service_order_id: Option<String>,
pub checkout_notes: Option<String>,
pub address_id: OrderAddressId,
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize)]
pub struct PublicAccountOrder {
pub id: AccountOrderId,
pub id: OrderId,
pub buyer_id: AccountId,
pub status: OrderStatus,
pub order_id: Option<OrderId>,
pub order_id: Option<ExtOrderId>,
pub checkout_notes: String,
pub address_id: OrderAddressId,
}
impl From<AccountOrder> for PublicAccountOrder {
impl From<Order> for PublicAccountOrder {
fn from(
AccountOrder {
Order {
id,
buyer_id,
status,
@ -886,7 +895,8 @@ impl From<AccountOrder> for PublicAccountOrder {
order_ext_id: _,
service_order_id: _,
checkout_notes,
}: AccountOrder,
address_id,
}: Order,
) -> Self {
Self {
id,
@ -894,6 +904,7 @@ impl From<AccountOrder> for PublicAccountOrder {
status,
order_id,
checkout_notes: checkout_notes.unwrap_or_default(),
address_id,
}
}
}
@ -910,7 +921,7 @@ pub struct OrderItemId(pub RecordId);
pub struct OrderItem {
pub id: OrderItemId,
pub product_id: ProductId,
pub order_id: AccountOrderId,
pub order_id: OrderId,
pub quantity: Quantity,
pub quantity_unit: QuantityUnit,
}
@ -1128,7 +1139,7 @@ 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)]
#[derive(Serialize, Deserialize, Debug, Clone, Default, Deref, DerefMut, From, Display)]
#[serde(transparent)]
pub struct Name(String);
@ -1140,7 +1151,7 @@ impl Name {
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[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)]
pub struct Street(String);
@ -1152,7 +1163,7 @@ impl Street {
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[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)]
pub struct City(String);
@ -1164,7 +1175,7 @@ impl City {
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[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)]
pub struct Country(String);
@ -1176,7 +1187,7 @@ impl Country {
#[cfg_attr(feature = "db", derive(sqlx::Type))]
#[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)]
pub struct Zip(String);
@ -1189,7 +1200,7 @@ impl Zip {
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize, Debug)]
pub struct Address {
pub struct AccountAddress {
pub id: AddressId,
pub name: Name,
pub email: Email,
@ -1198,4 +1209,18 @@ pub struct Address {
pub country: Country,
pub zip: Zip,
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
}
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::*;
@ -7,11 +9,28 @@ use crate::NetRes;
#[derive(Debug)]
pub enum CheckoutMsg {
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)]
pub struct CheckoutPage {
pub products: Products,
pub address: AddressForm,
}
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 {
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)) => {
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::*;
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::Msg;
@ -204,11 +245,15 @@ mod right_side {
]
}
fn contact(model: &crate::Model, _page: &CheckoutPage) -> Node<Msg> {
if model.shared.me.is_some() {
// TODO: Display user addresses
return empty![];
fn contact(model: &crate::Model, page: &CheckoutPage) -> Node<Msg> {
match &model.shared.me {
Some(me) if me.addresses.is_empty() => contact_form(model, page),
Some(_me) => empty![],
None => contact_form(model, page),
}
}
fn contact_form(model: &crate::Model, _page: &CheckoutPage) -> Node<Msg> {
div![
C!["w-full mx-auto rounded-lg bg-white border border-gray-200 p-3 text-gray-800 font-light mb-6"],
form![
@ -219,19 +264,13 @@ mod right_side {
div![
C!["mb-3"],
div![
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 => "client-name", At::Placeholder => model.i18n.t("Name")]
]
address_input(model, "client-name", "text", "Name", CheckoutMsg::AddressNameChanged),
],
],
div![
C!["mb-3"],
div![
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 => "client-email", At::Type => "email", At::Placeholder => model.i18n.t("E-Mail")]
]
address_input(model, "client-email", "email", "E-Mail", CheckoutMsg::AddressEmailChanged),
],
],
div![
@ -240,36 +279,47 @@ mod right_side {
],
div![
C!["mb-3"],
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 => "client-street", At::Placeholder => model.i18n.t("Street")]
]
address_input(model, "client-street", "text", "Street", CheckoutMsg::AddressStreetChanged),
],
div![
C!["mb-3"],
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 => "client-city", At::Placeholder => model.i18n.t("City")]
]
address_input(model, "client-city", "text", "City", CheckoutMsg::AddressCityChanged),
],
div![
C!["mb-3 inline-block w-1/2 pr-1"],
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 => "client-country", At::Placeholder => model.i18n.t("Country")]
]
address_input(model, "client-country", "text", "Country", CheckoutMsg::AddressCountryChanged),
],
div![
C!["mb-3 inline-block -mx-1 pl-1 w-1/2"],
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 => "client-zip", At::Placeholder => model.i18n.t("Zip")]
]
address_input(model, "client-zip", "text", "Zip", CheckoutMsg::AddressZipChanged),
],
]
]
}
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> {
div![
button![
@ -292,13 +342,7 @@ mod right_side {
}
fn pay_u(model: &crate::Model) -> Node<Msg> {
payment_input(
model,
model::PaymentMethod::PayU,
"pay_u",
"PayU",
pay_u_icon(),
)
payment_input(model, PaymentMethod::PayU, "pay_u", "PayU", pay_u_icon())
}
fn pay_u_icon() -> Node<Msg> {
@ -329,7 +373,7 @@ mod right_side {
fn pay_on_spot(model: &crate::Model) -> Node<Msg> {
payment_input(
model,
model::PaymentMethod::PaymentOnTheSpot,
PaymentMethod::PaymentOnTheSpot,
"pay_on_spot",
"Pay on spot",
pay_in_spot_icon(),
@ -397,7 +441,7 @@ mod right_side {
fn payment_input(
model: &crate::Model,
method: model::PaymentMethod,
method: PaymentMethod,
name: &str,
label: &'static str,
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 login = model.login.clone();
let password = model.password.clone();
let password_confirmation = model.password_confirmation.clone();
orders.perform_cmd(async move {
crate::Msg::Public(
@ -65,7 +64,6 @@ pub fn update(msg: RegisterMsg, model: &mut SignUpPage, orders: &mut impl Orders
email,
login,
password,
password_confirmation,
})
.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> {
let home = Urls::new(&model.url).home();
let logo = model
.logo
.as_deref()
.map(|src| {
let logo = model.logo.as_deref().map_or_else(
|| a![attrs![At::Href => home], "Logo"],
|src| {
a![
attrs![At::Href => home],
img![attrs![At::Src => src], C!["m-auto"]]
]
})
.unwrap_or_else(|| a![attrs![At::Href => home], "Logo"]);
},
);
let content = div![
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
}),
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![
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"],
ev(Ev::Change, |ev| {
ev.stop_propagation();
ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::EmailChanged)
})
}),
]
],
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![
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"],
ev(Ev::Change, |ev| {
ev.stop_propagation();
ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::LoginChanged)
})
}),
]
],
div![
C!["mt-4"],
div![
label![attrs!["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"]],
ev(Ev::Change, |ev| {
ev.stop_propagation();
ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::PasswordChanged)
})
label![attrs![At::For => "password"], C!["block text-sm text-indigo-800"], model.i18n.t("Password")],
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.stop_propagation();
ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::PasswordChanged)
})
]
],
div![
label![
attrs!["for" => "password-confirmation"],
attrs![At::For => "password-confirmation"],
C!["block text-sm text-indigo-800"],
model.i18n.t("Password confirmation")
],
input![
attrs!["type" => "password", "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"]
],
ev(Ev::Change, |ev| {
ev.stop_propagation();
ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::PasswordConfirmationChanged)
})
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"],
ev(Ev::Change, |ev| {
ev.stop_propagation();
ev.target().map(|target| seed::to_input(&target).value()).map(RegisterMsg::PasswordConfirmationChanged)
}),
]
],
div![
C!["mt-6"],