Replace every direct cross-app services.* call with HTTP-based communication: call_action() for writes, fetch_data() for reads. Each app now registers only its own domain service. Infrastructure: - shared/infrastructure/actions.py — POST client for /internal/actions/ - shared/infrastructure/data_client.py — GET client for /internal/data/ - shared/contracts/dtos.py — dto_to_dict/dto_from_dict serialization Action endpoints (writes): - events: 8 handlers (ticket adjust, claim/confirm, toggle, adopt) - market: 2 handlers (create/soft-delete marketplace) - cart: 1 handler (adopt cart for user) Data endpoints (reads): - blog: 4 (post-by-slug/id, posts-by-ids, search-posts) - events: 10 (pending entries/tickets, entries/tickets for page/order, entry-ids, associated-entries, calendars, visible-entries-for-period) - market: 1 (marketplaces-for-container) - cart: 1 (cart-summary) Service registration cleanup: - blog→blog+federation, events→calendar+federation, market→market+federation, cart→cart only, federation→federation only, account→nothing - Stubs reduced to minimal StubFederationService Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
130 lines
3.8 KiB
Python
130 lines
3.8 KiB
Python
from __future__ import annotations
|
|
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
from sqlalchemy import select
|
|
from sqlalchemy.sql import func
|
|
|
|
from models.calendars import CalendarEntry, CalendarEntryPost
|
|
from shared.infrastructure.data_client import fetch_data
|
|
from shared.contracts.dtos import PostDTO, dto_from_dict
|
|
|
|
|
|
async def add_post_to_entry(
|
|
session: AsyncSession,
|
|
entry_id: int,
|
|
post_id: int
|
|
) -> tuple[bool, str | None]:
|
|
"""
|
|
Associate a post with a calendar entry.
|
|
Returns (success, error_message).
|
|
"""
|
|
# Check if entry exists
|
|
entry = await session.scalar(
|
|
select(CalendarEntry).where(
|
|
CalendarEntry.id == entry_id,
|
|
CalendarEntry.deleted_at.is_(None)
|
|
)
|
|
)
|
|
if not entry:
|
|
return False, "Calendar entry not found"
|
|
|
|
# Check if post exists
|
|
raw = await fetch_data("blog", "post-by-id", params={"id": post_id}, required=False)
|
|
if not raw:
|
|
return False, "Post not found"
|
|
|
|
# Check if association already exists
|
|
existing = await session.scalar(
|
|
select(CalendarEntryPost).where(
|
|
CalendarEntryPost.entry_id == entry_id,
|
|
CalendarEntryPost.content_type == "post",
|
|
CalendarEntryPost.content_id == post_id,
|
|
CalendarEntryPost.deleted_at.is_(None)
|
|
)
|
|
)
|
|
|
|
if existing:
|
|
return False, "Post is already associated with this entry"
|
|
|
|
# Create association
|
|
association = CalendarEntryPost(
|
|
entry_id=entry_id,
|
|
content_type="post",
|
|
content_id=post_id
|
|
)
|
|
session.add(association)
|
|
await session.flush()
|
|
|
|
return True, None
|
|
|
|
|
|
async def remove_post_from_entry(
|
|
session: AsyncSession,
|
|
entry_id: int,
|
|
post_id: int
|
|
) -> tuple[bool, str | None]:
|
|
"""
|
|
Remove a post association from a calendar entry (soft delete).
|
|
Returns (success, error_message).
|
|
"""
|
|
# Find the association
|
|
association = await session.scalar(
|
|
select(CalendarEntryPost).where(
|
|
CalendarEntryPost.entry_id == entry_id,
|
|
CalendarEntryPost.content_type == "post",
|
|
CalendarEntryPost.content_id == post_id,
|
|
CalendarEntryPost.deleted_at.is_(None)
|
|
)
|
|
)
|
|
|
|
if not association:
|
|
return False, "Association not found"
|
|
|
|
# Soft delete
|
|
association.deleted_at = func.now()
|
|
await session.flush()
|
|
|
|
return True, None
|
|
|
|
|
|
async def get_entry_posts(
|
|
session: AsyncSession,
|
|
entry_id: int
|
|
) -> list:
|
|
"""
|
|
Get all posts (as PostDTOs) associated with a calendar entry.
|
|
"""
|
|
result = await session.execute(
|
|
select(CalendarEntryPost.content_id).where(
|
|
CalendarEntryPost.entry_id == entry_id,
|
|
CalendarEntryPost.content_type == "post",
|
|
CalendarEntryPost.deleted_at.is_(None),
|
|
)
|
|
)
|
|
post_ids = list(result.scalars().all())
|
|
if not post_ids:
|
|
return []
|
|
raw_posts = await fetch_data("blog", "posts-by-ids",
|
|
params={"ids": ",".join(str(i) for i in post_ids)},
|
|
required=False) or []
|
|
posts = [dto_from_dict(PostDTO, p) for p in raw_posts]
|
|
return sorted(posts, key=lambda p: (p.title or ""))
|
|
|
|
|
|
async def search_posts(
|
|
session: AsyncSession,
|
|
query: str,
|
|
page: int = 1,
|
|
per_page: int = 10
|
|
) -> tuple[list, int]:
|
|
"""
|
|
Search for posts by title with pagination.
|
|
If query is empty, returns all posts in published order.
|
|
Returns (post_dtos, total_count).
|
|
"""
|
|
raw = await fetch_data("blog", "search-posts",
|
|
params={"query": query, "page": page, "per_page": per_page},
|
|
required=False) or {"posts": [], "total": 0}
|
|
posts = [dto_from_dict(PostDTO, p) for p in raw.get("posts", [])]
|
|
return posts, raw.get("total", 0)
|