cart.py 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. from pathlib import Path
  2. from typing import NoReturn
  3. import shortuuid
  4. from app.schema import CartUpdate, DocumentProduct, CartSessionItem, Settings, ProductInfoCode
  5. from app.template import make_product_info_code
  6. from app.db import get_product_by_product_id, get_products_id_db
  7. from app.parser import parse_file
  8. def product_exist_in_cart(
  9. product_id: str,
  10. size: str | None,
  11. style: str | None,
  12. product_variation_id: str | None,
  13. session_cart_items: dict[str, dict[str, str | dict[str, str]]],
  14. ) -> bool:
  15. """Check if given product exists in the cart already by constructing
  16. a full label using the product's id and eventual size and style
  17. options.
  18. """
  19. if product_id in session_cart_items or product_variation_id in session_cart_items:
  20. return True
  21. return False
  22. def initialize_cart(session, currency: str) -> dict[int, str] | NoReturn:
  23. """Initialize cart if no cart object is present yet in session."""
  24. if "cart" not in session:
  25. order_id = shortuuid.uuid()
  26. session["cart"] = {
  27. "meta": {
  28. "order_id": order_id,
  29. "currency": currency,
  30. "subtotal": 0,
  31. "total_weight": 0,
  32. "shipping": 0,
  33. "total": 0,
  34. "item_amount": 0,
  35. },
  36. "items": {},
  37. }
  38. def get_inventory_amount(session_item, products, settings) -> int | None:
  39. """Returns the inventory amount value for any product by matching it
  40. against the list of selected products in the cart. We need to make
  41. use of the selected product's style and size options in order to
  42. retrieve the correct inventory amount value.
  43. """
  44. inventory_amount = None
  45. product = parse_file(settings.git_repo, Path(products["products"][session_item["product_id"]]), settings.block_types)
  46. for variation in product.meta.inventory:
  47. if variation.product_variation_id == session_item["options"]["product_variation_id"]:
  48. return variation.amount
  49. def update_cart_meta(
  50. session,
  51. shipping_amount: int,
  52. settings,
  53. ) -> NoReturn:
  54. """Update Meta key of Session Cart (subtotal, shipping, total).
  55. Re-compute Meta's item_count, subtotal, shipping and total from
  56. each item so that we don't need to check for inventory's item
  57. amount a second time (we did it when updating
  58. each item in update_cart).
  59. """
  60. session_meta_item_amount = 0
  61. session_meta_subtotal = 0
  62. session_meta_weight = 0
  63. products = get_products_id_db(settings)
  64. for item in session["items"].values():
  65. if products is not None:
  66. # when doing GET /checkout, checks that user's cart can
  67. # proceed with the payment step or if not:
  68. # - either remove any sold out item from the cart
  69. # - or adjust user's cart to the max available number of
  70. # items set in the inventory of the given item
  71. inventory_amount = get_inventory_amount(item, products, settings)
  72. if inventory_amount == 0:
  73. # if inventory amount for given item has meanwhile gone to 0,
  74. # don't update the meta fields
  75. pass
  76. else:
  77. # else re-compute meta values
  78. item_quantity = 0
  79. if item["quantity"] >= inventory_amount:
  80. item_quantity = inventory_amount
  81. else:
  82. item_quantity = item["quantity"]
  83. session_meta_item_amount += item_quantity
  84. session_meta_subtotal += item_quantity * item["price"]
  85. session_meta_weight += item_quantity * item["options"]["weight"]
  86. # -- update also each item quantity and availability under session.cart.items
  87. if item["options"]["product_variation_id"]:
  88. cart_product_id = item["options"]["product_variation_id"]
  89. else:
  90. cart_product_id = item["product_id"]
  91. session["items"][cart_product_id]["quantity"] = item_quantity
  92. session["items"][cart_product_id]["availability"] = inventory_amount > 0
  93. else:
  94. # when doing POST /checkout, re-compute meta values
  95. session_meta_item_amount += item["quantity"]
  96. session_meta_subtotal += item["quantity"] * item["price"]
  97. session_meta_weight += item["quantity"] * item["options"]["weight"]
  98. session["meta"]["item_amount"] = session_meta_item_amount
  99. session["meta"]["subtotal"] = session_meta_subtotal
  100. session["meta"]["total_weight"] = session_meta_weight
  101. # check items amount and eventually reset shipping value
  102. if len(session["items"]) > 0:
  103. session["meta"]["shipping"] = shipping_amount
  104. else:
  105. session["meta"]["shipping"] = 0
  106. # update session meta total
  107. session["meta"]["total"] = session["meta"]["subtotal"] + shipping_amount
  108. def update_cart(
  109. session,
  110. form: CartUpdate,
  111. product_exists: bool,
  112. shipping_amount: int,
  113. quantity: int,
  114. inventory_amount: int,
  115. settings,
  116. ) -> NoReturn:
  117. """Update (add / remove) Session Cart with given product_id + options
  118. item: if Cart contains the product, increase / decrease product's
  119. quantity, else add / remove the product to / from the Cart.
  120. """
  121. if form.options.product_variation_id:
  122. product_id = form.options.product_variation_id # variation ID is used for item session storage if it exists
  123. else:
  124. product_id = form.product_id
  125. if product_exists:
  126. # get currently updated product
  127. s = session["items"][product_id]
  128. if form.operation == "add":
  129. # make sure the sum of user's existing cart's product
  130. # quantity plus the newly requested quantity does not go
  131. # over the product's available quantity (inventory amount)
  132. if (s["quantity"] + quantity) <= inventory_amount:
  133. s["quantity"] = s["quantity"] + 1
  134. elif form.operation == "remove":
  135. if s["quantity"] > 1:
  136. s["quantity"] = s["quantity"] - 1
  137. else:
  138. del session["items"][product_id]
  139. elif form.operation == "delete":
  140. del session["items"][product_id]
  141. elif form.operation == "manual":
  142. if quantity > 0:
  143. # cap manual input amount against available inventory amount
  144. if quantity <= inventory_amount:
  145. s["quantity"] = quantity
  146. else:
  147. # else round up to the inventory's amount value
  148. s["quantity"] = inventory_amount
  149. else:
  150. del session["items"][product_id]
  151. # update item's availability
  152. s["availability"] = inventory_amount > 0
  153. else:
  154. if form.operation == "add":
  155. if quantity <= inventory_amount:
  156. update_data = {
  157. "product_id": form.product_id,
  158. "options": {
  159. "product_variation_id": form.options.product_variation_id,
  160. "size": form.options.size,
  161. "style": form.options.style,
  162. "weight": form.options.weight,
  163. },
  164. "price": form.price,
  165. "quantity": 1,
  166. "availability": inventory_amount > 0,
  167. }
  168. session["items"][product_id] = update_data
  169. update_cart_meta(session, shipping_amount, settings)
  170. def clear_cart(session, currency: str) -> NoReturn:
  171. """Clear cart from saved items.
  172. Usually this is run after a successful payment.
  173. """
  174. order_id = shortuuid.uuid()
  175. session["cart"] = {
  176. "meta": {
  177. "order_id": order_id,
  178. "currency": currency,
  179. "subtotal": 0,
  180. "total_weight": 0,
  181. "shipping": 0,
  182. "total": 0,
  183. "item_amount": 0,
  184. },
  185. "items": {},
  186. }
  187. def calculate_quantity_requested_by_user(
  188. session_cart_items: dict[str, dict[str, str | dict[str, str]]],
  189. product_id: str,
  190. size: str | None = None,
  191. style: str | None = None,
  192. ) -> int:
  193. """Helper function to retrieve the number of items of a specific
  194. product (and variation) that the user has in their cart.
  195. """
  196. quantity_requested_by_user = 0
  197. for item in session_cart_items.values():
  198. if item["product_id"] == product_id:
  199. item_size = item.get("options", {}).get("size")
  200. item_style = item.get("options", {}).get("style")
  201. if (size is None or item_size == size) and (style is None or item_style == style):
  202. quantity_requested_by_user += item["quantity"]
  203. return quantity_requested_by_user
  204. def create_product_list(settings: Settings, cart_items: dict[CartSessionItem]) -> str:
  205. """
  206. Return a string containing all the selected product in the user's
  207. cart as a "comma-based list".
  208. """
  209. product_list = []
  210. products = get_products_id_db(settings)
  211. for item in cart_items.values():
  212. product = get_product_by_product_id(settings, item.product_id, products)
  213. product_path = str(Path(product.meta.path).stem)
  214. item = ProductInfoCode(path=product_path,
  215. options=item.options,
  216. quantity=item.quantity)
  217. product_info = make_product_info_code(item)
  218. product_list.append(product_info)
  219. return ",".join(product_list)