Phase 5: Replace cross-domain writes with glue services, emit events
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 41s

- checkout.py: use claim_entries_for_order(), emit order.created
- check_sumup_status.py: use confirm_entries_for_order(), emit order.paid
- global_routes.py: use get_entries_for_order() instead of relationship
- order.py: remove calendar_entries relationship
- api.py: remove /adopt endpoint (replaced by event-driven adoption)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-14 17:35:43 +00:00
parent cd332b2544
commit d407957928
5 changed files with 23 additions and 104 deletions

View File

@@ -7,7 +7,7 @@ They are CSRF-exempt because they are server-to-server calls.
from __future__ import annotations
from quart import Blueprint, g, request, jsonify
from sqlalchemy import select, update, func
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from market.models.market import CartItem
@@ -120,57 +120,4 @@ def register() -> Blueprint:
}
)
@bp.post("/adopt")
@csrf_exempt
async def adopt():
"""
Adopt anonymous cart items + calendar entries for a user.
Called by the coop app after successful login.
Body: {"user_id": int, "session_id": str}
"""
data = await request.get_json() or {}
user_id = data.get("user_id")
session_id = data.get("session_id")
if not user_id or not session_id:
return jsonify({"ok": False, "error": "user_id and session_id required"}), 400
# --- adopt cart items ---
anon_result = await g.s.execute(
select(CartItem).where(
CartItem.deleted_at.is_(None),
CartItem.user_id.is_(None),
CartItem.session_id == session_id,
)
)
anon_items = anon_result.scalars().all()
if anon_items:
# Soft-delete existing user cart
await g.s.execute(
update(CartItem)
.where(CartItem.deleted_at.is_(None), CartItem.user_id == user_id)
.values(deleted_at=func.now())
)
for ci in anon_items:
ci.user_id = user_id
# --- adopt calendar entries ---
await g.s.execute(
update(CalendarEntry)
.where(CalendarEntry.deleted_at.is_(None), CalendarEntry.user_id == user_id)
.values(deleted_at=func.now())
)
cal_result = await g.s.execute(
select(CalendarEntry).where(
CalendarEntry.deleted_at.is_(None),
CalendarEntry.session_id == session_id,
)
)
for entry in cal_result.scalars().all():
entry.user_id = user_id
return jsonify({"ok": True})
return bp

View File

@@ -7,6 +7,7 @@ from sqlalchemy import select
from models.order import Order
from shared.browser.app.utils.htmx import is_htmx_request
from glue.services.order_lifecycle import get_entries_for_order
from .services import (
current_cart_identity,
get_cart,
@@ -187,7 +188,7 @@ def register(url_prefix: str) -> Blueprint:
except Exception:
status = status or "pending"
calendar_entries = order.calendar_entries or []
calendar_entries = await get_entries_for_order(g.s, order.id)
await g.s.flush()
html = await render_template(

View File

@@ -1,6 +1,6 @@
from shared.browser.app.payments.sumup import get_checkout as sumup_get_checkout
from sqlalchemy import update
from events.models.calendars import CalendarEntry
from shared.events import emit_event
from glue.services.order_lifecycle import confirm_entries_for_order
async def check_sumup_status(session, order):
@@ -13,21 +13,13 @@ async def check_sumup_status(session, order):
if sumup_status == "PAID":
if order.status != "paid":
order.status = "paid"
filters = [
CalendarEntry.deleted_at.is_(None),
CalendarEntry.state == "ordered",
CalendarEntry.order_id==order.id,
]
if order.user_id is not None:
filters.append(CalendarEntry.user_id == order.user_id)
elif order.session_id is not None:
filters.append(CalendarEntry.session_id == order.session_id)
await session.execute(
update(CalendarEntry)
.where(*filters)
.values(state="provisional")
await confirm_entries_for_order(
session, order.id, order.user_id, order.session_id
)
await emit_event(session, "order.paid", "order", order.id, {
"order_id": order.id,
"user_id": order.user_id,
})
elif sumup_status == "FAILED":
order.status = "failed"
else:

View File

@@ -3,7 +3,7 @@ from __future__ import annotations
from typing import Optional
from urllib.parse import urlencode
from sqlalchemy import select, update
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from sqlalchemy.orm import selectinload
@@ -13,6 +13,8 @@ from events.models.calendars import CalendarEntry, Calendar
from models.page_config import PageConfig
from market.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
async def find_or_create_cart_item(
@@ -148,34 +150,17 @@ async def create_order_from_cart(
)
session.add(oi)
# Update calendar entries to reference this order
calendar_filters = [
CalendarEntry.deleted_at.is_(None),
CalendarEntry.state == "pending",
]
if order.user_id is not None:
calendar_filters.append(CalendarEntry.user_id == order.user_id)
elif order.session_id is not None:
calendar_filters.append(CalendarEntry.session_id == order.session_id)
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()
calendar_filters.append(CalendarEntry.calendar_id.in_(cal_ids))
await session.execute(
update(CalendarEntry)
.where(*calendar_filters)
.values(
state="ordered",
order_id=order.id,
)
# Mark pending calendar entries as "ordered" via glue service
await claim_entries_for_order(
session, order.id, user_id, session_id, page_post_id
)
await emit_event(session, "order.created", "order", order.id, {
"order_id": order.id,
"user_id": user_id,
"session_id": session_id,
})
return order
@@ -234,7 +219,6 @@ async def get_order_with_details(session: AsyncSession, order_id: int) -> Option
select(Order)
.options(
selectinload(Order.items).selectinload(OrderItem.product),
selectinload(Order.calendar_entries),
)
.where(Order.id == order_id)
)

View File

@@ -69,11 +69,6 @@ class Order(Base):
cascade="all, delete-orphan",
lazy="selectin",
)
calendar_entries: Mapped[List["CalendarEntry"]] = relationship(
"CalendarEntry",
back_populates="order",
lazy="selectin",
)
page_config: Mapped[Optional["PageConfig"]] = relationship(
"PageConfig",
foreign_keys=[page_config_id],