# app/services/letzshop/order_service.py """ Letzshop order service for handling order-related database operations. This service moves database queries out of the API layer to comply with architecture rules (API-002: endpoints should not contain business logic). """ import logging from datetime import UTC, datetime from typing import Any from sqlalchemy import func from sqlalchemy.orm import Session from models.database.letzshop import ( LetzshopFulfillmentQueue, LetzshopOrder, LetzshopSyncLog, VendorLetzshopCredentials, ) from models.database.vendor import Vendor logger = logging.getLogger(__name__) class VendorNotFoundError(Exception): """Raised when a vendor is not found.""" class OrderNotFoundError(Exception): """Raised when a Letzshop order is not found.""" class LetzshopOrderService: """Service for Letzshop order database operations.""" def __init__(self, db: Session): self.db = db # ========================================================================= # Vendor Operations # ========================================================================= def get_vendor(self, vendor_id: int) -> Vendor | None: """Get vendor by ID.""" return self.db.query(Vendor).filter(Vendor.id == vendor_id).first() def get_vendor_or_raise(self, vendor_id: int) -> Vendor: """Get vendor by ID or raise VendorNotFoundError.""" vendor = self.get_vendor(vendor_id) if vendor is None: raise VendorNotFoundError(f"Vendor with ID {vendor_id} not found") return vendor def list_vendors_with_letzshop_status( self, skip: int = 0, limit: int = 100, configured_only: bool = False, ) -> tuple[list[dict[str, Any]], int]: """ List vendors with their Letzshop integration status. Returns a tuple of (vendor_overviews, total_count). """ # Build query query = self.db.query(Vendor).filter(Vendor.is_active == True) # noqa: E712 if configured_only: query = query.join( VendorLetzshopCredentials, Vendor.id == VendorLetzshopCredentials.vendor_id, ) # Get total count total = query.count() # Get vendors vendors = query.order_by(Vendor.name).offset(skip).limit(limit).all() # Build response with Letzshop status vendor_overviews = [] for vendor in vendors: # Get credentials credentials = ( self.db.query(VendorLetzshopCredentials) .filter(VendorLetzshopCredentials.vendor_id == vendor.id) .first() ) # Get order counts pending_orders = 0 total_orders = 0 if credentials: pending_orders = ( self.db.query(func.count(LetzshopOrder.id)) .filter( LetzshopOrder.vendor_id == vendor.id, LetzshopOrder.sync_status == "pending", ) .scalar() or 0 ) total_orders = ( self.db.query(func.count(LetzshopOrder.id)) .filter(LetzshopOrder.vendor_id == vendor.id) .scalar() or 0 ) vendor_overviews.append( { "vendor_id": vendor.id, "vendor_name": vendor.name, "vendor_code": vendor.vendor_code, "is_configured": credentials is not None, "auto_sync_enabled": credentials.auto_sync_enabled if credentials else False, "last_sync_at": credentials.last_sync_at if credentials else None, "last_sync_status": credentials.last_sync_status if credentials else None, "pending_orders": pending_orders, "total_orders": total_orders, } ) return vendor_overviews, total # ========================================================================= # Order Operations # ========================================================================= def get_order(self, vendor_id: int, order_id: int) -> LetzshopOrder | None: """Get a Letzshop order by ID for a specific vendor.""" return ( self.db.query(LetzshopOrder) .filter( LetzshopOrder.id == order_id, LetzshopOrder.vendor_id == vendor_id, ) .first() ) def get_order_or_raise(self, vendor_id: int, order_id: int) -> LetzshopOrder: """Get a Letzshop order or raise OrderNotFoundError.""" order = self.get_order(vendor_id, order_id) if order is None: raise OrderNotFoundError(f"Letzshop order {order_id} not found") return order def get_order_by_shipment_id( self, vendor_id: int, shipment_id: str ) -> LetzshopOrder | None: """Get a Letzshop order by shipment ID.""" return ( self.db.query(LetzshopOrder) .filter( LetzshopOrder.vendor_id == vendor_id, LetzshopOrder.letzshop_shipment_id == shipment_id, ) .first() ) def list_orders( self, vendor_id: int, skip: int = 0, limit: int = 50, sync_status: str | None = None, letzshop_state: str | None = None, ) -> tuple[list[LetzshopOrder], int]: """ List Letzshop orders for a vendor. Returns a tuple of (orders, total_count). """ query = self.db.query(LetzshopOrder).filter( LetzshopOrder.vendor_id == vendor_id ) if sync_status: query = query.filter(LetzshopOrder.sync_status == sync_status) if letzshop_state: query = query.filter(LetzshopOrder.letzshop_state == letzshop_state) total = query.count() orders = ( query.order_by(LetzshopOrder.created_at.desc()) .offset(skip) .limit(limit) .all() ) return orders, total def create_order( self, vendor_id: int, shipment_data: dict[str, Any], ) -> LetzshopOrder: """Create a new Letzshop order from shipment data.""" order_data = shipment_data.get("order", {}) order = LetzshopOrder( vendor_id=vendor_id, letzshop_order_id=order_data.get("id", ""), letzshop_shipment_id=shipment_data["id"], letzshop_order_number=order_data.get("number"), letzshop_state=shipment_data.get("state"), customer_email=order_data.get("email"), total_amount=str(order_data.get("totalPrice", {}).get("amount", "")), currency=order_data.get("totalPrice", {}).get("currency", "EUR"), raw_order_data=shipment_data, inventory_units=[ {"id": u["id"], "state": u["state"]} for u in shipment_data.get("inventoryUnits", {}).get("nodes", []) ], sync_status="pending", ) self.db.add(order) return order def update_order_from_shipment( self, order: LetzshopOrder, shipment_data: dict[str, Any], ) -> LetzshopOrder: """Update an existing order from shipment data.""" order.letzshop_state = shipment_data.get("state") order.raw_order_data = shipment_data return order def mark_order_confirmed(self, order: LetzshopOrder) -> LetzshopOrder: """Mark an order as confirmed.""" order.confirmed_at = datetime.now(UTC) order.sync_status = "confirmed" return order def mark_order_rejected(self, order: LetzshopOrder) -> LetzshopOrder: """Mark an order as rejected.""" order.rejected_at = datetime.now(UTC) order.sync_status = "rejected" return order def set_order_tracking( self, order: LetzshopOrder, tracking_number: str, tracking_carrier: str, ) -> LetzshopOrder: """Set tracking information for an order.""" order.tracking_number = tracking_number order.tracking_carrier = tracking_carrier order.tracking_set_at = datetime.now(UTC) order.sync_status = "shipped" return order # ========================================================================= # Sync Log Operations # ========================================================================= def list_sync_logs( self, vendor_id: int, skip: int = 0, limit: int = 50, ) -> tuple[list[LetzshopSyncLog], int]: """ List sync logs for a vendor. Returns a tuple of (logs, total_count). """ query = self.db.query(LetzshopSyncLog).filter( LetzshopSyncLog.vendor_id == vendor_id ) total = query.count() logs = ( query.order_by(LetzshopSyncLog.started_at.desc()) .offset(skip) .limit(limit) .all() ) return logs, total # ========================================================================= # Fulfillment Queue Operations # ========================================================================= def list_fulfillment_queue( self, vendor_id: int, skip: int = 0, limit: int = 50, status: str | None = None, ) -> tuple[list[LetzshopFulfillmentQueue], int]: """ List fulfillment queue items for a vendor. Returns a tuple of (items, total_count). """ query = self.db.query(LetzshopFulfillmentQueue).filter( LetzshopFulfillmentQueue.vendor_id == vendor_id ) if status: query = query.filter(LetzshopFulfillmentQueue.status == status) total = query.count() items = ( query.order_by(LetzshopFulfillmentQueue.created_at.desc()) .offset(skip) .limit(limit) .all() ) return items, total