From 049b35479bc43a1feabd6ca0165f62f8781dd5fa Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 19 Feb 2026 04:30:17 +0000 Subject: [PATCH] Domain isolation: replace cross-domain imports with service calls MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- app.py | 13 +++----- bp/cart/api.py | 42 +++++++++----------------- bp/cart/global_routes.py | 7 ++--- bp/cart/services/calendar_cart.py | 32 +++++--------------- bp/cart/services/check_sumup_status.py | 4 +-- bp/cart/services/checkout.py | 8 ++--- bp/cart/services/page_cart.py | 40 ++++++------------------ services/__init__.py | 25 +++++++++++++++ shared | 2 +- 9 files changed, 72 insertions(+), 101 deletions(-) create mode 100644 services/__init__.py diff --git a/app.py b/app.py index 19cbd8a..b66ee79 100644 --- a/app.py +++ b/app.py @@ -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 = ( diff --git a/bp/cart/api.py b/bp/cart/api.py index 15bfe5b..22110f8 100644 --- a/bp/cart/api.py +++ b/bp/cart/api.py @@ -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) diff --git a/bp/cart/global_routes.py b/bp/cart/global_routes.py index 47481e8..d9fc6ca 100644 --- a/bp/cart/global_routes.py +++ b/bp/cart/global_routes.py @@ -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( diff --git a/bp/cart/services/calendar_cart.py b/bp/cart/services/calendar_cart.py index 434cbbd..57960cc 100644 --- a/bp/cart/services/calendar_cart.py +++ b/bp/cart/services/calendar_cart.py @@ -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: diff --git a/bp/cart/services/check_sumup_status.py b/bp/cart/services/check_sumup_status.py index 7973e17..fec2a2c 100644 --- a/bp/cart/services/check_sumup_status.py +++ b/bp/cart/services/check_sumup_status.py @@ -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, { diff --git a/bp/cart/services/checkout.py b/bp/cart/services/checkout.py index 3d83884..85c7cc1 100644 --- a/bp/cart/services/checkout.py +++ b/bp/cart/services/checkout.py @@ -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 ) diff --git a/bp/cart/services/page_cart.py b/bp/cart/services/page_cart.py index 454865c..eff814f 100644 --- a/bp/cart/services/page_cart.py +++ b/bp/cart/services/page_cart.py @@ -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( diff --git a/services/__init__.py b/services/__init__.py new file mode 100644 index 0000000..94c9535 --- /dev/null +++ b/services/__init__.py @@ -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() diff --git a/shared b/shared index ea7dc97..70b1c7d 160000 --- a/shared +++ b/shared @@ -1 +1 @@ -Subproject commit ea7dc9723a8f86b82b42ee9631fca096d0ec11bb +Subproject commit 70b1c7de1007f1759a20a5d17eaab83f96d26b72