"""Order creation and SumUp checkout helpers. Moved from cart/bp/cart/services/checkout.py. Only the order-side logic lives here; find_or_create_cart_item stays in cart. """ from __future__ import annotations from typing import Optional from urllib.parse import urlencode from types import SimpleNamespace from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy.orm import selectinload from shared.models.order import Order, OrderItem from shared.config import config from shared.events import emit_activity from shared.infrastructure.actions import call_action from shared.infrastructure.data_client import fetch_data async def resolve_page_config_from_post_id(post_id: int) -> Optional[SimpleNamespace]: """Fetch the PageConfig for *post_id* from the blog service.""" raw_pc = await fetch_data( "blog", "page-config", params={"container_type": "page", "container_id": post_id}, required=False, ) return SimpleNamespace(**raw_pc) if raw_pc else None async def create_order( session: AsyncSession, cart_items: list[dict], calendar_entries: list, user_id: Optional[int], session_id: Optional[str], product_total: float, calendar_total: float, *, ticket_total: float = 0, page_post_id: int | None = None, ) -> Order: """Create an Order + OrderItems from serialized cart data.""" cart_total = product_total + calendar_total + ticket_total currency = (cart_items[0].get("product_price_currency") if cart_items else None) or "GBP" order = Order( user_id=user_id, session_id=session_id, status="pending", currency=currency, total_amount=cart_total, ) session.add(order) await session.flush() for ci in cart_items: price = ci.get("product_special_price") or ci.get("product_regular_price") or 0 oi = OrderItem( order=order, product_id=ci["product_id"], product_title=ci.get("product_title"), product_slug=ci.get("product_slug"), product_image=ci.get("product_image"), quantity=ci.get("quantity", 1), unit_price=price, currency=currency, ) session.add(oi) await call_action("events", "claim-entries-for-order", payload={ "order_id": order.id, "user_id": user_id, "session_id": session_id, "page_post_id": page_post_id, }) await call_action("events", "claim-tickets-for-order", payload={ "order_id": order.id, "user_id": user_id, "session_id": session_id, "page_post_id": page_post_id, }) await emit_activity( session, activity_type="Create", actor_uri="internal:orders", object_type="rose:Order", object_data={ "order_id": order.id, "user_id": user_id, "session_id": session_id, }, source_type="order", source_id=order.id, ) return order def build_sumup_description(cart_items: list[dict], order_id: int, *, ticket_count: int = 0) -> str: titles = [ci.get("product_title") for ci in cart_items if ci.get("product_title")] item_count = sum(ci.get("quantity", 1) for ci in cart_items) parts = [] if titles: if len(titles) <= 3: parts.append(", ".join(titles)) else: parts.append(", ".join(titles[:3]) + f" + {len(titles) - 3} more") if ticket_count: parts.append(f"{ticket_count} ticket{'s' if ticket_count != 1 else ''}") summary = ", ".join(parts) if parts else "order items" total_count = item_count + ticket_count return f"Order {order_id} ({total_count} item{'s' if total_count != 1 else ''}): {summary}" def build_sumup_reference(order_id: int, page_config=None) -> str: if page_config and page_config.sumup_checkout_prefix: prefix = page_config.sumup_checkout_prefix else: sumup_cfg = config().get("sumup", {}) or {} prefix = sumup_cfg.get("checkout_reference_prefix", "") return f"{prefix}{order_id}" def build_webhook_url(base_url: str) -> str: sumup_cfg = config().get("sumup", {}) or {} webhook_secret = sumup_cfg.get("webhook_secret") if webhook_secret: sep = "&" if "?" in base_url else "?" return f"{base_url}{sep}{urlencode({'token': webhook_secret})}" return base_url def validate_webhook_secret(token: Optional[str]) -> bool: sumup_cfg = config().get("sumup", {}) or {} webhook_secret = sumup_cfg.get("webhook_secret") if not webhook_secret: return True return token is not None and token == webhook_secret async def get_order_with_details(session: AsyncSession, order_id: int) -> Optional[Order]: result = await session.execute( select(Order) .options(selectinload(Order.items)) .where(Order.id == order_id) ) return result.scalar_one_or_none()