# Glue Layer The glue layer sits between the four domain apps (blog, market, cart, events) and encapsulates all **cross-domain logic** — the code that would otherwise create tight coupling between services. Each app includes `glue/` as a git submodule. All apps share one PostgreSQL database, so glue services operate within the caller's existing DB transaction. ## Architecture ``` blog ──┐ market ─┤ cart ───┤──→ glue (services, handlers, models) events ─┘ ``` **Rule:** Apps never import models or write data belonging to another domain directly. Instead they call a glue service or emit a domain event. ## Models ### ContainerRelation (`models/container_relation.py`) Generic parent/child relationship between any two entities. - `parent_type + parent_id` → `child_type + child_id` - Soft-deletable (`deleted_at`), ordered (`sort_order`) - Unique constraint on (parent_type, parent_id, child_type, child_id) Used to attach calendars to pages, markets to pages, etc. without FK coupling. ### MenuNode (`models/menu_node.py`) Navigation tree nodes, scoped to a container (container_type + container_id). - Self-referential (`parent_id`), ordered (`sort_order`), with `depth` - Stores `label`, `slug`, `href`, `icon`, `feature_image` ## Services ### `services/relationships.py` — Container Relations | Function | Description | |----------|-------------| | `attach_child()` | Create or revive a ContainerRelation (upsert). Emits `container.child_attached`. | | `get_children()` | Query children of a container, optionally filtered by child_type. | | `detach_child()` | Soft-delete a ContainerRelation. Emits `container.child_detached`. | ### `services/order_lifecycle.py` — Order ↔ Calendar Bridging Cross-domain writes between **cart** and **events** domains. These run in the same transaction as the caller (not event-driven) because order creation and payment require immediate consistency. | Function | Description | |----------|-------------| | `claim_entries_for_order()` | Mark pending CalendarEntries as "ordered" and set order_id. Called from checkout. | | `confirm_entries_for_order()` | Mark ordered CalendarEntries as "provisional". Called when payment confirms. | | `get_entries_for_order()` | Return CalendarEntries for an order. Replaces the old `Order.calendar_entries` relationship. | ### `services/cart_adoption.py` — Login Adoption | Function | Description | |----------|-------------| | `adopt_session_for_user()` | Adopt anonymous CartItems + CalendarEntries for a logged-in user. Soft-deletes any existing user cart/entries first. | ### `services/navigation.py` — Navigation | Function | Description | |----------|-------------| | `get_navigation_tree()` | Return top-level MenuNodes ordered by sort_order. | | `rebuild_navigation()` | Placeholder for syncing ContainerRelations → MenuNodes. | ## Event Handlers Handlers are registered at import time via `shared.events.register_handler()`. They're imported in `setup.py` → `register_glue_handlers()`, which each app calls at startup. ### `handlers/container_handlers.py` | Event | Handler | |-------|---------| | `container.child_attached` | Triggers `rebuild_navigation()` | | `container.child_detached` | Triggers `rebuild_navigation()` | ### `handlers/login_handlers.py` | Event | Handler | |-------|---------| | `user.logged_in` | Calls `adopt_session_for_user()` to adopt anonymous cart/entries | ### `handlers/order_handlers.py` | Event | Handler | |-------|---------| | `order.created` | Logging (placeholder for future workflows) | | `order.paid` | Logging (placeholder for future workflows) | ## Event Flow Diagrams ### Checkout ``` cart/checkout.py ├─ create order + order items (cart domain) ├─ glue: claim_entries_for_order() ← cross-domain write (same txn) └─ emit: order.created ← observability ``` ### Payment Confirmation ``` cart/check_sumup_status.py ├─ update order.status = "paid" (cart domain) ├─ glue: confirm_entries_for_order() ← cross-domain write (same txn) └─ emit: order.paid ← observability ``` ### Login Adoption ``` blog/auth/routes.py └─ emit: user.logged_in ← event (in last_login_at txn) ↓ (EventProcessor) glue/handlers/login_handlers.py └─ glue: adopt_session_for_user() ← cross-domain write (handler txn) ``` ## Remaining Cross-Domain Reads (Pragmatic Exceptions) These stay in the app code (not in glue) because they're read-only queries against the shared DB. Moving them to glue would be pure churn: - `cart/bp/cart/services/checkout.py` → `resolve_page_config()` reads Calendar + MarketPlace - `cart/bp/cart/api.py` → `/summary` reads CalendarEntry + Calendar + MarketPlace - `cart/bp/cart/` → calendar_cart.py, page_cart.py read CalendarEntry/Calendar for display ## Adding New Cross-Domain Logic 1. **Cross-domain write?** → Add a function in `glue/services/`. Call it from the app code within the existing transaction. 2. **Eventually consistent?** → Emit a domain event from the originating app. Add a handler in `glue/handlers/`. 3. **Cross-domain read?** → If it's a simple query against the shared DB, keep it in the app. Only move to glue if multiple apps need the same query.