From fc14d8323adcb404894c8ab875431c9d571cfabb Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 14 Feb 2026 19:18:01 +0000 Subject: [PATCH] Add README documenting glue layer architecture Covers models, services, handlers, event flows, and guidelines for adding new cross-domain logic. Co-Authored-By: Claude Opus 4.6 --- README.md | 122 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 122 insertions(+) create mode 100644 README.md diff --git a/README.md b/README.md new file mode 100644 index 0000000..a75df28 --- /dev/null +++ b/README.md @@ -0,0 +1,122 @@ +# 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.