Domain isolation: replace cross-domain imports with service calls
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 44s

Replace direct Post, Calendar, CalendarEntry model queries and glue
lifecycle imports with typed service calls. Cart registers all 4
services via domain_services_fn with has() guards.

Key changes:
- app.py: use domain_services_fn, Post query → services.blog
- api.py: Calendar/CalendarEntry → services.calendar
- checkout: glue order_lifecycle → services.calendar.claim/confirm
- calendar_cart: CalendarEntry → services.calendar.pending_entries()
- page_cart: Post/Calendar queries → services.blog/calendar
- global_routes: glue imports → service calls

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-19 04:30:17 +00:00
parent b7f09d638d
commit 049b35479b
9 changed files with 72 additions and 101 deletions

13
app.py
View File

@@ -46,7 +46,7 @@ async def cart_context() -> dict:
Global cart_count / cart_total stay global for cart-mini. Global cart_count / cart_total stay global for cart-mini.
""" """
from shared.infrastructure.context import base_context from shared.infrastructure.context import base_context
from glue.services.navigation import get_navigation_tree from shared.services.navigation import get_navigation_tree
ctx = await base_context() ctx = await base_context()
@@ -81,13 +81,14 @@ async def cart_context() -> dict:
def create_app() -> "Quart": def create_app() -> "Quart":
from shared.models.ghost_content import Post
from shared.models.page_config import PageConfig from shared.models.page_config import PageConfig
from services import register_domain_services
app = create_base_app( app = create_base_app(
"cart", "cart",
context_fn=cart_context, context_fn=cart_context,
before_request_fns=[_load_cart], before_request_fns=[_load_cart],
domain_services_fn=register_domain_services,
) )
# App-specific templates override shared templates # App-specific templates override shared templates
@@ -116,12 +117,8 @@ def create_app() -> "Quart":
slug = getattr(g, "page_slug", None) slug = getattr(g, "page_slug", None)
if not slug: if not slug:
return return
post = ( post = await services.blog.get_post_by_slug(g.s, slug)
await g.s.execute( if not post or not post.is_page:
select(Post).where(Post.slug == slug, Post.is_page == True) # noqa: E712
)
).scalar_one_or_none()
if not post:
abort(404) abort(404)
g.page_post = post g.page_post = post
g.page_config = ( g.page_config = (

View File

@@ -12,10 +12,9 @@ from sqlalchemy.orm import selectinload
from shared.models.market import CartItem from shared.models.market import CartItem
from shared.models.market_place import MarketPlace from shared.models.market_place import MarketPlace
from shared.models.calendars import CalendarEntry, Calendar
from shared.models.ghost_content import Post
from shared.browser.app.csrf import csrf_exempt from shared.browser.app.csrf import csrf_exempt
from shared.infrastructure.cart_identity import current_cart_identity from shared.infrastructure.cart_identity import current_cart_identity
from shared.services.registry import services
def register() -> Blueprint: def register() -> Blueprint:
@@ -38,12 +37,8 @@ def register() -> Blueprint:
page_slug = request.args.get("page_slug") page_slug = request.args.get("page_slug")
page_post_id = None page_post_id = None
if page_slug: if page_slug:
post = ( post = await services.blog.get_post_by_slug(g.s, page_slug)
await g.s.execute( if post and post.is_page:
select(Post).where(Post.slug == page_slug, Post.is_page == True) # noqa: E712
)
).scalar_one_or_none()
if post:
page_post_id = post.id page_post_id = post.id
# --- product cart --- # --- product cart ---
@@ -73,26 +68,19 @@ def register() -> Blueprint:
if ci.product and (ci.product.special_price or ci.product.regular_price) if ci.product and (ci.product.special_price or ci.product.regular_price)
) )
# --- calendar entries --- # --- calendar entries via service ---
cal_q = select(CalendarEntry).where(
CalendarEntry.deleted_at.is_(None),
CalendarEntry.state == "pending",
)
if ident["user_id"] is not None:
cal_q = cal_q.where(CalendarEntry.user_id == ident["user_id"])
else:
cal_q = cal_q.where(CalendarEntry.session_id == ident["session_id"])
if page_post_id is not None: if page_post_id is not None:
cal_ids = select(Calendar.id).where( cal_entries = await services.calendar.entries_for_page(
Calendar.container_type == "page", g.s, page_post_id,
Calendar.container_id == page_post_id, user_id=ident["user_id"],
Calendar.deleted_at.is_(None), session_id=ident["session_id"],
).scalar_subquery() )
cal_q = cal_q.where(CalendarEntry.calendar_id.in_(cal_ids)) else:
cal_entries = await services.calendar.pending_entries(
cal_result = await g.s.execute(cal_q) g.s,
cal_entries = cal_result.scalars().all() user_id=ident["user_id"],
session_id=ident["session_id"],
)
calendar_count = len(cal_entries) calendar_count = len(cal_entries)
calendar_total = sum((e.cost or 0) for e in cal_entries if e.cost is not None) calendar_total = sum((e.cost or 0) for e in cal_entries if e.cost is not None)

