Decouple PageConfig cross-domain queries + merge cart into db_market

PageConfig (db_blog) decoupling:
- Blog: add page-config, page-config-by-id, page-configs-batch data endpoints
- Blog: add update-page-config action endpoint for events payment admin
- Cart: hydrate_page, resolve_page_config, get_cart_grouped_by_page all
  fetch PageConfig from blog via HTTP instead of direct DB query
- Cart: check_sumup_status auto-fetches page_config from blog when needed
- Events: payment routes read/write PageConfig via blog HTTP endpoints
- Order model: remove cross-domain page_config ORM relationship (keep column)

Cart + Market DB merge:
- Cart tables (cart_items, orders, order_items) moved into db_market
- Cart app DATABASE_URL now points to db_market (same bounded context)
- CartItem.product / CartItem.market_place relationships work again
  (same database, no cross-domain join issues)
- Updated split-databases.sh, init-databases.sql, docker-compose.yml

Ghost sync fix:
- Wrap PostAuthor/PostTag delete+re-add in no_autoflush block
- Use synchronize_session="fetch" to keep identity map consistent
- Prevents query-invoked autoflush IntegrityError on composite PK

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
giles
2026-02-25 11:59:35 +00:00
parent 3be287532d
commit 3053cb321d
15 changed files with 270 additions and 96 deletions

View File

@@ -4,9 +4,10 @@ import path_setup # noqa: F401 # adds shared/ to sys.path
from decimal import Decimal
from pathlib import Path
from types import SimpleNamespace
from quart import g, abort, request
from jinja2 import FileSystemLoader, ChoiceLoader
from sqlalchemy import select
from shared.infrastructure.factory import create_base_app
@@ -114,8 +115,12 @@ async def cart_context() -> dict:
return ctx
def _make_page_config(raw: dict) -> SimpleNamespace:
"""Convert a page-config JSON dict to a namespace for SumUp helpers."""
return SimpleNamespace(**raw)
def create_app() -> "Quart":
from shared.models.page_config import PageConfig
from shared.services.registry import services
from services import register_domain_services
@@ -168,14 +173,12 @@ def create_app() -> "Quart":
if not post or not post.is_page:
abort(404)
g.page_post = post
g.page_config = (
await g.s.execute(
select(PageConfig).where(
PageConfig.container_type == "page",
PageConfig.container_id == post.id,
)
)
).scalar_one_or_none()
raw_pc = await fetch_data(
"blog", "page-config",
params={"container_type": "page", "container_id": post.id},
required=False,
)
g.page_config = _make_page_config(raw_pc) if raw_pc else None
# --- Blueprint registration ---
# Static prefixes first, dynamic (page_slug) last
@@ -211,10 +214,10 @@ def create_app() -> "Quart":
"""
import logging
from datetime import datetime, timezone, timedelta
from sqlalchemy import select
from sqlalchemy.orm import selectinload
from sqlalchemy import select as sel
from shared.db.session import get_session
from shared.models.order import Order
from shared.infrastructure.data_client import fetch_data
from bp.cart.services.check_sumup_status import check_sumup_status
log = logging.getLogger("cart.reconcile")
@@ -222,17 +225,14 @@ def create_app() -> "Quart":
try:
async with get_session() as sess:
async with sess.begin():
# Orders that are pending, have a SumUp checkout, and are
# older than 2 minutes (avoid racing with in-flight checkouts)
cutoff = datetime.now(timezone.utc) - timedelta(minutes=2)
result = await sess.execute(
select(Order)
sel(Order)
.where(
Order.status == "pending",
Order.sumup_checkout_id.isnot(None),
Order.created_at < cutoff,
)
.options(selectinload(Order.page_config))
.limit(50)
)
stale_orders = result.scalars().all()
@@ -243,7 +243,17 @@ def create_app() -> "Quart":
log.info("Reconciling %d stale pending orders", len(stale_orders))
for order in stale_orders:
try:
await check_sumup_status(sess, order)
# Fetch page_config from blog if order has one
pc = None
if order.page_config_id:
raw_pc = await fetch_data(
"blog", "page-config-by-id",
params={"id": order.page_config_id},
required=False,
)
if raw_pc:
pc = _make_page_config(raw_pc)
await check_sumup_status(sess, order, page_config=pc)
log.info(
"Order %d reconciled: %s",
order.id, order.status,