Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Phase 1 - Relations service (internal): owns ContainerRelation, exposes get-children data + attach/detach-child actions. Retargeted events, blog, market callers from cart to relations. Phase 2 - Likes service (internal): unified Like model replaces ProductLike and PostLike with generic target_type/target_slug/target_id. Exposes is-liked, liked-slugs, liked-ids data + toggle action. Phase 3 - PageConfig → blog: moved ownership to blog with direct DB queries, removed proxy endpoints from cart. Phase 4 - Orders service (public): owns Order/OrderItem + SumUp checkout flow. Cart checkout now delegates to orders via create-order action. Webhook/return routes and reconciliation moved to orders. Phase 5 - Infrastructure: docker-compose, deploy.sh, Dockerfiles updated for all 3 new services. Added orders_url helper and factory model imports. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
221 lines
7.6 KiB
Python
221 lines
7.6 KiB
Python
# bp/cart/global_routes.py — Global cart routes (add, quantity, delete, checkout)
|
|
|
|
from __future__ import annotations
|
|
|
|
from quart import Blueprint, g, request, render_template, redirect, url_for, make_response
|
|
from sqlalchemy import select
|
|
|
|
from shared.models.market import CartItem
|
|
from shared.infrastructure.actions import call_action
|
|
from .services import (
|
|
current_cart_identity,
|
|
get_cart,
|
|
total,
|
|
get_calendar_cart_entries,
|
|
calendar_total,
|
|
get_ticket_cart_entries,
|
|
ticket_total,
|
|
)
|
|
from .services.checkout import (
|
|
find_or_create_cart_item,
|
|
resolve_page_config,
|
|
)
|
|
|
|
|
|
def register(url_prefix: str) -> Blueprint:
|
|
bp = Blueprint("cart_global", __name__, url_prefix=url_prefix)
|
|
|
|
@bp.post("/add/<int:product_id>/")
|
|
async def add_to_cart(product_id: int):
|
|
from shared.infrastructure.data_client import fetch_data
|
|
|
|
ident = current_cart_identity()
|
|
|
|
# Fetch product data from market service (cart DB doesn't have products)
|
|
products_raw = await fetch_data(
|
|
"market", "products-by-ids",
|
|
params={"ids": str(product_id)},
|
|
required=False,
|
|
) or []
|
|
product_data = products_raw[0] if products_raw else None
|
|
|
|
cart_item = await find_or_create_cart_item(
|
|
g.s,
|
|
product_id,
|
|
ident["user_id"],
|
|
ident["session_id"],
|
|
product_title=product_data["title"] if product_data else None,
|
|
product_slug=product_data["slug"] if product_data else None,
|
|
product_image=product_data["image"] if product_data else None,
|
|
product_regular_price=product_data["regular_price"] if product_data else None,
|
|
product_special_price=product_data["special_price"] if product_data else None,
|
|
)
|
|
|
|
if not cart_item:
|
|
return await make_response("Product not found", 404)
|
|
|
|
if request.headers.get("HX-Request") == "true":
|
|
# Redirect to overview for HTMX
|
|
return redirect(url_for("cart_overview.overview"))
|
|
|
|
return redirect(url_for("cart_overview.overview"))
|
|
|
|
@bp.post("/quantity/<int:product_id>/")
|
|
async def update_quantity(product_id: int):
|
|
ident = current_cart_identity()
|
|
form = await request.form
|
|
count = int(form.get("count", 0))
|
|
|
|
filters = [
|
|
CartItem.deleted_at.is_(None),
|
|
CartItem.product_id == product_id,
|
|
]
|
|
if ident["user_id"] is not None:
|
|
filters.append(CartItem.user_id == ident["user_id"])
|
|
else:
|
|
filters.append(CartItem.session_id == ident["session_id"])
|
|
|
|
existing = await g.s.scalar(select(CartItem).where(*filters))
|
|
|
|
if existing:
|
|
existing.quantity = max(count, 0)
|
|
await g.s.flush()
|
|
|
|
resp = await make_response("", 200)
|
|
resp.headers["HX-Refresh"] = "true"
|
|
return resp
|
|
|
|
@bp.post("/ticket-quantity/")
|
|
async def update_ticket_quantity():
|
|
"""Adjust reserved ticket count (+/- pattern, like products)."""
|
|
ident = current_cart_identity()
|
|
form = await request.form
|
|
entry_id = int(form.get("entry_id", 0))
|
|
count = max(int(form.get("count", 0)), 0)
|
|
tt_raw = (form.get("ticket_type_id") or "").strip()
|
|
ticket_type_id = int(tt_raw) if tt_raw else None
|
|
|
|
await call_action("events", "adjust-ticket-quantity", payload={
|
|
"entry_id": entry_id, "count": count,
|
|
"user_id": ident["user_id"],
|
|
"session_id": ident["session_id"],
|
|
"ticket_type_id": ticket_type_id,
|
|
})
|
|
|
|
resp = await make_response("", 200)
|
|
resp.headers["HX-Refresh"] = "true"
|
|
return resp
|
|
|
|
@bp.post("/delete/<int:product_id>/")
|
|
async def delete_item(product_id: int):
|
|
ident = current_cart_identity()
|
|
|
|
filters = [
|
|
CartItem.deleted_at.is_(None),
|
|
CartItem.product_id == product_id,
|
|
]
|
|
if ident["user_id"] is not None:
|
|
filters.append(CartItem.user_id == ident["user_id"])
|
|
else:
|
|
filters.append(CartItem.session_id == ident["session_id"])
|
|
|
|
existing = await g.s.scalar(select(CartItem).where(*filters))
|
|
|
|
if existing:
|
|
await g.s.delete(existing)
|
|
await g.s.flush()
|
|
|
|
resp = await make_response("", 200)
|
|
resp.headers["HX-Refresh"] = "true"
|
|
return resp
|
|
|
|
@bp.post("/checkout/")
|
|
async def checkout():
|
|
"""Global checkout — delegates order creation to orders service."""
|
|
cart = await get_cart(g.s)
|
|
calendar_entries = await get_calendar_cart_entries(g.s)
|
|
tickets = await get_ticket_cart_entries(g.s)
|
|
|
|
if not cart and not calendar_entries and not tickets:
|
|
return redirect(url_for("cart_overview.overview"))
|
|
|
|
product_total = total(cart) or 0
|
|
calendar_amount = calendar_total(calendar_entries) or 0
|
|
ticket_amount = ticket_total(tickets) or 0
|
|
cart_total = product_total + calendar_amount + ticket_amount
|
|
|
|
if cart_total <= 0:
|
|
return redirect(url_for("cart_overview.overview"))
|
|
|
|
try:
|
|
page_config = await resolve_page_config(g.s, cart, calendar_entries, tickets)
|
|
except ValueError as e:
|
|
html = await render_template(
|
|
"_types/cart/checkout_error.html",
|
|
order=None,
|
|
error=str(e),
|
|
)
|
|
return await make_response(html, 400)
|
|
|
|
ident = current_cart_identity()
|
|
|
|
# Serialize cart items for the orders service
|
|
cart_items_data = []
|
|
for ci in cart:
|
|
cart_items_data.append({
|
|
"product_id": ci.product_id,
|
|
"product_title": ci.product_title,
|
|
"product_slug": ci.product_slug,
|
|
"product_image": ci.product_image,
|
|
"product_regular_price": float(ci.product_regular_price) if ci.product_regular_price else None,
|
|
"product_special_price": float(ci.product_special_price) if ci.product_special_price else None,
|
|
"product_price_currency": ci.product_price_currency,
|
|
"quantity": ci.quantity,
|
|
})
|
|
|
|
# Serialize calendar entries and tickets
|
|
cal_data = []
|
|
for e in calendar_entries:
|
|
cal_data.append({
|
|
"id": e.id,
|
|
"calendar_container_id": getattr(e, "calendar_container_id", None),
|
|
})
|
|
ticket_data = []
|
|
for t in tickets:
|
|
ticket_data.append({
|
|
"id": t.id,
|
|
"calendar_container_id": getattr(t, "calendar_container_id", None),
|
|
})
|
|
|
|
page_post_id = None
|
|
if page_config:
|
|
page_post_id = getattr(page_config, "container_id", None)
|
|
|
|
result = await call_action("orders", "create-order", payload={
|
|
"cart_items": cart_items_data,
|
|
"calendar_entries": cal_data,
|
|
"tickets": ticket_data,
|
|
"user_id": ident.get("user_id"),
|
|
"session_id": ident.get("session_id"),
|
|
"product_total": float(product_total),
|
|
"calendar_total": float(calendar_amount),
|
|
"ticket_total": float(ticket_amount),
|
|
"page_post_id": page_post_id,
|
|
})
|
|
|
|
# Update redirect/webhook URLs with real order_id
|
|
order_id = result["order_id"]
|
|
hosted_url = result.get("sumup_hosted_url")
|
|
|
|
if not hosted_url:
|
|
html = await render_template(
|
|
"_types/cart/checkout_error.html",
|
|
order=None,
|
|
error="No hosted checkout URL returned from SumUp.",
|
|
)
|
|
return await make_response(html, 500)
|
|
|
|
return redirect(hosted_url)
|
|
|
|
return bp
|