View File

@@ -6,9 +6,8 @@ from quart import Blueprint, g, request, render_template, redirect, url_for, mak
from sqlalchemy import select from sqlalchemy import select
from shared.models.order import Order from shared.models.order import Order
from shared.models.ghost_content import Post
from shared.models.market_place import MarketPlace from shared.models.market_place import MarketPlace
from glue.services.order_lifecycle import get_entries_for_order from shared.services.registry import services
from .services import ( from .services import (
current_cart_identity, current_cart_identity,
get_cart, get_cart,
@@ -182,7 +181,7 @@ def register(url_prefix: str) -> Blueprint:
# Resolve page/market slugs so product links render correctly # Resolve page/market slugs so product links render correctly
if order.page_config: if order.page_config:
post = await g.s.get(Post, order.page_config.container_id) post = await services.blog.get_post_by_id(g.s, order.page_config.container_id)
if post: if post:
g.page_slug = post.slug g.page_slug = post.slug
result = await g.s.execute( result = await g.s.execute(
@@ -204,7 +203,7 @@ def register(url_prefix: str) -> Blueprint:
status = (order.status or "pending").lower() status = (order.status or "pending").lower()
calendar_entries = await get_entries_for_order(g.s, order.id) calendar_entries = await services.calendar.get_entries_for_order(g.s, order.id)
await g.s.flush() await g.s.flush()
html = await render_template( html = await render_template(

View File

@@ -1,38 +1,20 @@
from __future__ import annotations from __future__ import annotations
from sqlalchemy import select from shared.services.registry import services
from sqlalchemy.orm import selectinload
from shared.models.calendars import CalendarEntry
from .identity import current_cart_identity from .identity import current_cart_identity
async def get_calendar_cart_entries(session): async def get_calendar_cart_entries(session):
""" """
Return all *pending* calendar entries for the current cart identity Return all *pending* calendar entries (as CalendarEntryDTOs) for the
(user or anonymous session). current cart identity (user or anonymous session).
""" """
ident = current_cart_identity() ident = current_cart_identity()
return await services.calendar.pending_entries(
filters = [ session,
CalendarEntry.deleted_at.is_(None), user_id=ident["user_id"],
CalendarEntry.state == "pending", session_id=ident["session_id"],
]
if ident["user_id"] is not None:
filters.append(CalendarEntry.user_id == ident["user_id"])
else:
filters.append(CalendarEntry.session_id == ident["session_id"])
result = await session.execute(
select(CalendarEntry)
.where(*filters)
.order_by(CalendarEntry.start_at.asc())
.options(
selectinload(CalendarEntry.calendar),
)
) )
return result.scalars().all()
def calendar_total(entries) -> float: def calendar_total(entries) -> float:

View File

@@ -1,6 +1,6 @@
from shared.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 shared.events import emit_event from shared.events import emit_event
from glue.services.order_lifecycle import confirm_entries_for_order from shared.services.registry import services
async def check_sumup_status(session, order): async def check_sumup_status(session, order):
@@ -13,7 +13,7 @@ async def check_sumup_status(session, order):
if sumup_status == "PAID": if sumup_status == "PAID":
if order.status != "paid": if order.status != "paid":
order.status = "paid" order.status = "paid"
await confirm_entries_for_order( await services.calendar.confirm_entries_for_order(
session, order.id, order.user_id, order.session_id session, order.id, order.user_id, order.session_id
) )
await emit_event(session, "order.paid", "order", order.id, { await emit_event(session, "order.paid", "order", order.id, {

View File

@@ -9,12 +9,12 @@ from sqlalchemy.orm import selectinload
from shared.models.market import Product, CartItem from shared.models.market import Product, CartItem
from shared.models.order import Order, OrderItem from shared.models.order import Order, OrderItem
from shared.models.calendars import CalendarEntry, Calendar from shared.models.calendars import Calendar
from shared.models.page_config import PageConfig from shared.models.page_config import PageConfig
from shared.models.market_place import MarketPlace from shared.models.market_place import MarketPlace
from shared.config import config from shared.config import config
from shared.events import emit_event from shared.events import emit_event
from glue.services.order_lifecycle import claim_entries_for_order from shared.services.registry import services
async def find_or_create_cart_item( async def find_or_create_cart_item(
@@ -150,8 +150,8 @@ async def create_order_from_cart(
) )
session.add(oi) session.add(oi)
# Mark pending calendar entries as "ordered" via glue service # Mark pending calendar entries as "ordered" via calendar service
await claim_entries_for_order( await services.calendar.claim_entries_for_order(
session, order.id, user_id, session_id, page_post_id session, order.id, user_id, session_id, page_post_id
) )

View File

@@ -14,9 +14,8 @@ from sqlalchemy.orm import selectinload
from shared.models.market import CartItem from shared.models.market import CartItem
from shared.models.market_place import MarketPlace from shared.models.market_place import MarketPlace
from shared.models.calendars import CalendarEntry, Calendar
from shared.models.ghost_content import Post
from shared.models.page_config import PageConfig from shared.models.page_config import PageConfig
from shared.services.registry import services
from .identity import current_cart_identity from .identity import current_cart_identity
@@ -48,30 +47,14 @@ async def get_cart_for_page(session, post_id: int) -> list[CartItem]:
return result.scalars().all() return result.scalars().all()
async def get_calendar_entries_for_page(session, post_id: int) -> list[CalendarEntry]: async def get_calendar_entries_for_page(session, post_id: int):
"""Return pending calendar entries scoped to a specific page (via Calendar.container_id).""" """Return pending calendar entries (DTOs) scoped to a specific page."""
ident = current_cart_identity() ident = current_cart_identity()
return await services.calendar.entries_for_page(
filters = [ session, post_id,
CalendarEntry.deleted_at.is_(None), user_id=ident["user_id"],
CalendarEntry.state == "pending", session_id=ident["session_id"],
Calendar.container_type == "page",
Calendar.container_id == post_id,
Calendar.deleted_at.is_(None),
]
if ident["user_id"] is not None:
filters.append(CalendarEntry.user_id == ident["user_id"])
else:
filters.append(CalendarEntry.session_id == ident["session_id"])
result = await session.execute(
select(CalendarEntry)
.join(Calendar, CalendarEntry.calendar_id == Calendar.id)
.where(*filters)
.order_by(CalendarEntry.start_at.asc())
.options(selectinload(CalendarEntry.calendar))
) )
return result.scalars().all()
async def get_cart_grouped_by_page(session) -> list[dict]: async def get_cart_grouped_by_page(session) -> list[dict]:
@@ -125,16 +108,13 @@ async def get_cart_grouped_by_page(session) -> list[dict]:
groups[pid]["post_id"] = pid groups[pid]["post_id"] = pid
groups[pid]["calendar_entries"].append(ce) groups[pid]["calendar_entries"].append(ce)
# Batch-load Post and PageConfig objects # Batch-load Post DTOs and PageConfig objects
post_ids = [pid for pid in groups if pid is not None] post_ids = [pid for pid in groups if pid is not None]
posts_by_id: dict[int, Post] = {} posts_by_id: dict[int, object] = {}
configs_by_post: dict[int, PageConfig] = {} configs_by_post: dict[int, PageConfig] = {}
if post_ids: if post_ids:
post_result = await session.execute( for p in await services.blog.get_posts_by_ids(session, post_ids):
select(Post).where(Post.id.in_(post_ids))
)
for p in post_result.scalars().all():
posts_by_id[p.id] = p posts_by_id[p.id] = p
pc_result = await session.execute( pc_result = await session.execute(

25
services/__init__.py Normal file
View File

@@ -0,0 +1,25 @@
"""Cart app service registration."""
from __future__ import annotations
def register_domain_services() -> None:
"""Register services for the cart app.
Cart owns: Order, OrderItem.
Standard deployment registers all 4 services as real DB impls
(shared DB). For composable deployments, swap non-owned services
with stubs from shared.services.stubs.
"""
from shared.services.registry import services
from shared.services.blog_impl import SqlBlogService
from shared.services.calendar_impl import SqlCalendarService
from shared.services.market_impl import SqlMarketService
from shared.services.cart_impl import SqlCartService
services.cart = SqlCartService()
if not services.has("blog"):
services.blog = SqlBlogService()
if not services.has("calendar"):
services.calendar = SqlCalendarService()
if not services.has("market"):
services.market = SqlMarketService()

2
shared

Submodule shared updated: ea7dc9723a...70b1c7de10