"""Cart internal inbox endpoint. Receives AP-shaped activities from other services via HMAC-authenticated POST to ``/internal/inbox``. Routes to handlers registered via the internal inbox dispatch infrastructure. """ from __future__ import annotations import logging from datetime import datetime, timezone from quart import Blueprint, g, jsonify, request from sqlalchemy import select from shared.infrastructure.internal_inbox import dispatch_internal_activity, register_internal_handler from shared.infrastructure.internal_inbox_client import INBOX_HEADER from shared.models.market import CartItem log = logging.getLogger(__name__) def register() -> Blueprint: bp = Blueprint("inbox", __name__, url_prefix="/internal/inbox") @bp.before_request async def _require_inbox_header(): if not request.headers.get(INBOX_HEADER): return jsonify({"error": "forbidden"}), 403 from shared.infrastructure.internal_auth import validate_internal_request if not validate_internal_request(): return jsonify({"error": "forbidden"}), 403 @bp.post("") async def handle_inbox(): body = await request.get_json() if not body: return jsonify({"error": "empty body"}), 400 try: result = await dispatch_internal_activity(g.s, body) return jsonify(result) except ValueError as exc: return jsonify({"error": str(exc)}), 400 except Exception as exc: log.exception("Internal inbox dispatch failed") return jsonify({"error": str(exc)}), 500 # --- Handler: Add rose:CartItem --- async def _handle_add_cart_item(session, body: dict) -> dict: obj = body["object"] user_id = obj.get("user_id") session_id = obj.get("session_id") product_id = obj["product_id"] count = obj.get("quantity", 1) market_place_id = obj.get("market_place_id") # Look for existing cart item filters = [ CartItem.deleted_at.is_(None), CartItem.product_id == product_id, ] if user_id is not None: filters.append(CartItem.user_id == user_id) else: filters.append(CartItem.session_id == session_id) existing = await session.scalar(select(CartItem).where(*filters)) if existing: if count > 0: existing.quantity = count else: existing.deleted_at = datetime.now(timezone.utc) ci = existing else: if count <= 0: return {"ok": True, "action": "noop"} ci = CartItem( user_id=user_id, session_id=session_id, product_id=product_id, quantity=count, market_place_id=market_place_id, # Denormalized product data product_title=obj.get("product_title"), product_slug=obj.get("product_slug"), product_image=obj.get("product_image"), product_brand=obj.get("product_brand"), product_regular_price=obj.get("product_regular_price"), product_special_price=obj.get("product_special_price"), product_price_currency=obj.get("product_price_currency"), # Denormalized marketplace data market_place_name=obj.get("market_place_name"), market_place_container_id=obj.get("market_place_container_id"), ) session.add(ci) await session.flush() return { "ok": True, "cart_item_id": ci.id, "quantity": ci.quantity, } register_internal_handler("Add", "rose:CartItem", _handle_add_cart_item) # --- Handler: Remove rose:CartItem --- async def _handle_remove_cart_item(session, body: dict) -> dict: obj = body["object"] user_id = obj.get("user_id") session_id = obj.get("session_id") product_id = obj["product_id"] filters = [ CartItem.deleted_at.is_(None), CartItem.product_id == product_id, ] if user_id is not None: filters.append(CartItem.user_id == user_id) else: filters.append(CartItem.session_id == session_id) existing = await session.scalar(select(CartItem).where(*filters)) if existing: existing.deleted_at = datetime.now(timezone.utc) await session.flush() return {"ok": True} register_internal_handler("Remove", "rose:CartItem", _handle_remove_cart_item) # --- Handler: Update rose:CartItem --- async def _handle_update_cart_item(session, body: dict) -> dict: obj = body["object"] user_id = obj.get("user_id") session_id = obj.get("session_id") product_id = obj["product_id"] quantity = obj.get("quantity", 1) filters = [ CartItem.deleted_at.is_(None), CartItem.product_id == product_id, ] if user_id is not None: filters.append(CartItem.user_id == user_id) else: filters.append(CartItem.session_id == session_id) existing = await session.scalar(select(CartItem).where(*filters)) if existing: if quantity <= 0: existing.deleted_at = datetime.now(timezone.utc) else: existing.quantity = quantity await session.flush() return {"ok": True, "quantity": existing.quantity} return {"ok": True, "action": "noop"} register_internal_handler("Update", "rose:CartItem", _handle_update_cart_item) return bp