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
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:
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
)
|
||||
|
||||
@@ -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],
|
||||
|
||||
Reference in New Issue
Block a user