Split cart into 4 microservices: relations, likes, orders, page-config→blog
Phase 1 - Relations service (internal): owns ContainerRelation, exposes get-children data + attach/detach-child actions. Retargeted events, blog, market callers from cart to relations. Phase 2 - Likes service (internal): unified Like model replaces ProductLike and PostLike with generic target_type/target_slug/target_id. Exposes is-liked, liked-slugs, liked-ids data + toggle action. Phase 3 - PageConfig → blog: moved ownership to blog with direct DB queries, removed proxy endpoints from cart. Phase 4 - Orders service (public): owns Order/OrderItem + SumUp checkout flow. Cart checkout now delegates to orders via create-order action. Webhook/return routes and reconciliation moved to orders. Phase 5 - Infrastructure: docker-compose, deploy.sh, Dockerfiles updated for all 3 new services. Added orders_url helper and factory model imports. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
129
orders/app.py
Normal file
129
orders/app.py
Normal file
@@ -0,0 +1,129 @@
|
||||
from __future__ import annotations
|
||||
import path_setup # noqa: F401 # adds shared/ to sys.path
|
||||
|
||||
from pathlib import Path
|
||||
from types import SimpleNamespace
|
||||
|
||||
from quart import g, abort, request
|
||||
from jinja2 import FileSystemLoader, ChoiceLoader
|
||||
|
||||
from shared.infrastructure.factory import create_base_app
|
||||
|
||||
from bp import (
|
||||
register_orders,
|
||||
register_order,
|
||||
register_checkout,
|
||||
register_fragments,
|
||||
register_actions,
|
||||
register_data,
|
||||
)
|
||||
|
||||
|
||||
async def orders_context() -> dict:
|
||||
"""Orders app context processor."""
|
||||
from shared.infrastructure.context import base_context
|
||||
from shared.infrastructure.cart_identity import current_cart_identity
|
||||
from shared.infrastructure.fragments import fetch_fragments
|
||||
|
||||
ctx = await base_context()
|
||||
ctx["menu_items"] = []
|
||||
|
||||
user = getattr(g, "user", None)
|
||||
ident = current_cart_identity()
|
||||
cart_params = {}
|
||||
if ident["user_id"] is not None:
|
||||
cart_params["user_id"] = ident["user_id"]
|
||||
if ident["session_id"] is not None:
|
||||
cart_params["session_id"] = ident["session_id"]
|
||||
|
||||
cart_mini_html, auth_menu_html, nav_tree_html = await fetch_fragments([
|
||||
("cart", "cart-mini", cart_params or None),
|
||||
("account", "auth-menu", {"email": user.email} if user else None),
|
||||
("blog", "nav-tree", {"app_name": "orders", "path": request.path}),
|
||||
])
|
||||
ctx["cart_mini_html"] = cart_mini_html
|
||||
ctx["auth_menu_html"] = auth_menu_html
|
||||
ctx["nav_tree_html"] = nav_tree_html
|
||||
|
||||
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 services import register_domain_services
|
||||
|
||||
app = create_base_app(
|
||||
"orders",
|
||||
context_fn=orders_context,
|
||||
domain_services_fn=register_domain_services,
|
||||
)
|
||||
|
||||
# App-specific templates override shared templates
|
||||
app_templates = str(Path(__file__).resolve().parent / "templates")
|
||||
app.jinja_loader = ChoiceLoader([
|
||||
FileSystemLoader(app_templates),
|
||||
app.jinja_loader,
|
||||
])
|
||||
|
||||
app.register_blueprint(register_fragments())
|
||||
app.register_blueprint(register_actions())
|
||||
app.register_blueprint(register_data())
|
||||
|
||||
# Orders list at /
|
||||
app.register_blueprint(register_orders(url_prefix="/"))
|
||||
|
||||
# Checkout webhook + return
|
||||
app.register_blueprint(register_checkout())
|
||||
|
||||
# --- Reconcile stale pending orders on startup ---
|
||||
@app.before_serving
|
||||
async def _reconcile_pending_orders():
|
||||
"""Check SumUp status for orders stuck in 'pending' with a checkout ID."""
|
||||
import logging
|
||||
from datetime import datetime, timezone, timedelta
|
||||
from sqlalchemy import select as sel
|
||||
from shared.db.session import get_session
|
||||
from shared.models.order import Order
|
||||
from services.check_sumup_status import check_sumup_status
|
||||
|
||||
log = logging.getLogger("orders.reconcile")
|
||||
|
||||
try:
|
||||
async with get_session() as sess:
|
||||
async with sess.begin():
|
||||
cutoff = datetime.now(timezone.utc) - timedelta(minutes=2)
|
||||
result = await sess.execute(
|
||||
sel(Order)
|
||||
.where(
|
||||
Order.status == "pending",
|
||||
Order.sumup_checkout_id.isnot(None),
|
||||
Order.created_at < cutoff,
|
||||
)
|
||||
.limit(50)
|
||||
)
|
||||
stale_orders = result.scalars().all()
|
||||
|
||||
if not stale_orders:
|
||||
return
|
||||
|
||||
log.info("Reconciling %d stale pending orders", len(stale_orders))
|
||||
for order in stale_orders:
|
||||
try:
|
||||
await check_sumup_status(sess, order)
|
||||
log.info(
|
||||
"Order %d reconciled: %s",
|
||||
order.id, order.status,
|
||||
)
|
||||
except Exception:
|
||||
log.exception("Failed to reconcile order %d", order.id)
|
||||
except Exception:
|
||||
log.exception("Order reconciliation failed")
|
||||
|
||||
return app
|
||||
|
||||
|
||||
app = create_app()
|
||||
Reference in New Issue
Block a user