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.
"""
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()
@@ -81,13 +81,14 @@ async def cart_context() -> dict:
def create_app() -> "Quart":
from shared.models.ghost_content import Post
from shared.models.page_config import PageConfig
from services import register_domain_services
app = create_base_app(
"cart",
context_fn=cart_context,
before_request_fns=[_load_cart],
domain_services_fn=register_domain_services,
)
# App-specific templates override shared templates
@@ -116,12 +117,8 @@ def create_app() -> "Quart":
slug = getattr(g, "page_slug", None)
if not slug:
return
post = (
await g.s.execute(
select(Post).where(Post.slug == slug, Post.is_page == True) # noqa: E712
)
).scalar_one_or_none()
if not post:
post = await services.blog.get_post_by_slug(g.s, slug)
if not post or not post.is_page:
abort(404)
g.page_post = post
g.page_config = (

View File

@@ -12,10 +12,9 @@ from sqlalchemy.orm import selectinload
from shared.models.market import CartItem
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.infrastructure.cart_identity import current_cart_identity
from shared.services.registry import services
def register() -> Blueprint:
@@ -38,12 +37,8 @@ def register() -> Blueprint:
page_slug = request.args.get("page_slug")
page_post_id = None
if page_slug:
post = (
await g.s.execute(
select(Post).where(Post.slug == page_slug, Post.is_page == True) # noqa: E712
)
).scalar_one_or_none()
if post:
post = await services.blog.get_post_by_slug(g.s, page_slug)
if post and post.is_page:
page_post_id = post.id
# --- product cart ---
@@ -73,26 +68,19 @@ def register() -> Blueprint:
if ci.product and (ci.product.special_price or ci.product.regular_price)
)
# --- calendar entries ---
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"])
# --- calendar entries via service ---
if page_post_id is not None:
cal_ids = select(Calendar.id).where(
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))
cal_result = await g.s.execute(cal_q)
cal_entries = cal_result.scalars().all()
cal_entries = await services.calendar.entries_for_page(
g.s, page_post_id,
user_id=ident["user_id"],
session_id=ident["session_id"],
)
else:
cal_entries = await services.calendar.pending_entries(
g.s,
user_id=ident["user_id"],
session_id=ident["session_id"],
)
calendar_count = len(cal_entries)
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 shared.models.order import Order
from shared.models.ghost_content import Post
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 (
current_cart_identity,
get_cart,
@@ -182,7 +181,7 @@ def register(url_prefix: str) -> Blueprint:
# Resolve page/market slugs so product links render correctly
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:
g.page_slug = post.slug
result = await g.s.execute(
@@ -204,7 +203,7 @@ def register(url_prefix: str) -> Blueprint:
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()
html = await render_template(

View File

@@ -1,38 +1,20 @@
from __future__ import annotations
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from shared.models.calendars import CalendarEntry
from shared.services.registry import services
from .identity import current_cart_identity
async def get_calendar_cart_entries(session):
"""
Return all *pending* calendar entries for the current cart identity
(user or anonymous session).
Return all *pending* calendar entries (as CalendarEntryDTOs) for the
current cart identity (user or anonymous session).
"""
ident = current_cart_identity()
filters = [
CalendarEntry.deleted_at.is_(None),
CalendarEntry.state == "pending",
]
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 await services.calendar.pending_entries(
session,
user_id=ident["user_id"],
session_id=ident["session_id"],
)
return result.scalars().all()
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.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):
@@ -13,7 +13,7 @@ async def check_sumup_status(session, order):
if sumup_status == "PAID":
if 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
)
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.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.market_place import MarketPlace
from shared.config import config
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(
@@ -150,8 +150,8 @@ async def create_order_from_cart(
)
session.add(oi)
# Mark pending calendar entries as "ordered" via glue service
await claim_entries_for_order(
# Mark pending calendar entries as "ordered" via calendar service
await services.calendar.claim_entries_for_order(
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_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.services.registry import services
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()
async def get_calendar_entries_for_page(session, post_id: int) -> list[CalendarEntry]:
"""Return pending calendar entries scoped to a specific page (via Calendar.container_id)."""
async def get_calendar_entries_for_page(session, post_id: int):
"""Return pending calendar entries (DTOs) scoped to a specific page."""
ident = current_cart_identity()
filters = [
CalendarEntry.deleted_at.is_(None),
CalendarEntry.state == "pending",
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 await services.calendar.entries_for_page(
session, post_id,
user_id=ident["user_id"],
session_id=ident["session_id"],
)
return result.scalars().all()
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]["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]
posts_by_id: dict[int, Post] = {}
posts_by_id: dict[int, object] = {}
configs_by_post: dict[int, PageConfig] = {}
if post_ids:
post_result = await session.execute(
select(Post).where(Post.id.in_(post_ids))
)
for p in post_result.scalars().all():
for p in await services.blog.get_posts_by_ids(session, post_ids):
posts_by_id[p.id] = p
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