"""Order service — order number generation and business logic.""" from datetime import datetime, timezone from sqlalchemy.ext.asyncio import AsyncSession from sqlalchemy import select, func, create_engine, update as sql_update from sqlalchemy.orm import Session from app.domains.orders.models import Order, OrderLine, OrderStatus import logging logger = logging.getLogger(__name__) def _utcnow_naive() -> datetime: """Return UTC as a naive datetime for legacy TIMESTAMP WITHOUT TIME ZONE columns.""" return datetime.now(timezone.utc).replace(tzinfo=None) async def generate_order_number(db: AsyncSession) -> str: """Generate next sequential order number: SA-2026-XXXXX.""" year = datetime.now(timezone.utc).year prefix = f"SA-{year}-" # Use MAX to find the highest existing sequence number this year. # COUNT-based approach breaks when orders are deleted (produces duplicates). result = await db.execute( select(func.max(Order.order_number)).where(Order.order_number.like(f"{prefix}%")) ) max_num = result.scalar() if max_num: last_seq = int(max_num.split("-")[-1]) return f"{prefix}{last_seq + 1:05d}" return f"{prefix}00001" def check_order_completion(order_id: str) -> bool: """If all renderable lines are done, auto-advance order to completed. Called from Celery tasks (sync context). Returns True if the order was advanced to completed. """ from app.config import settings as app_settings sync_url = app_settings.database_url.replace("+asyncpg", "") engine = create_engine(sync_url) try: with Session(engine) as session: # Get all lines that have an output type (i.e. renderable) lines = session.execute( select(OrderLine).where( OrderLine.order_id == order_id, OrderLine.output_type_id.isnot(None), ) ).scalars().all() if not lines: return False # Check if all renderable lines are in a terminal state all_terminal = all( line.render_status in ("completed", "failed", "cancelled") for line in lines ) if not all_terminal: return False # Check order is still in processing state order = session.execute( select(Order).where(Order.id == order_id) ).scalar_one_or_none() if order is None or order.status != OrderStatus.processing: return False # Auto-advance to completed now = _utcnow_naive() session.execute( sql_update(Order) .where(Order.id == order_id) .values( status=OrderStatus.completed, completed_at=now, updated_at=now, ) ) session.commit() logger.info(f"Order {order_id} auto-advanced to completed (all {len(lines)} lines done)") # Emit a single batch notification summarising all render results try: from app.domains.notifications.service import emit_batch_render_notification_sync emit_batch_render_notification_sync(order_id) except Exception: logger.exception("Failed to emit batch render notification for order %s", order_id) return True finally: engine.dispose()