Add checkout notes

This commit is contained in:
Adrian Woźniak 2022-05-19 14:03:18 +02:00
parent b1b4d083b7
commit cafafb0f24
No known key found for this signature in database
GPG Key ID: 0012845A89C7352B
18 changed files with 131 additions and 72 deletions

View File

@ -1,3 +1,11 @@
#[package]
#name = "bazzar"
#version = "0.1.0"
#edition = "2021"
#
#[[bin]]
#name = "api"
#path = "./api/src/main.rs"
[workspace]
members = [
"api",

View File

@ -94,7 +94,7 @@ impl CartManager {
}
}
#[derive(actix::Message)]
#[derive(actix::Message, Debug)]
#[rtype(result = "Result<Option<ShoppingCartItem>>")]
pub struct ModifyItem {
pub buyer_id: AccountId,
@ -196,7 +196,7 @@ pub(crate) async fn remove_product(
))
}
#[derive(actix::Message)]
#[derive(actix::Message, Debug)]
#[rtype(result = "Result<Vec<ShoppingCartItem>>")]
pub struct ModifyCart {
pub buyer_id: AccountId,
@ -206,6 +206,7 @@ pub struct ModifyCart {
cart_async_handler!(ModifyCart, modify_cart, Vec<ShoppingCartItem>);
async fn modify_cart(msg: ModifyCart, db: actix::Addr<Database>) -> Result<Vec<ShoppingCartItem>> {
log::debug!("{:?}", msg);
let _cart = query_db!(
db,
database_manager::EnsureActiveShoppingCart {
@ -222,6 +223,7 @@ async fn modify_cart(msg: ModifyCart, db: actix::Addr<Database>) -> Result<Vec<S
passthrough Error::Db,
Error::CartNotAvailable
);
log::debug!("carts {:?}", carts);
let cart = if carts.is_empty() {
return Err(Error::CartNotAvailable);
} else {
@ -236,7 +238,7 @@ async fn modify_cart(msg: ModifyCart, db: actix::Addr<Database>) -> Result<Vec<S
agg
});
let mut items: Vec<model::ShoppingCartItem> = query_db!(
let items: Vec<model::ShoppingCartItem> = query_db!(
db,
database_manager::CartItems {
shopping_cart_id: cart.id
@ -244,7 +246,10 @@ async fn modify_cart(msg: ModifyCart, db: actix::Addr<Database>) -> Result<Vec<S
Error::CantModifyCart
);
for item in items.drain_filter(|item| !existing.contains(&item.product_id)) {
for item in items
.into_iter()
.filter(|item| !existing.contains(&item.product_id))
{
query_db!(
db,
database_manager::RemoveCartItem {
@ -256,27 +261,10 @@ async fn modify_cart(msg: ModifyCart, db: actix::Addr<Database>) -> Result<Vec<S
);
}
let mut out = Vec::with_capacity(items.len());
let mut out = Vec::with_capacity(msg.items.len());
for ShoppingCartItem {
id: _,
product_id,
shopping_cart_id: _,
quantity,
quantity_unit,
} in items
{
if let Some(item) = modify_item(
ModifyItem {
buyer_id: msg.buyer_id,
product_id,
quantity,
quantity_unit,
},
db.clone(),
)
.await?
{
for item in msg.items {
if let Some(item) = modify_item(item, db.clone()).await? {
out.push(item);
}
}

View File

@ -31,7 +31,7 @@ pub(crate) async fn all_account_orders(
) -> Result<Vec<AccountOrder>> {
sqlx::query_as(
r#"
SELECT id, buyer_id, status, order_ext_id, service_order_id
SELECT id, buyer_id, status, order_ext_id, service_order_id, checkout_notes
FROM account_orders
ORDER BY id DESC
"#,
@ -60,6 +60,7 @@ pub struct CreateAccountOrder {
pub buyer_id: AccountId,
pub items: Vec<create_order::OrderItem>,
pub shopping_cart_id: ShoppingCartId,
pub checkout_notes: Option<String>,
}
db_async_handler!(
@ -77,7 +78,7 @@ pub(crate) async fn create_account_order(
r#"
INSERT INTO account_orders (buyer_id, status)
VALUES ($1, $2, $3)
RETURNING id, buyer_id, status, order_ext_id, service_order_id
RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes
"#,
)
.bind(msg.buyer_id)
@ -113,6 +114,7 @@ RETURNING id, buyer_id, status, order_ext_id, service_order_id
ShoppingCartSetState {
id: msg.shopping_cart_id,
state: ShoppingCartState::Closed,
checkout_notes: msg.checkout_notes,
},
t,
)
@ -146,7 +148,7 @@ pub(crate) async fn update_account_order(
UPDATE account_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
RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes
"#,
)
.bind(msg.id)
@ -183,7 +185,7 @@ pub(crate) async fn update_account_order_by_ext(
UPDATE account_orders
SET status = $2
WHERE order_ext_id = $1
RETURNING id, buyer_id, status, order_ext_id, service_order_id
RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes
"#,
)
.bind(msg.order_ext_id)
@ -207,7 +209,7 @@ db_async_handler!(FindAccountOrder, find_account_order, AccountOrder);
pub(crate) async fn find_account_order(msg: FindAccountOrder, db: PgPool) -> Result<AccountOrder> {
sqlx::query_as(
r#"
SELECT id, buyer_id, status, order_ext_id, service_order_id
SELECT id, buyer_id, status, order_ext_id, service_order_id, checkout_notes
FROM account_orders
WHERE id = $1
"#,
@ -239,7 +241,7 @@ pub(crate) async fn set_order_service_id(
UPDATE account_orders
SET service_order_id = $2
WHERE id = $1
RETURNING id, buyer_id, status, order_ext_id, service_order_id
RETURNING id, buyer_id, status, order_ext_id, service_order_id, checkout_notes
"#,
)
.bind(msg.id)

View File

@ -172,7 +172,7 @@ pub(crate) async fn update_shopping_cart_item(
sqlx::query_as(
r#"
UPDATE shopping_cart_items
SET product_id = $2 AND shopping_cart_id = $3 AND quantity = $4 AND quantity_unit = $5
SET product_id = $2, shopping_cart_id = $3, quantity = $4, quantity_unit = $5
WHERE id = $1
RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit
"#,

View File

@ -30,7 +30,7 @@ pub(crate) async fn all_shopping_carts(
) -> Result<Vec<ShoppingCart>> {
sqlx::query_as(
r#"
SELECT id, buyer_id, payment_method, state
SELECT id, buyer_id, payment_method, state, checkout_notes
FROM shopping_carts
"#,
)
@ -62,9 +62,9 @@ pub(crate) async fn account_shopping_carts(
if let Some(state) = msg.state {
sqlx::query_as(
r#"
SELECT id, buyer_id, payment_method, state
FROM shopping_carts
WHERE buyer_id = $1 AND state = $2
SELECT id, buyer_id, payment_method, state, checkout_notes
FROM shopping_carts
WHERE buyer_id = $1 AND state = $2
"#,
)
.bind(msg.account_id)
@ -72,7 +72,7 @@ SELECT id, buyer_id, payment_method, state
} else {
sqlx::query_as(
r#"
SELECT id, buyer_id, payment_method, state
SELECT id, buyer_id, payment_method, state, checkout_notes
FROM shopping_carts
WHERE buyer_id = $1
"#,
@ -104,7 +104,7 @@ pub(crate) async fn create_shopping_cart(
r#"
INSERT INTO shopping_carts (buyer_id, payment_method)
VALUES ($1, $2)
RETURNING id, buyer_id, payment_method, state
RETURNING id, buyer_id, payment_method, state, checkout_notes
"#,
)
.bind(msg.buyer_id)
@ -137,7 +137,7 @@ pub(crate) async fn update_shopping_cart(
UPDATE shopping_carts
SET buyer_id = $2 AND payment_method = $2 AND state = $4
WHERE id = $1
RETURNING id, buyer_id, payment_method, state
RETURNING id, buyer_id, payment_method, state, checkout_notes
"#,
)
.bind(msg.id)
@ -157,38 +157,31 @@ RETURNING id, buyer_id, payment_method, state
pub struct ShoppingCartSetState {
pub id: ShoppingCartId,
pub state: ShoppingCartState,
pub checkout_notes: Option<String>,
}
db_async_handler!(
ShoppingCartSetState,
inner_shopping_cart_set_state,
ShoppingCart
shopping_cart_set_state,
ShoppingCart,
inner_shopping_cart_set_state
);
async fn inner_shopping_cart_set_state(
pub(crate) async fn shopping_cart_set_state(
msg: ShoppingCartSetState,
pool: PgPool,
pool: &mut sqlx::Transaction<'_, sqlx::Postgres>,
) -> Result<ShoppingCart> {
shopping_cart_set_state(msg, &pool).await
}
pub(crate) async fn shopping_cart_set_state<'e, E>(
msg: ShoppingCartSetState,
pool: E,
) -> Result<ShoppingCart>
where
E: sqlx::Executor<'e, Database = sqlx::Postgres>,
{
sqlx::query_as(
r#"
UPDATE shopping_carts
SET state = $2
SET state = $2, checkout_notes = $3
WHERE id = $1
RETURNING id, buyer_id, payment_method, state
RETURNING id, buyer_id, payment_method, state, checkout_notes
"#,
)
.bind(msg.id)
.bind(msg.state)
.bind(msg.checkout_notes)
.fetch_one(pool)
.await
.map_err(|e| {
@ -208,7 +201,7 @@ db_async_handler!(FindShoppingCart, find_shopping_cart, ShoppingCart);
pub(crate) async fn find_shopping_cart(msg: FindShoppingCart, db: PgPool) -> Result<ShoppingCart> {
sqlx::query_as(
r#"
SELECT id, buyer_id, payment_method, state
SELECT id, buyer_id, payment_method, state, checkout_notes
FROM shopping_carts
WHERE id = $1
"#,
@ -245,7 +238,7 @@ INSERT INTO shopping_carts (buyer_id, state)
VALUES ($1, 'active')
ON CONFLICT
DO NOTHING
RETURNING id, buyer_id, payment_method, state;
RETURNING id, buyer_id, payment_method, state, checkout_notes
"#,
)
.bind(msg.buyer_id)
@ -259,7 +252,7 @@ RETURNING id, buyer_id, payment_method, state;
};
sqlx::query_as(
r#"
SELECT id, buyer_id, payment_method, state
SELECT id, buyer_id, payment_method, state, checkout_notes
FROM shopping_carts
WHERE buyer_id = $1 AND state = 'active'
"#,

View File

@ -90,6 +90,7 @@ pub(crate) async fn create_order(
quantity_unit: item.quantity_unit,
})
.collect(),
checkout_notes: cart.checkout_notes
},
Error::CreateAccountOrder,
Error::DatabaseInternal

View File

@ -231,6 +231,7 @@ pub(crate) async fn request_payment(
})
.collect(),
shopping_cart_id: cart.id,
checkout_notes: cart.checkout_notes,
},
Error::CreateOrder
);

4
api/i18n.toml Normal file
View File

@ -0,0 +1,4 @@
available-locales = ["en", "pl"]
default-locale = "en"
fallback_language = "en"
load-path = "locales"

0
api/locales/en.yml Normal file
View File

0
api/locales/pl.yml Normal file
View File

View File

@ -59,6 +59,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
let fs_manager = fs_manager::FsManager::build(app_config.clone())
.await
.expect("Failed to initialize file system storage");
let cart_manager = cart_manager::CartManager::new(db.clone()).start();
let addr = {
let l = app_config.lock();
let w = l.web();
@ -87,6 +88,7 @@ async fn server(opts: ServerOpts) -> Result<()> {
.app_data(Data::new(payment_manager.clone()))
.app_data(Data::new(search_manager.clone()))
.app_data(Data::new(fs_manager.clone()))
.app_data(Data::new(cart_manager.clone()))
.configure(routes::configure)
.service({
let l = app_config.lock();

View File

@ -0,0 +1,2 @@
ALTER TABLE shopping_carts
ADD checkout_notes TEXT;

View File

@ -0,0 +1,2 @@
ALTER TABLE account_orders
ADD checkout_notes TEXT;

View File

@ -38,6 +38,7 @@ impl From<(Vec<crate::AccountOrder>, Vec<crate::OrderItem>)> for AccountOrders {
order_id,
order_ext_id: _,
service_order_id: _,
checkout_notes,
}| {
AccountOrder {
id,
@ -45,6 +46,7 @@ impl From<(Vec<crate::AccountOrder>, Vec<crate::OrderItem>)> for AccountOrders {
status,
order_id,
items: items.drain_filter(|item| item.order_id == id).collect(),
checkout_notes,
}
},
)
@ -63,6 +65,7 @@ impl From<(crate::AccountOrder, Vec<crate::OrderItem>)> for AccountOrder {
order_id,
order_ext_id: _,
service_order_id: _,
checkout_notes,
},
mut items,
): (crate::AccountOrder, Vec<crate::OrderItem>),
@ -73,6 +76,7 @@ impl From<(crate::AccountOrder, Vec<crate::OrderItem>)> for AccountOrder {
status,
order_id,
items: items.drain_filter(|item| item.order_id == id).collect(),
checkout_notes,
}
}
}
@ -85,6 +89,7 @@ pub struct AccountOrder {
pub status: crate::OrderStatus,
pub order_id: Option<crate::OrderId>,
pub items: Vec<crate::OrderItem>,
pub checkout_notes: Option<String>,
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
@ -125,6 +130,7 @@ pub struct ShoppingCart {
pub payment_method: PaymentMethod,
pub state: ShoppingCartState,
pub items: Vec<ShoppingCartItem>,
pub checkout_notes: String,
}
impl From<(crate::ShoppingCart, Vec<crate::ShoppingCartItem>)> for ShoppingCart {
@ -135,6 +141,7 @@ impl From<(crate::ShoppingCart, Vec<crate::ShoppingCartItem>)> for ShoppingCart
buyer_id,
payment_method,
state,
checkout_notes,
},
items,
): (crate::ShoppingCart, Vec<crate::ShoppingCartItem>),
@ -144,6 +151,7 @@ impl From<(crate::ShoppingCart, Vec<crate::ShoppingCartItem>)> for ShoppingCart
buyer_id,
payment_method,
state,
checkout_notes: checkout_notes.unwrap_or_default(),
items: items
.into_iter()
.map(

View File

@ -856,6 +856,7 @@ pub struct AccountOrder {
pub order_id: Option<OrderId>,
pub order_ext_id: uuid::Uuid,
pub service_order_id: Option<String>,
pub checkout_notes: Option<String>,
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
@ -866,6 +867,7 @@ pub struct PublicAccountOrder {
pub buyer_id: AccountId,
pub status: OrderStatus,
pub order_id: Option<OrderId>,
pub checkout_notes: String,
}
impl From<AccountOrder> for PublicAccountOrder {
@ -877,6 +879,7 @@ impl From<AccountOrder> for PublicAccountOrder {
order_id,
order_ext_id: _,
service_order_id: _,
checkout_notes,
}: AccountOrder,
) -> Self {
Self {
@ -884,6 +887,7 @@ impl From<AccountOrder> for PublicAccountOrder {
buyer_id,
status,
order_id,
checkout_notes: checkout_notes.unwrap_or_default(),
}
}
}
@ -914,12 +918,13 @@ pub struct ShoppingCartId(pub RecordId);
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct ShoppingCart {
pub id: ShoppingCartId,
pub buyer_id: AccountId,
pub payment_method: PaymentMethod,
pub state: ShoppingCartState,
pub checkout_notes: Option<String>,
}
#[cfg_attr(feature = "dummy", derive(fake::Dummy))]
@ -930,7 +935,7 @@ pub struct ShoppingCart {
pub struct ShoppingCartItemId(RecordId);
#[cfg_attr(feature = "db", derive(sqlx::FromRow))]
#[derive(Serialize, Deserialize)]
#[derive(Serialize, Deserialize, Debug)]
pub struct ShoppingCartItem {
pub id: ShoppingCartItemId,
pub product_id: ProductId,

View File

@ -25,7 +25,7 @@ wasm-bindgen = { version = "0.2.80", features = ["default"] }
web-sys = { version = "0.3.57", features = ["Navigator"] }
js-sys = { version = "0.3.57", features = [] }
indexmap = { version = "1", features = ["serde-1"] }
indexmap = { version = "1", default-features = false, features = ["serde-1", "std"] }
rusty-money = { version = "0.4.1", features = ["iso"] }

View File

@ -80,6 +80,7 @@ mod summary_left {
use seed::*;
use crate::pages::public::shopping_cart::ShoppingCartPage;
use crate::shopping_cart::CartMsg;
pub fn view(model: &crate::Model, page: &ShoppingCartPage) -> Node<crate::Msg> {
div![
@ -94,7 +95,16 @@ mod summary_left {
]
],
div![C!["p-4"], p![C!["mb-4 italic"], model.i18n.t("If you have some information for the seller you can leave them in the box below")]],
textarea![C!["w-full h-24 p-2 bg-gray-100 rounded border-none"]]
textarea![
C!["w-full h-24 p-2 bg-gray-100 rounded border-none"],
ev(Ev::Change, move |ev| {
ev.stop_propagation();
let target = ev.target()?;
let input = seed::to_textarea(&target);
Some(crate::Msg::from(CartMsg::ChangeNotes(input.value())))
}),
model.cart.checkout_notes.as_str()
]
]
}

View File

@ -23,6 +23,7 @@ pub enum CartMsg {
/// Send current non-empty cart to server
Sync,
SyncResult(NetRes<model::api::UpdateCartOutput>),
ChangeNotes(String),
}
impl From<CartMsg> for Msg {
@ -44,6 +45,8 @@ pub struct Item {
pub struct ShoppingCart {
pub cart_id: Option<model::ShoppingCartId>,
pub items: Items,
#[serde(default)]
pub checkout_notes: String,
#[serde(skip)]
pub hover: bool,
}
@ -70,6 +73,7 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
entry.quantity_unit = quantity_unit;
}
store_local(&model.cart);
sync_cart(model, orders);
}
CartMsg::ModifyItem {
product_id,
@ -89,10 +93,12 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
entry.quantity_unit = quantity_unit;
}
store_local(&model.cart);
sync_cart(model, orders);
}
CartMsg::Remove(product_id) => {
model.cart.items.remove(&product_id);
store_local(&model.cart);
sync_cart(model, orders);
}
CartMsg::Hover => {
model.cart.hover = true;
@ -100,18 +106,30 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
CartMsg::Leave => {
model.cart.hover = false;
}
CartMsg::Sync => {
if let Some(access_token) = model.shared.access_token.as_ref().cloned() {
let items: Vec<Item> = model.cart.items.values().map(Clone::clone).collect();
orders.perform_cmd(async {
crate::Msg::from(CartMsg::SyncResult(
crate::api::public::update_cart(access_token, items).await,
))
});
}
}
CartMsg::Sync => sync_cart(model, orders),
CartMsg::SyncResult(NetRes::Success(cart)) => {
// cart.items
let len = cart.items.len();
model.cart.items = cart.items.into_iter().fold(
IndexMap::with_capacity(len),
|mut set,
model::api::ShoppingCartItem {
id: _,
product_id,
shopping_cart_id: _,
quantity,
quantity_unit,
}| {
set.insert(
product_id,
Item {
product_id,
quantity,
quantity_unit,
},
);
set
},
);
}
CartMsg::SyncResult(NetRes::Error(failure)) => {
for msg in failure.errors {
@ -121,6 +139,21 @@ pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
CartMsg::SyncResult(NetRes::Http(_cart)) => {
orders.send_msg(NotificationMsg::Error("Unable to sync cart".into()).into());
}
CartMsg::ChangeNotes(notes) => {
model.cart.checkout_notes = notes;
store_local(&model.cart);
}
}
}
fn sync_cart(model: &mut Model, orders: &mut impl Orders<Msg>) {
if let Some(access_token) = model.shared.access_token.as_ref().cloned() {
let items: Vec<Item> = model.cart.items.values().map(Clone::clone).collect();
orders.perform_cmd(async {
crate::Msg::from(CartMsg::SyncResult(
crate::api::public::update_cart(access_token, items).await,
))
});
}
}