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 __future__ import annotations
|
||||||
|
|
||||||
from quart import Blueprint, g, request, jsonify
|
from quart import Blueprint, g, request, jsonify
|
||||||
from sqlalchemy import select, update, func
|
from sqlalchemy import select
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
from market.models.market import CartItem
|
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
|
return bp
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ from sqlalchemy import select
|
|||||||
|
|
||||||
from models.order import Order
|
from models.order import Order
|
||||||
from shared.browser.app.utils.htmx import is_htmx_request
|
from shared.browser.app.utils.htmx import is_htmx_request
|
||||||
|
from glue.services.order_lifecycle import get_entries_for_order
|
||||||
from .services import (
|
from .services import (
|
||||||
current_cart_identity,
|
current_cart_identity,
|
||||||
get_cart,
|
get_cart,
|
||||||
@@ -187,7 +188,7 @@ def register(url_prefix: str) -> Blueprint:
|
|||||||
except Exception:
|
except Exception:
|
||||||
status = status or "pending"
|
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()
|
await g.s.flush()
|
||||||
|
|
||||||
html = await render_template(
|
html = await render_template(
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from shared.browser.app.payments.sumup import get_checkout as sumup_get_checkout
|
from shared.browser.app.payments.sumup import get_checkout as sumup_get_checkout
|
||||||
from sqlalchemy import update
|
from shared.events import emit_event
|
||||||
from events.models.calendars import CalendarEntry
|
from glue.services.order_lifecycle import confirm_entries_for_order
|
||||||
|
|
||||||
|
|
||||||
async def check_sumup_status(session, order):
|
async def check_sumup_status(session, order):
|
||||||
@@ -13,21 +13,13 @@ async def check_sumup_status(session, order):
|
|||||||
if sumup_status == "PAID":
|
if sumup_status == "PAID":
|
||||||
if order.status != "paid":
|
if order.status != "paid":
|
||||||
order.status = "paid"
|
order.status = "paid"
|
||||||
filters = [
|
await confirm_entries_for_order(
|
||||||
CalendarEntry.deleted_at.is_(None),
|
session, order.id, order.user_id, order.session_id
|
||||||
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 emit_event(session, "order.paid", "order", order.id, {
|
||||||
|
"order_id": order.id,
|
||||||
|
"user_id": order.user_id,
|
||||||
|
})
|
||||||
elif sumup_status == "FAILED":
|
elif sumup_status == "FAILED":
|
||||||
order.status = "failed"
|
order.status = "failed"
|
||||||
else:
|
else:
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ from __future__ import annotations
|
|||||||
from typing import Optional
|
from typing import Optional
|
||||||
from urllib.parse import urlencode
|
from urllib.parse import urlencode
|
||||||
|
|
||||||
from sqlalchemy import select, update
|
from sqlalchemy import select
|
||||||
from sqlalchemy.ext.asyncio import AsyncSession
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
from sqlalchemy.orm import selectinload
|
from sqlalchemy.orm import selectinload
|
||||||
|
|
||||||
@@ -13,6 +13,8 @@ from events.models.calendars import CalendarEntry, Calendar
|
|||||||
from models.page_config import PageConfig
|
from models.page_config import PageConfig
|
||||||
from market.models.market_place import MarketPlace
|
from market.models.market_place import MarketPlace
|
||||||
from shared.config import config
|
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(
|
async def find_or_create_cart_item(
|
||||||
@@ -148,34 +150,17 @@ async def create_order_from_cart(
|
|||||||
)
|
)
|
||||||
session.add(oi)
|
session.add(oi)
|
||||||
|
|
||||||
# Update calendar entries to reference this order
|
# Mark pending calendar entries as "ordered" via glue service
|
||||||
calendar_filters = [
|
await claim_entries_for_order(
|
||||||
CalendarEntry.deleted_at.is_(None),
|
session, order.id, user_id, session_id, page_post_id
|
||||||
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,
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
await emit_event(session, "order.created", "order", order.id, {
|
||||||
|
"order_id": order.id,
|
||||||
|
"user_id": user_id,
|
||||||
|
"session_id": session_id,
|
||||||
|
})
|
||||||
|
|
||||||
return order
|
return order
|
||||||
|
|
||||||
|
|
||||||
@@ -234,7 +219,6 @@ async def get_order_with_details(session: AsyncSession, order_id: int) -> Option
|
|||||||
select(Order)
|
select(Order)
|
||||||
.options(
|
.options(
|
||||||
selectinload(Order.items).selectinload(OrderItem.product),
|
selectinload(Order.items).selectinload(OrderItem.product),
|
||||||
selectinload(Order.calendar_entries),
|
|
||||||
)
|
)
|
||||||
.where(Order.id == order_id)
|
.where(Order.id == order_id)
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -69,11 +69,6 @@ class Order(Base):
|
|||||||
cascade="all, delete-orphan",
|
cascade="all, delete-orphan",
|
||||||
lazy="selectin",
|
lazy="selectin",
|
||||||
)
|
)
|
||||||
calendar_entries: Mapped[List["CalendarEntry"]] = relationship(
|
|
||||||
"CalendarEntry",
|
|
||||||
back_populates="order",
|
|
||||||
lazy="selectin",
|
|
||||||
)
|
|
||||||
page_config: Mapped[Optional["PageConfig"]] = relationship(
|
page_config: Mapped[Optional["PageConfig"]] = relationship(
|
||||||
"PageConfig",
|
"PageConfig",
|
||||||
foreign_keys=[page_config_id],
|
foreign_keys=[page_config_id],
|
||||||
|
|||||||
Reference in New Issue
Block a user