Decouple cart/market DBs: denormalize product data, AP internal inbox, OAuth scraper auth

Remove cross-DB relationships (CartItem.product, CartItem.market_place,
OrderItem.product) that break with per-service databases. Denormalize
product and marketplace fields onto cart_items/order_items at write time.

- Add AP internal inbox infrastructure (shared/infrastructure/internal_inbox*)
  for synchronous inter-service writes via HMAC-authenticated POST
- Cart inbox blueprint handles Add/Remove/Update rose:CartItem activities
- Market app sends AP activities to cart inbox instead of writing CartItem directly
- Cart services use denormalized columns instead of cross-DB hydration/joins
- Add marketplaces-by-ids data endpoint to market service
- Alembic migration adds denormalized columns to cart_items and order_items
- Add OAuth device flow auth to market scraper persist_api (artdag client pattern)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-26 14:49:04 +00:00
parent cf7fbd8e9b
commit 81112c716b
28 changed files with 739 additions and 186 deletions

View File

@@ -410,19 +410,18 @@ class CartItem(Base):
nullable=True,
)
# Cross-domain relationships — explicit join, viewonly (no FK constraint)
market_place: Mapped["MarketPlace | None"] = relationship(
"MarketPlace",
primaryjoin="CartItem.market_place_id == MarketPlace.id",
foreign_keys="[CartItem.market_place_id]",
viewonly=True,
)
product: Mapped["Product"] = relationship(
"Product",
primaryjoin="CartItem.product_id == Product.id",
foreign_keys="[CartItem.product_id]",
viewonly=True,
)
# Denormalized product data (snapshotted at write time)
product_title: Mapped[str | None] = mapped_column(String(512), nullable=True)
product_slug: Mapped[str | None] = mapped_column(String(512), nullable=True)
product_image: Mapped[str | None] = mapped_column(Text, nullable=True)
product_brand: Mapped[str | None] = mapped_column(String(255), nullable=True)
product_regular_price: Mapped[float | None] = mapped_column(Numeric(12, 2), nullable=True)
product_special_price: Mapped[float | None] = mapped_column(Numeric(12, 2), nullable=True)
product_price_currency: Mapped[str | None] = mapped_column(String(16), nullable=True)
# Denormalized marketplace data (snapshotted at write time)
market_place_name: Mapped[str | None] = mapped_column(String(255), nullable=True)
market_place_container_id: Mapped[int | None] = mapped_column(Integer, nullable=True)
__table_args__ = (
Index("ix_cart_items_user_product", "user_id", "product_id"),

View File

@@ -87,6 +87,8 @@ class OrderItem(Base):
nullable=False,
)
product_title: Mapped[Optional[str]] = mapped_column(String(512), nullable=True)
product_slug: Mapped[Optional[str]] = mapped_column(String(512), nullable=True)
product_image: Mapped[Optional[str]] = mapped_column(Text, nullable=True)
quantity: Mapped[int] = mapped_column(Integer, nullable=False, default=1)
unit_price: Mapped[float] = mapped_column(Numeric(12, 2), nullable=False)
@@ -102,12 +104,3 @@ class OrderItem(Base):
"Order",
back_populates="items",
)
# Cross-domain relationship — explicit join, viewonly (no FK constraint)
product: Mapped["Product"] = relationship(
"Product",
primaryjoin="OrderItem.product_id == Product.id",
foreign_keys="[OrderItem.product_id]",
viewonly=True,
lazy="selectin",
)