feat: decouple cart from shared_lib, add app-owned models

Phase 1-3 of decoupling:
- path_setup.py adds project root to sys.path
- Cart-owned models in cart/models/ (order, page_config)
- All imports updated: shared.infrastructure, shared.db, shared.browser, etc.
- PageConfig uses container_type/container_id instead of post_id FK

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-11 12:46:34 +00:00
parent 8ce8fc5380
commit 5d0653bf2e
25 changed files with 345 additions and 84 deletions

View File

@@ -10,12 +10,12 @@ 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.market_place import MarketPlace
from models.calendars import CalendarEntry, Calendar
from models.ghost_content import Post
from suma_browser.app.csrf import csrf_exempt
from shared.cart_identity import current_cart_identity
from market.models.market import CartItem
from market.models.market_place import MarketPlace
from events.models.calendars import CalendarEntry, Calendar
from blog.models.ghost_content import Post
from shared.browser.app.csrf import csrf_exempt
from shared.infrastructure.cart_identity import current_cart_identity
def register() -> Blueprint:
@@ -55,7 +55,8 @@ def register() -> Blueprint:
if page_post_id is not None:
mp_ids = select(MarketPlace.id).where(
MarketPlace.post_id == page_post_id,
MarketPlace.container_type == "page",
MarketPlace.container_id == page_post_id,
MarketPlace.deleted_at.is_(None),
).scalar_subquery()
cart_q = cart_q.where(CartItem.market_place_id.in_(mp_ids))
@@ -84,7 +85,8 @@ def register() -> Blueprint:
if page_post_id is not None:
cal_ids = select(Calendar.id).where(
Calendar.post_id == page_post_id,
Calendar.container_type == "page",
Calendar.container_id == page_post_id,
Calendar.deleted_at.is_(None),
).scalar_subquery()
cal_q = cal_q.where(CalendarEntry.calendar_id.in_(cal_ids))

View File

