This repository has been archived on 2026-02-24. You can view files and clone it. You cannot open issues or pull requests or push a commit.
Files
cart/bp/cart/api.py
giles 967303093d
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
feat: initialize cart app with blueprints, templates, and CI
Extract cart, order, and orders blueprints with their service layer,
templates, Dockerfile (APP_MODULE=app:app, IMAGE=cart), entrypoint,
and Gitea CI workflow from the coop monolith.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-09 23:17:41 +00:00

149 lines
4.9 KiB
Python

"""
Internal JSON API for the cart app.
These endpoints are called by other apps (coop, market) over HTTP.
They are CSRF-exempt because they are server-to-server calls.
"""
from __future__ import annotations
from quart import Blueprint, g, request, jsonify
from sqlalchemy import select, update, func
from sqlalchemy.orm import selectinload
from models.market import CartItem
from models.calendars import CalendarEntry
from suma_browser.app.csrf import csrf_exempt
from shared.cart_identity import current_cart_identity
def register() -> Blueprint:
bp = Blueprint("cart_api", __name__, url_prefix="/internal/cart")
@bp.get("/summary")
@csrf_exempt
async def summary():
"""
Return a lightweight cart summary (count + total) for the
current session/user. Called by coop and market apps to
populate the cart-mini widget without importing cart services.
"""
ident = current_cart_identity()
# --- product cart ---
cart_filters = [CartItem.deleted_at.is_(None)]
if ident["user_id"] is not None:
cart_filters.append(CartItem.user_id == ident["user_id"])
else:
cart_filters.append(CartItem.session_id == ident["session_id"])
result = await g.s.execute(
select(CartItem)
.where(*cart_filters)
.options(selectinload(CartItem.product))
.order_by(CartItem.created_at.desc())
)
cart_items = result.scalars().all()
cart_count = sum(ci.quantity for ci in cart_items)
cart_total = sum(
(ci.product.special_price or ci.product.regular_price or 0) * ci.quantity
for ci in cart_items
if ci.product and (ci.product.special_price or ci.product.regular_price)
)
# --- calendar entries ---
cal_filters = [
CalendarEntry.deleted_at.is_(None),
CalendarEntry.state == "pending",
]
if ident["user_id"] is not None:
cal_filters.append(CalendarEntry.user_id == ident["user_id"])
else:
cal_filters.append(CalendarEntry.session_id == ident["session_id"])
cal_result = await g.s.execute(
select(CalendarEntry).where(*cal_filters)
)
cal_entries = cal_result.scalars().all()
calendar_count = len(cal_entries)
calendar_total = sum((e.cost or 0) for e in cal_entries if e.cost is not None)
items = [
{
"slug": ci.product.slug if ci.product else None,
"title": ci.product.title if ci.product else None,
"image": ci.product.image if ci.product else None,
"quantity": ci.quantity,
"price": float(ci.product.special_price or ci.product.regular_price or 0)
if ci.product
else 0,
}
for ci in cart_items
]
return jsonify(
{
"count": cart_count,
"total": float(cart_total),
"calendar_count": calendar_count,
"calendar_total": float(calendar_total),
"items": items,
}
)
@bp.post("/adopt")
@csrf_exempt
async def adopt():
"""
Adopt anonymous cart items + calendar entries for a user.
Called by the coop app after successful login.
Body: {"user_id": int, "session_id": str}
"""
data = await request.get_json() or {}
user_id = data.get("user_id")
session_id = data.get("session_id")
if not user_id or not session_id:
return jsonify({"ok": False, "error": "user_id and session_id required"}), 400
# --- adopt cart items ---
anon_result = await g.s.execute(
select(CartItem).where(
CartItem.deleted_at.is_(None),
CartItem.user_id.is_(None),
CartItem.session_id == session_id,
)
)
anon_items = anon_result.scalars().all()
if anon_items:
# Soft-delete existing user cart
await g.s.execute(
update(CartItem)
.where(CartItem.deleted_at.is_(None), CartItem.user_id == user_id)
.values(deleted_at=func.now())
)
for ci in anon_items:
ci.user_id = user_id
# --- adopt calendar entries ---
await g.s.execute(
update(CalendarEntry)
.where(CalendarEntry.deleted_at.is_(None), CalendarEntry.user_id == user_id)
.values(deleted_at=func.now())
)
cal_result = await g.s.execute(
select(CalendarEntry).where(
CalendarEntry.deleted_at.is_(None),
CalendarEntry.session_id == session_id,
)
)
for entry in cal_result.scalars().all():
entry.user_id = user_id
return jsonify({"ok": True})
return bp