Sync cart

This commit is contained in:
eraden 2022-05-19 07:47:47 +02:00
parent a84fda2382
commit b1b4d083b7
11 changed files with 300 additions and 92 deletions

View File

@ -1,3 +1,7 @@
#![feature(drain_filter)]
use std::collections::HashSet;
use database_manager::{query_db, Database};
use model::{
AccountId, ProductId, Quantity, QuantityUnit, ShoppingCartId, ShoppingCartItem,
@ -60,8 +64,10 @@ pub enum Error {
ShoppingCartFailed,
#[error("Shopping cart is not available for unknown reason")]
CartNotAvailable,
#[error("Failed to add item to cart")]
CantAddItem,
#[error("Failed to modify item to cart")]
CantModifyItem,
#[error("Failed to modify cart")]
CantModifyCart,
#[error("{0}")]
Db(#[from] database_manager::Error),
#[error("Unable to update cart item")]
@ -89,7 +95,7 @@ impl CartManager {
}
#[derive(actix::Message)]
#[rtype(result = "Result<ShoppingCartItem>")]
#[rtype(result = "Result<Option<ShoppingCartItem>>")]
pub struct ModifyItem {
pub buyer_id: AccountId,
pub product_id: ProductId,
@ -97,9 +103,12 @@ pub struct ModifyItem {
pub quantity_unit: QuantityUnit,
}
cart_async_handler!(ModifyItem, modify_item, ShoppingCartItem);
cart_async_handler!(ModifyItem, modify_item, Option<ShoppingCartItem>);
async fn modify_item(msg: ModifyItem, db: actix::Addr<Database>) -> Result<ShoppingCartItem> {
async fn modify_item(
msg: ModifyItem,
db: actix::Addr<Database>,
) -> Result<Option<ShoppingCartItem>> {
let _cart = query_db!(
db,
database_manager::EnsureActiveShoppingCart {
@ -127,11 +136,17 @@ async fn modify_item(msg: ModifyItem, db: actix::Addr<Database>) -> Result<Shopp
database_manager::ActiveCartItemByProduct {
product_id: msg.product_id
},
Error::CantAddItem
Error::CantModifyItem
);
match item {
Some(item) => Ok(query_db!(
Some(item) if **item.quantity == 0 => Ok(query_db!(
db,
database_manager::DeleteShoppingCartItem { id: item.id },
passthrough Error::Db,
Error::CantModifyItem
)),
Some(item) => Ok(Some(query_db!(
db,
database_manager::UpdateShoppingCartItem {
id: item.id,
@ -141,9 +156,9 @@ async fn modify_item(msg: ModifyItem, db: actix::Addr<Database>) -> Result<Shopp
quantity_unit: msg.quantity_unit,
},
passthrough Error::Db,
Error::CantAddItem
)),
None => Ok(query_db!(
Error::CantModifyItem
))),
None => Ok(Some(query_db!(
db,
database_manager::CreateShoppingCartItem {
product_id: msg.product_id,
@ -152,8 +167,8 @@ async fn modify_item(msg: ModifyItem, db: actix::Addr<Database>) -> Result<Shopp
quantity_unit: msg.quantity_unit,
},
passthrough Error::Db,
Error::CantAddItem
)),
Error::CantModifyItem
))),
}
}
@ -170,68 +185,101 @@ pub(crate) async fn remove_product(
msg: RemoveProduct,
db: actix::Addr<Database>,
) -> Result<Option<ShoppingCartItem>> {
match db
.send(database_manager::RemoveCartItem {
Ok(query_db!(
db,
database_manager::RemoveCartItem {
shopping_cart_id: msg.shopping_cart_id,
shopping_cart_item_id: Some(msg.shopping_cart_item_id),
product_id: None,
})
.await
{
Ok(Ok(row)) => Ok(row),
Ok(Err(db_err)) => {
log::error!("{db_err}");
Err(Error::UpdateFailed)
}
Err(act_err) => {
log::error!("{act_err:?}");
Err(Error::UpdateFailed)
}
}
},
Error::UpdateFailed
))
}
#[derive(actix::Message)]
#[rtype(result = "Result<Option<ShoppingCartItem>>")]
pub struct ChangeQuantity {
pub shopping_cart_id: ShoppingCartId,
pub shopping_cart_item_id: ShoppingCartItemId,
pub quantity: Quantity,
pub quantity_unit: QuantityUnit,
#[rtype(result = "Result<Vec<ShoppingCartItem>>")]
pub struct ModifyCart {
pub buyer_id: AccountId,
pub items: Vec<ModifyItem>,
}
cart_async_handler!(ChangeQuantity, change_quantity, Option<ShoppingCartItem>);
cart_async_handler!(ModifyCart, modify_cart, Vec<ShoppingCartItem>);
pub(crate) async fn change_quantity(
msg: ChangeQuantity,
db: actix::Addr<Database>,
) -> Result<Option<ShoppingCartItem>> {
if **msg.quantity == 0 {
return remove_product(
RemoveProduct {
shopping_cart_id: msg.shopping_cart_id,
shopping_cart_item_id: msg.shopping_cart_item_id,
},
db,
)
.await;
}
let item: ShoppingCartItem = query_db!(
async fn modify_cart(msg: ModifyCart, db: actix::Addr<Database>) -> Result<Vec<ShoppingCartItem>> {
let _cart = query_db!(
db,
database_manager::FindShoppingCartItem {
id: msg.shopping_cart_item_id,
database_manager::EnsureActiveShoppingCart {
buyer_id: msg.buyer_id,
},
Error::NotExists(msg.shopping_cart_item_id)
Error::ShoppingCartFailed
);
let mut carts: Vec<model::ShoppingCart> = query_db!(
db,
database_manager::AccountShoppingCarts {
account_id: msg.buyer_id,
state: Some(ShoppingCartState::Active),
},
passthrough Error::Db,
Error::CartNotAvailable
);
let cart = if carts.is_empty() {
return Err(Error::CartNotAvailable);
} else {
carts.remove(0)
};
let existing =
msg.items
.iter()
.fold(HashSet::with_capacity(msg.items.len()), |mut agg, item| {
agg.insert(item.product_id);
agg
});
let mut items: Vec<model::ShoppingCartItem> = query_db!(
db,
database_manager::CartItems {
shopping_cart_id: cart.id
},
Error::CantModifyCart
);
Ok(Some(query_db!(
db,
database_manager::UpdateShoppingCartItem {
id: msg.shopping_cart_item_id,
product_id: item.product_id,
shopping_cart_id: item.shopping_cart_id,
quantity: msg.quantity,
quantity_unit: msg.quantity_unit,
},
Error::ChangeQuantity
)))
for item in items.drain_filter(|item| !existing.contains(&item.product_id)) {
query_db!(
db,
database_manager::RemoveCartItem {
shopping_cart_id: cart.id,
shopping_cart_item_id: Some(item.id),
product_id: None,
},
Error::CantModifyCart
);
}
let mut out = Vec::with_capacity(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?
{
out.push(item);
}
}
Ok(out)
}

View File

@ -190,6 +190,38 @@ RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit
})
}
#[derive(actix::Message)]
#[rtype(result = "Result<Option<ShoppingCartItem>>")]
pub struct DeleteShoppingCartItem {
pub id: ShoppingCartItemId,
}
db_async_handler!(
DeleteShoppingCartItem,
delete_shopping_cart_item,
Option<ShoppingCartItem>
);
pub(crate) async fn delete_shopping_cart_item(
msg: DeleteShoppingCartItem,
db: PgPool,
) -> Result<Option<ShoppingCartItem>> {
sqlx::query_as(
r#"
DELETE FROM shopping_cart_items
WHERE id = $1
RETURNING id, product_id, shopping_cart_id, quantity, quantity_unit
"#,
)
.bind(msg.id)
.fetch_optional(&db)
.await
.map_err(|e| {
log::error!("{e:?}");
super::Error::ShoppingCartItem(Error::CantUpdate(msg.id))
})
}
#[derive(actix::Message)]
#[rtype(result = "Result<ShoppingCartItem>")]
pub struct FindShoppingCartItem {

View File

@ -110,7 +110,7 @@ impl Responder for Error {
}),
},
Error::Public(PublicError::ApiV1(
V1Error::AddItem | V1Error::RemoveItem | V1Error::AddOrder,
V1Error::ModifyItem | V1Error::RemoveItem | V1Error::AddOrder,
)) => HttpResponse::BadRequest()
.content_type("application/json")
.json(Failure {

View File

@ -17,7 +17,7 @@ pub enum Error {
#[error("Failed to remove shopping cart item")]
RemoveItem,
#[error("Failed to add shopping cart item")]
AddItem,
ModifyItem,
#[error("Failed to create order")]
AddOrder,

View File

@ -93,11 +93,11 @@ async fn update_cart_item(
cart: Data<Addr<CartManager>>,
tm: Data<Addr<TokenManager>>,
credentials: BearerAuth,
Json(payload): Json<api::CreateItemInput>,
) -> Result<Json<api::CreateItemOutput>> {
Json(payload): Json<api::UpdateItemInput>,
) -> Result<Json<api::UpdateItemOutput>> {
let token = credentials.require_user(tm.into_inner()).await?;
let item: model::ShoppingCartItem = query_cart!(
let item: Option<model::ShoppingCartItem> = query_cart!(
cart,
cart_manager::ModifyItem {
buyer_id: token.account_id(),
@ -105,13 +105,55 @@ async fn update_cart_item(
quantity: payload.quantity,
quantity_unit: payload.quantity_unit,
},
routes::Error::Public(super::Error::AddItem.into()),
routes::Error::Public(super::Error::ModifyItem.into()),
routes::Error::Public(PublicError::DatabaseConnection)
);
Ok(Json(api::CreateItemOutput {
success: true,
shopping_cart_item: item.into(),
match item {
Some(item) => Ok(Json(api::UpdateItemOutput {
shopping_cart_item: item.into(),
})),
None => Err(routes::Error::Public(super::Error::ModifyItem.into())),
}
}
#[put("/shopping-cart")]
async fn update_cart(
cart: Data<Addr<CartManager>>,
tm: Data<Addr<TokenManager>>,
credentials: BearerAuth,
Json(payload): Json<api::UpdateCartInput>,
) -> Result<Json<api::UpdateCartOutput>> {
let token = credentials.require_user(tm.into_inner()).await?;
let items = payload
.items
.into_iter()
.map(
|model::api::UpdateItemInput {
product_id,
quantity,
quantity_unit,
}| cart_manager::ModifyItem {
buyer_id: token.account_id(),
product_id,
quantity,
quantity_unit,
},
)
.collect();
let items: Vec<model::ShoppingCartItem> = query_cart!(
cart,
cart_manager::ModifyCart {
buyer_id: token.account_id(),
items
},
routes::Error::Public(super::Error::ModifyItem.into()),
routes::Error::Public(PublicError::DatabaseConnection)
);
Ok(Json(api::UpdateCartOutput {
items: items.into_iter().map(Into::into).collect(),
}))
}
@ -229,6 +271,8 @@ pub(crate) fn configure(config: &mut ServiceConfig) {
.service(shopping_cart)
.service(delete_cart_item)
.service(create_order)
.service(update_cart)
.service(update_cart_item)
.service(me)
.service(verify_token)
.service(refresh_token);

View File

@ -366,18 +366,27 @@ pub struct DeleteItemOutput {
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CreateItemInput {
pub struct UpdateItemInput {
pub product_id: ProductId,
pub quantity: Quantity,
pub quantity_unit: QuantityUnit,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct CreateItemOutput {
pub success: bool,
pub struct UpdateItemOutput {
pub shopping_cart_item: ShoppingCartItem,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct UpdateCartInput {
pub items: Vec<UpdateItemInput>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct UpdateCartOutput {
pub items: Vec<ShoppingCartItem>,
}
#[derive(Serialize, Deserialize, Debug)]
pub struct SearchRequest {
/// Match string

View File

@ -2,6 +2,7 @@ use model::{AccessTokenString, RefreshTokenString};
use seed::fetch::{Header, Method, Request};
use crate::api::perform;
use crate::NetRes;
pub async fn config() -> super::NetRes<model::api::Config> {
perform(Request::new("/config").method(Method::Get)).await
@ -65,3 +66,54 @@ pub async fn sign_up(
)
.await
}
pub async fn update_cart_item(
access_token: &AccessTokenString,
product_id: model::ProductId,
quantity: model::Quantity,
quantity_unit: model::QuantityUnit,
) -> NetRes<model::api::UpdateItemOutput> {
let input = model::api::UpdateItemInput {
product_id,
quantity,
quantity_unit,
};
perform(
Request::new("/api/v1/shopping-cart-item")
.method(Method::Put)
.header(Header::bearer(access_token.as_str()))
.json(&input)
.map_err(crate::api::NetRes::Http)?,
)
.await
}
pub async fn update_cart(
access_token: AccessTokenString,
items: Vec<crate::shopping_cart::Item>,
) -> NetRes<model::api::UpdateCartOutput> {
let input = model::api::UpdateCartInput {
items: items
.into_iter()
.map(
|crate::shopping_cart::Item {
product_id,
quantity,
quantity_unit,
}| model::api::UpdateItemInput {
product_id,
quantity,
quantity_unit,
},
)
.collect(),
};
perform(
Request::new("/api/v1/shopping-cart")
.method(Method::Put)
.header(Header::bearer(access_token.as_str()))
.json(&input)
.map_err(crate::api::NetRes::Http)?,
)
.await
}

View File

@ -169,13 +169,6 @@ fn update(msg: Msg, model: &mut Model, orders: &mut impl Orders<Msg>) {
let page = fetch_page!(public model, SignIn);
pages::public::sign_in::update(msg, page, &mut orders.proxy(Into::into))
}
Msg::Public(pages::public::PublicMsg::SignUp(
pages::public::sign_up::RegisterMsg::AccountCreated(res),
)) => {
orders
.skip()
.send_msg(Msg::Session(SessionMsg::TokenRefreshed(res)));
}
Msg::Public(pages::public::PublicMsg::SignUp(msg)) => {
let page = fetch_page!(public model, SignUp);
pages::public::sign_up::update(msg, page, &mut orders.proxy(Into::into))

View File

@ -62,7 +62,6 @@ pub fn view(model: &crate::Model, page: &SignInPage) -> Node<crate::Msg> {
]
.map_msg(Into::into);
// crate::shared::view::public_navbar(model),
div![super::layout::view(model, content, None)]
}

View File

@ -1,10 +1,11 @@
use std::str::FromStr;
use model::{Email, Login, Password, PasswordConfirmation};
use seed::prelude::*;
use seed::*;
use crate::pages::Urls;
use crate::shopping_cart::CartMsg;
use crate::SessionMsg;
#[derive(Debug)]
pub enum RegisterMsg {
@ -38,18 +39,18 @@ pub fn page_changed(_url: Url, _model: &mut SignUpPage) {}
pub fn update(msg: RegisterMsg, model: &mut SignUpPage, orders: &mut impl Orders<crate::Msg>) {
match msg {
RegisterMsg::LoginChanged(value) => {
model.login = Login::new(value);
model.login = model::Login::new(value);
}
RegisterMsg::EmailChanged(value) => {
if let Ok(email) = Email::from_str(&value) {
if let Ok(email) = model::Email::from_str(&value) {
model.email = email;
}
}
RegisterMsg::PasswordChanged(value) => {
model.password = Password::new(value);
model.password = model::Password::new(value);
}
RegisterMsg::PasswordConfirmationChanged(value) => {
model.password_confirmation = PasswordConfirmation::new(value);
model.password_confirmation = model::PasswordConfirmation::new(value);
}
RegisterMsg::Submit => {
let email = model.email.clone();
@ -72,7 +73,12 @@ pub fn update(msg: RegisterMsg, model: &mut SignUpPage, orders: &mut impl Orders
)
});
}
RegisterMsg::AccountCreated(_) => {}
RegisterMsg::AccountCreated(res) => {
orders
.skip()
.send_msg(crate::Msg::Session(SessionMsg::TokenRefreshed(res)))
.send_msg(crate::Msg::Cart(CartMsg::Sync));
}
}
}

View File

@ -2,7 +2,8 @@ use model::{ProductId, Quantity, QuantityUnit};
use seed::prelude::*;
use serde::{Deserialize, Serialize};
use crate::{Model, Msg};
use crate::shared::notification::NotificationMsg;
use crate::{Model, Msg, NetRes};
#[derive(Debug)]
pub enum CartMsg {
@ -19,6 +20,9 @@ pub enum CartMsg {
Remove(ProductId),
Hover,
Leave,
/// Send current non-empty cart to server
Sync,
SyncResult(NetRes<model::api::UpdateCartOutput>),
}
impl From<CartMsg> for Msg {
@ -29,7 +33,7 @@ impl From<CartMsg> for Msg {
pub type Items = indexmap::IndexMap<ProductId, Item>;
#[derive(Debug, Serialize, Deserialize)]
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
pub struct Item {
pub product_id: ProductId,
pub quantity: Quantity,
@ -48,7 +52,7 @@ pub fn init(model: &mut Model, _orders: &mut impl Orders<Msg>) {
model.cart = load_local();
}
pub fn update(msg: CartMsg, model: &mut Model, _orders: &mut impl Orders<Msg>) {
pub fn update(msg: CartMsg, model: &mut Model, orders: &mut impl Orders<Msg>) {
match msg {
CartMsg::AddItem {
quantity,
@ -96,6 +100,27 @@ 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::SyncResult(NetRes::Success(cart)) => {
// cart.items
}
CartMsg::SyncResult(NetRes::Error(failure)) => {
for msg in failure.errors {
orders.send_msg(NotificationMsg::Error(msg).into());
}
}
CartMsg::SyncResult(NetRes::Http(_cart)) => {
orders.send_msg(NotificationMsg::Error("Unable to sync cart".into()).into());
}
}
}