@@ -6,7 +6,7 @@ from quart import Blueprint, g, request, render_template, redirect, url_for, mak
from sqlalchemy import select
from models.order import Order
from suma_browser.app.utils.htmx import is_htmx_request
from shared.browser.app.utils.htmx import is_htmx_request
from .services import (
current_cart_identity,
get_cart,
@@ -26,8 +26,8 @@ from .services.checkout import (
validate_webhook_secret,
get_order_with_details,
)
from suma_browser.app.payments.sumup import create_checkout as sumup_create_checkout
from config import config
from shared.browser.app.payments.sumup import create_checkout as sumup_create_checkout
from shared.config import config
def register(url_prefix: str) -> Blueprint:

View File

@@ -6,7 +6,7 @@ from quart import g, session as qsession
from sqlalchemy import select
from typing import Optional
from models.market import CartItem
from market.models.market import CartItem
async def merge_anonymous_cart_into_user(user_id: int) -> None:

View File

@@ -4,7 +4,7 @@ from __future__ import annotations
from quart import Blueprint, render_template, make_response
from suma_browser.app.utils.htmx import is_htmx_request
from shared.browser.app.utils.htmx import is_htmx_request
from .services import get_cart_grouped_by_page

View File

@@ -4,9 +4,9 @@ from __future__ import annotations
from quart import Blueprint, g, render_template, redirect, make_response, url_for
from suma_browser.app.utils.htmx import is_htmx_request
from suma_browser.app.payments.sumup import create_checkout as sumup_create_checkout
from config import config
from shared.browser.app.utils.htmx import is_htmx_request
from shared.browser.app.payments.sumup import create_checkout as sumup_create_checkout
from shared.config import config
from .services import (
total,
clear_cart_for_order,

View File

@@ -6,9 +6,9 @@ from quart import Blueprint, g, request, render_template, redirect, url_for, mak
from sqlalchemy import select, update
from sqlalchemy.orm import selectinload
from models.market import Product, CartItem
from market.models.market import Product, CartItem
from models.order import Order, OrderItem
from suma_browser.app.payments.sumup import create_checkout as sumup_create_checkout
from shared.browser.app.payments.sumup import create_checkout as sumup_create_checkout
from .services import (
current_cart_identity,
get_cart,
@@ -28,9 +28,9 @@ from .services.checkout import (
validate_webhook_secret,
get_order_with_details,
)
from config import config
from models.calendars import CalendarEntry # NEW
from suma_browser.app.utils.htmx import is_htmx_request
from shared.config import config
from events.models.calendars import CalendarEntry # NEW
from shared.browser.app.utils.htmx import is_htmx_request
def register(url_prefix: str) -> Blueprint:
bp = Blueprint("cart", __name__, url_prefix=url_prefix)

View File

@@ -1,6 +1,6 @@
from sqlalchemy import select, update, func
from models.market import CartItem
from market.models.market import CartItem
async def adopt_session_cart_for_user(session, user_id: int, session_id: str | None) -> None:

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from models.calendars import CalendarEntry
from events.models.calendars import CalendarEntry
from .identity import current_cart_identity

View File

@@ -1,6 +1,6 @@
from suma_browser.app.payments.sumup import get_checkout as sumup_get_checkout
from shared.browser.app.payments.sumup import get_checkout as sumup_get_checkout
from sqlalchemy import update
from models.calendars import CalendarEntry
from events.models.calendars import CalendarEntry
async def check_sumup_status(session, order):

View File

@@ -7,12 +7,12 @@ from sqlalchemy import select, update
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
from models.market import Product, CartItem
from market.models.market import Product, CartItem
from models.order import Order, OrderItem
from models.calendars import CalendarEntry, Calendar
from events.models.calendars import CalendarEntry, Calendar
from models.page_config import PageConfig
from models.market_place import MarketPlace
from config import config
from market.models.market_place import MarketPlace
from shared.config import config
async def find_or_create_cart_item(
@@ -76,13 +76,13 @@ async def resolve_page_config(
if ci.market_place_id:
mp = await session.get(MarketPlace, ci.market_place_id)
if mp:
post_ids.add(mp.post_id)
post_ids.add(mp.container_id)
# From calendar entries via calendar
for entry in calendar_entries:
cal = await session.get(Calendar, entry.calendar_id)
if cal and cal.post_id:
post_ids.add(cal.post_id)
if cal and cal.container_id:
post_ids.add(cal.container_id)
if len(post_ids) > 1:
raise ValueError("Cannot checkout items from multiple pages")
@@ -92,7 +92,10 @@ async def resolve_page_config(
post_id = post_ids.pop()
pc = (await session.execute(
select(PageConfig).where(PageConfig.post_id == post_id)
select(PageConfig).where(
PageConfig.container_type == "page",
PageConfig.container_id == post_id,
)
)).scalar_one_or_none()
return pc
@@ -158,7 +161,8 @@ async def create_order_from_cart(
if page_post_id is not None:
cal_ids = select(Calendar.id).where(
Calendar.post_id == page_post_id,
Calendar.container_type == "page",
Calendar.container_id == page_post_id,
Calendar.deleted_at.is_(None),
).scalar_subquery()
calendar_filters.append(CalendarEntry.calendar_id.in_(cal_ids))

View File

@@ -1,7 +1,7 @@
from sqlalchemy import update, func, select
from models.market import CartItem
from models.market_place import MarketPlace
from market.models.market import CartItem
from market.models.market_place import MarketPlace
from models.order import Order
@@ -24,7 +24,8 @@ async def clear_cart_for_order(session, order: Order, *, page_post_id: int | Non
if page_post_id is not None:
mp_ids = select(MarketPlace.id).where(
MarketPlace.post_id == page_post_id,
MarketPlace.container_type == "page",
MarketPlace.container_id == page_post_id,
MarketPlace.deleted_at.is_(None),
).scalar_subquery()
filters.append(CartItem.market_place_id.in_(mp_ids))

View File

@@ -1,7 +1,7 @@
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from models.market import CartItem
from market.models.market import CartItem
from .identity import current_cart_identity
async def get_cart(session):

View File

@@ -1,4 +1,4 @@
# Re-export from canonical shared location
from shared.cart_identity import CartIdentity, current_cart_identity
from shared.infrastructure.cart_identity import CartIdentity, current_cart_identity
__all__ = ["CartIdentity", "current_cart_identity"]

View File

@@ -2,7 +2,8 @@
Page-scoped cart queries.
Groups cart items and calendar entries by their owning page (Post),
determined via CartItem.market_place.post_id and CalendarEntry.calendar.post_id.
determined via CartItem.market_place.container_id and CalendarEntry.calendar.container_id
(where container_type == "page").
"""
from __future__ import annotations
@@ -11,21 +12,22 @@ from collections import defaultdict
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from models.market import CartItem
from models.market_place import MarketPlace
from models.calendars import CalendarEntry, Calendar
from models.ghost_content import Post
from market.models.market import CartItem
from market.models.market_place import MarketPlace
from events.models.calendars import CalendarEntry, Calendar
from blog.models.ghost_content import Post
from models.page_config import PageConfig
from .identity import current_cart_identity
async def get_cart_for_page(session, post_id: int) -> list[CartItem]:
"""Return cart items scoped to a specific page (via MarketPlace.post_id)."""
"""Return cart items scoped to a specific page (via MarketPlace.container_id)."""
ident = current_cart_identity()
filters = [
CartItem.deleted_at.is_(None),
MarketPlace.post_id == post_id,
MarketPlace.container_type == "page",
MarketPlace.container_id == post_id,
MarketPlace.deleted_at.is_(None),
]
if ident["user_id"] is not None:
@@ -47,13 +49,14 @@ async def get_cart_for_page(session, post_id: int) -> list[CartItem]:
async def get_calendar_entries_for_page(session, post_id: int) -> list[CalendarEntry]:
"""Return pending calendar entries scoped to a specific page (via Calendar.post_id)."""
"""Return pending calendar entries scoped to a specific page (via Calendar.container_id)."""
ident = current_cart_identity()
filters = [
CalendarEntry.deleted_at.is_(None),
CalendarEntry.state == "pending",
Calendar.post_id == post_id,
Calendar.container_type == "page",
Calendar.container_id == post_id,
Calendar.deleted_at.is_(None),
]
if ident["user_id"] is not None:
@@ -99,7 +102,7 @@ async def get_cart_grouped_by_page(session) -> list[dict]:
cart_items = await get_cart(session)
cal_entries = await get_calendar_cart_entries(session)
# Group by post_id
# Group by container_id (all current data has container_type="page")
groups: dict[int | None, dict] = defaultdict(lambda: {
"post_id": None,
"cart_items": [],
@@ -107,16 +110,16 @@ async def get_cart_grouped_by_page(session) -> list[dict]:
})
for ci in cart_items:
if ci.market_place and ci.market_place.post_id:
pid = ci.market_place.post_id
if ci.market_place and ci.market_place.container_id:
pid = ci.market_place.container_id
else:
pid = None
groups[pid]["post_id"] = pid
groups[pid]["cart_items"].append(ci)
for ce in cal_entries:
if ce.calendar and ce.calendar.post_id:
pid = ce.calendar.post_id
if ce.calendar and ce.calendar.container_id:
pid = ce.calendar.container_id
else:
pid = None
groups[pid]["post_id"] = pid
@@ -135,10 +138,13 @@ async def get_cart_grouped_by_page(session) -> list[dict]:
posts_by_id[p.id] = p
pc_result = await session.execute(
select(PageConfig).where(PageConfig.post_id.in_(post_ids))
select(PageConfig).where(
PageConfig.container_type == "page",
PageConfig.container_id.in_(post_ids),
)
)
for pc in pc_result.scalars().all():
configs_by_post[pc.post_id] = pc
configs_by_post[pc.container_id] = pc
# Build result list (pages first, orphan last)
result = []