from pathlib import Path from typing import NoReturn import shortuuid from app.schema import CartUpdate, DocumentProduct, CartSessionItem, Settings, ProductInfoCode from app.template import make_product_info_code from app.db import get_product_by_product_id, get_products_id_db from app.parser import parse_file def product_exist_in_cart( product_id: str, size: str | None, style: str | None, product_variation_id: str | None, session_cart_items: dict[str, dict[str, str | dict[str, str]]], ) -> bool: """Check if given product exists in the cart already by constructing a full label using the product's id and eventual size and style options. """ if product_id in session_cart_items or product_variation_id in session_cart_items: return True return False def initialize_cart(session, currency: str) -> dict[int, str] | NoReturn: """Initialize cart if no cart object is present yet in session.""" if "cart" not in session: order_id = shortuuid.uuid() session["cart"] = { "meta": { "order_id": order_id, "currency": currency, "subtotal": 0, "total_weight": 0, "shipping": 0, "total": 0, "item_amount": 0, }, "items": {}, } def get_inventory_amount(session_item, products, settings) -> int | None: """Returns the inventory amount value for any product by matching it against the list of selected products in the cart. We need to make use of the selected product's style and size options in order to retrieve the correct inventory amount value. """ inventory_amount = None product = parse_file(settings.git_repo, Path(products["products"][session_item["product_id"]]), settings.block_types) for variation in product.meta.inventory: if variation.product_variation_id == session_item["options"]["product_variation_id"]: return variation.amount def update_cart_meta( session, shipping_amount: int, settings, ) -> NoReturn: """Update Meta key of Session Cart (subtotal, shipping, total). Re-compute Meta's item_count, subtotal, shipping and total from each item so that we don't need to check for inventory's item amount a second time (we did it when updating each item in update_cart). """ session_meta_item_amount = 0 session_meta_subtotal = 0 session_meta_weight = 0 products = get_products_id_db(settings) for item in session["items"].values(): if products is not None: # when doing GET /checkout, checks that user's cart can # proceed with the payment step or if not: # - either remove any sold out item from the cart # - or adjust user's cart to the max available number of # items set in the inventory of the given item inventory_amount = get_inventory_amount(item, products, settings) if inventory_amount == 0: # if inventory amount for given item has meanwhile gone to 0, # don't update the meta fields pass else: # else re-compute meta values item_quantity = 0 if item["quantity"] >= inventory_amount: item_quantity = inventory_amount else: item_quantity = item["quantity"] session_meta_item_amount += item_quantity session_meta_subtotal += item_quantity * item["price"] session_meta_weight += item_quantity * item["options"]["weight"] # -- update also each item quantity and availability under session.cart.items if item["options"]["product_variation_id"]: cart_product_id = item["options"]["product_variation_id"] else: cart_product_id = item["product_id"] session["items"][cart_product_id]["quantity"] = item_quantity session["items"][cart_product_id]["availability"] = inventory_amount > 0 else: # when doing POST /checkout, re-compute meta values session_meta_item_amount += item["quantity"] session_meta_subtotal += item["quantity"] * item["price"] session_meta_weight += item["quantity"] * item["options"]["weight"] session["meta"]["item_amount"] = session_meta_item_amount session["meta"]["subtotal"] = session_meta_subtotal session["meta"]["total_weight"] = session_meta_weight # check items amount and eventually reset shipping value if len(session["items"]) > 0: session["meta"]["shipping"] = shipping_amount else: session["meta"]["shipping"] = 0 # update session meta total session["meta"]["total"] = session["meta"]["subtotal"] + shipping_amount def update_cart( session, form: CartUpdate, product_exists: bool, shipping_amount: int, quantity: int, inventory_amount: int, settings, ) -> NoReturn: """Update (add / remove) Session Cart with given product_id + options item: if Cart contains the product, increase / decrease product's quantity, else add / remove the product to / from the Cart. """ if form.options.product_variation_id: product_id = form.options.product_variation_id # variation ID is used for item session storage if it exists else: product_id = form.product_id if product_exists: # get currently updated product s = session["items"][product_id] if form.operation == "add": # make sure the sum of user's existing cart's product # quantity plus the newly requested quantity does not go # over the product's available quantity (inventory amount) if (s["quantity"] + quantity) <= inventory_amount: s["quantity"] = s["quantity"] + 1 elif form.operation == "remove": if s["quantity"] > 1: s["quantity"] = s["quantity"] - 1 else: del session["items"][product_id] elif form.operation == "delete": del session["items"][product_id] elif form.operation == "manual": if quantity > 0: # cap manual input amount against available inventory amount if quantity <= inventory_amount: s["quantity"] = quantity else: # else round up to the inventory's amount value s["quantity"] = inventory_amount else: del session["items"][product_id] # update item's availability s["availability"] = inventory_amount > 0 else: if form.operation == "add": if quantity <= inventory_amount: update_data = { "product_id": form.product_id, "options": { "product_variation_id": form.options.product_variation_id, "size": form.options.size, "style": form.options.style, "weight": form.options.weight, }, "price": form.price, "quantity": 1, "availability": inventory_amount > 0, } session["items"][product_id] = update_data update_cart_meta(session, shipping_amount, settings) def clear_cart(session, currency: str) -> NoReturn: """Clear cart from saved items. Usually this is run after a successful payment. """ order_id = shortuuid.uuid() session["cart"] = { "meta": { "order_id": order_id, "currency": currency, "subtotal": 0, "total_weight": 0, "shipping": 0, "total": 0, "item_amount": 0, }, "items": {}, } def calculate_quantity_requested_by_user( session_cart_items: dict[str, dict[str, str | dict[str, str]]], product_id: str, size: str | None = None, style: str | None = None, ) -> int: """Helper function to retrieve the number of items of a specific product (and variation) that the user has in their cart. """ quantity_requested_by_user = 0 for item in session_cart_items.values(): if item["product_id"] == product_id: item_size = item.get("options", {}).get("size") item_style = item.get("options", {}).get("style") if (size is None or item_size == size) and (style is None or item_style == style): quantity_requested_by_user += item["quantity"] return quantity_requested_by_user def create_product_list(settings: Settings, cart_items: dict[CartSessionItem]) -> str: """ Return a string containing all the selected product in the user's cart as a "comma-based list". """ product_list = [] products = get_products_id_db(settings) for item in cart_items.values(): product = get_product_by_product_id(settings, item.product_id, products) product_path = str(Path(product.meta.path).stem) item = ProductInfoCode(path=product_path, options=item.options, quantity=item.quantity) product_info = make_product_info_code(item) product_list.append(product_info) return ",".join(product_list)