Decouple per-service Alembic migrations and fix cross-DB queries
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m19s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m19s
Each service (blog, market, cart, events, federation, account) now owns its own database schema with independent Alembic migrations. Removes the monolithic shared/alembic/ that ran all migrations against a single DB. - Add per-service alembic.ini, env.py, and 0001_initial.py migrations - Add shared/db/alembic_env.py helper with table-name filtering - Fix cross-DB FK in blog/models/snippet.py (users lives in db_account) - Fix cart_impl.py cross-DB queries: fetch products and market_places via internal data endpoints instead of direct SQL joins - Fix blog ghost_sync to fetch page_configs from cart via data endpoint - Add products-by-ids and page-config-ensure data endpoints - Update all entrypoint.sh to create own DB and run own migrations - Cart now uses db_cart instead of db_market - Add docker-compose.dev.yml, dev.sh for local development - CI deploys both rose-ash swarm stack and rose-ash-dev compose stack - Fix Quart namespace package crash (root_path in factory.py) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -9,27 +9,34 @@ from decimal import Decimal
|
||||
|
||||
from sqlalchemy import select, update, func
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
from sqlalchemy.orm import selectinload
|
||||
|
||||
from shared.models.market import CartItem
|
||||
from shared.models.market_place import MarketPlace
|
||||
from shared.contracts.dtos import CartItemDTO, CartSummaryDTO
|
||||
|
||||
|
||||
def _item_to_dto(ci: CartItem) -> CartItemDTO:
|
||||
product = ci.product
|
||||
def _item_to_dto(ci: CartItem, product: dict | None) -> CartItemDTO:
|
||||
return CartItemDTO(
|
||||
id=ci.id,
|
||||
product_id=ci.product_id,
|
||||
quantity=ci.quantity,
|
||||
product_title=product.title if product else None,
|
||||
product_slug=product.slug if product else None,
|
||||
product_image=product.image if product else None,
|
||||
unit_price=Decimal(str(product.special_price or product.regular_price or 0)) if product else None,
|
||||
product_title=product["title"] if product else None,
|
||||
product_slug=product["slug"] if product else None,
|
||||
product_image=product["image"] if product else None,
|
||||
unit_price=Decimal(str(product.get("special_price") or product.get("regular_price") or 0)) if product else None,
|
||||
market_place_id=ci.market_place_id,
|
||||
)
|
||||
|
||||
|
||||
async def _fetch_products_map(fetch_data, product_ids: list[int]) -> dict[int, dict]:
|
||||
"""Fetch product details from market service, return {id: product_dict}."""
|
||||
if not product_ids:
|
||||
return {}
|
||||
raw = await fetch_data("market", "products-by-ids",
|
||||
params={"ids": ",".join(str(i) for i in product_ids)},
|
||||
required=False) or []
|
||||
return {p["id"]: p for p in raw}
|
||||
|
||||
|
||||
class SqlCartService:
|
||||
|
||||
async def cart_summary(
|
||||
@@ -59,24 +66,31 @@ class SqlCartService:
|
||||
return CartSummaryDTO()
|
||||
|
||||
if page_post_id is not None:
|
||||
mp_ids = select(MarketPlace.id).where(
|
||||
MarketPlace.container_type == "page",
|
||||
MarketPlace.container_id == page_post_id,
|
||||
MarketPlace.deleted_at.is_(None),
|
||||
).scalar_subquery()
|
||||
cart_q = cart_q.where(CartItem.market_place_id.in_(mp_ids))
|
||||
mps = await fetch_data("market", "marketplaces-for-container",
|
||||
params={"type": "page", "id": page_post_id},
|
||||
required=False) or []
|
||||
mp_ids = [mp["id"] for mp in mps]
|
||||
if mp_ids:
|
||||
cart_q = cart_q.where(CartItem.market_place_id.in_(mp_ids))
|
||||
else:
|
||||
return CartSummaryDTO()
|
||||
|
||||
cart_q = cart_q.options(selectinload(CartItem.product))
|
||||
result = await session.execute(cart_q)
|
||||
cart_items = result.scalars().all()
|
||||
|
||||
count = sum(ci.quantity for ci in cart_items)
|
||||
total = sum(
|
||||
Decimal(str(ci.product.special_price or ci.product.regular_price or 0)) * ci.quantity
|
||||
for ci in cart_items
|
||||
if ci.product and (ci.product.special_price or ci.product.regular_price)
|
||||
products = await _fetch_products_map(
|
||||
fetch_data, list({ci.product_id for ci in cart_items}),
|
||||
)
|
||||
|
||||
count = sum(ci.quantity for ci in cart_items)
|
||||
total = Decimal("0")
|
||||
for ci in cart_items:
|
||||
p = products.get(ci.product_id)
|
||||
if p:
|
||||
price = p.get("special_price") or p.get("regular_price")
|
||||
if price:
|
||||
total += Decimal(str(price)) * ci.quantity
|
||||
|
||||
# --- calendar entries via events data endpoint ---
|
||||
cal_params: dict = {}
|
||||
if user_id is not None:
|
||||
@@ -109,7 +123,7 @@ class SqlCartService:
|
||||
ticket_count = len(tickets)
|
||||
ticket_total = sum(Decimal(str(t.price or 0)) for t in tickets)
|
||||
|
||||
items = [_item_to_dto(ci) for ci in cart_items]
|
||||
items = [_item_to_dto(ci, products.get(ci.product_id)) for ci in cart_items]
|
||||
|
||||
return CartSummaryDTO(
|
||||
count=count,
|
||||
@@ -125,6 +139,8 @@ class SqlCartService:
|
||||
self, session: AsyncSession, *,
|
||||
user_id: int | None, session_id: str | None,
|
||||
) -> list[CartItemDTO]:
|
||||
from shared.infrastructure.data_client import fetch_data
|
||||
|
||||
cart_q = select(CartItem).where(CartItem.deleted_at.is_(None))
|
||||
if user_id is not None:
|
||||
cart_q = cart_q.where(CartItem.user_id == user_id)
|
||||
@@ -133,9 +149,14 @@ class SqlCartService:
|
||||
else:
|
||||
return []
|
||||
|
||||
cart_q = cart_q.options(selectinload(CartItem.product)).order_by(CartItem.created_at.desc())
|
||||
cart_q = cart_q.order_by(CartItem.created_at.desc())
|
||||
result = await session.execute(cart_q)
|
||||
return [_item_to_dto(ci) for ci in result.scalars().all()]
|
||||
items = result.scalars().all()
|
||||
|
||||
products = await _fetch_products_map(
|
||||
fetch_data, list({ci.product_id for ci in items}),
|
||||
)
|
||||
return [_item_to_dto(ci, products.get(ci.product_id)) for ci in items]
|
||||
|
||||
async def adopt_cart_for_user(
|
||||
self, session: AsyncSession, user_id: int, session_id: str,
|
||||
|
||||
Reference in New Issue
Block a user