"""SQL-backed CartService implementation. Queries ``shared.models.market.CartItem`` — only this module may write to cart-domain tables on behalf of other domains. """ from __future__ import annotations from decimal import Decimal from sqlalchemy import select, update, func from sqlalchemy.ext.asyncio import AsyncSession from shared.models.market import CartItem from shared.contracts.dtos import CartItemDTO, CartSummaryDTO def _item_to_dto(ci: CartItem, product: dict | None) -> CartItemDTO: return CartItemDTO( id=ci.id, product_id=ci.product_id, quantity=ci.quantity, product_title=product["title"] if product else None, product_slug=product["slug"] if product else None, product_image=product["image"] if product else None, unit_price=Decimal(str(product.get("special_price") or product.get("regular_price") or 0)) if product else None, market_place_id=ci.market_place_id, ) async def _fetch_products_map(fetch_data, product_ids: list[int]) -> dict[int, dict]: """Fetch product details from market service, return {id: product_dict}.""" if not product_ids: return {} raw = await fetch_data("market", "products-by-ids", params={"ids": ",".join(str(i) for i in product_ids)}, required=False) or [] return {p["id"]: p for p in raw} class SqlCartService: async def cart_summary( self, session: AsyncSession, *, user_id: int | None, session_id: str | None, page_slug: str | None = None, ) -> CartSummaryDTO: """Build a lightweight cart summary for the current identity.""" from shared.infrastructure.data_client import fetch_data from shared.contracts.dtos import CalendarEntryDTO, TicketDTO, dto_from_dict # Resolve page filter via blog data endpoint page_post_id: int | None = None if page_slug: post = await fetch_data("blog", "post-by-slug", params={"slug": page_slug}, required=False) if post and post.get("is_page"): page_post_id = post["id"] # --- product cart --- cart_q = select(CartItem).where(CartItem.deleted_at.is_(None)) if user_id is not None: cart_q = cart_q.where(CartItem.user_id == user_id) elif session_id is not None: cart_q = cart_q.where(CartItem.session_id == session_id) else: return CartSummaryDTO() if page_post_id is not None: mps = await fetch_data("market", "marketplaces-for-container", params={"type": "page", "id": page_post_id}, required=False) or [] mp_ids = [mp["id"] for mp in mps] if mp_ids: cart_q = cart_q.where(CartItem.market_place_id.in_(mp_ids)) else: return CartSummaryDTO() result = await session.execute(cart_q) cart_items = result.scalars().all() products = await _fetch_products_map( fetch_data, list({ci.product_id for ci in cart_items}), ) count = sum(ci.quantity for ci in cart_items) total = Decimal("0") for ci in cart_items: p = products.get(ci.product_id) if p: price = p.get("special_price") or p.get("regular_price") if price: total += Decimal(str(price)) * ci.quantity # --- calendar entries via events data endpoint --- cal_params: dict = {} if user_id is not None: cal_params["user_id"] = user_id if session_id is not None: cal_params["session_id"] = session_id if page_post_id is not None: cal_params["page_id"] = page_post_id raw_entries = await fetch_data("events", "entries-for-page", params=cal_params, required=False) or [] else: raw_entries = await fetch_data("events", "pending-entries", params=cal_params, required=False) or [] cal_entries = [dto_from_dict(CalendarEntryDTO, e) for e in raw_entries] calendar_count = len(cal_entries) calendar_total = sum(Decimal(str(e.cost or 0)) for e in cal_entries if e.cost is not None) # --- tickets via events data endpoint --- if page_post_id is not None: raw_tickets = await fetch_data("events", "tickets-for-page", params=cal_params, required=False) or [] else: tk_params = {k: v for k, v in cal_params.items() if k != "page_id"} raw_tickets = await fetch_data("events", "pending-tickets", params=tk_params, required=False) or [] tickets = [dto_from_dict(TicketDTO, t) for t in raw_tickets] ticket_count = len(tickets) ticket_total = sum(Decimal(str(t.price or 0)) for t in tickets) items = [_item_to_dto(ci, products.get(ci.product_id)) for ci in cart_items] return CartSummaryDTO( count=count, total=total, calendar_count=calendar_count, calendar_total=calendar_total, items=items, ticket_count=ticket_count, ticket_total=ticket_total, ) async def cart_items( self, session: AsyncSession, *, user_id: int | None, session_id: str | None, ) -> list[CartItemDTO]: from shared.infrastructure.data_client import fetch_data cart_q = select(CartItem).where(CartItem.deleted_at.is_(None)) if user_id is not None: cart_q = cart_q.where(CartItem.user_id == user_id) elif session_id is not None: cart_q = cart_q.where(CartItem.session_id == session_id) else: return [] cart_q = cart_q.order_by(CartItem.created_at.desc()) result = await session.execute(cart_q) items = result.scalars().all() products = await _fetch_products_map( fetch_data, list({ci.product_id for ci in items}), ) return [_item_to_dto(ci, products.get(ci.product_id)) for ci in items] async def adopt_cart_for_user( self, session: AsyncSession, user_id: int, session_id: str, ) -> None: """Adopt anonymous cart items for a logged-in user.""" anon_result = await session.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 session.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