# app/services/letzshop/order_service.py """ Letzshop order service for handling order-related database operations. This service handles Letzshop-specific order operations while using the unified Order model. All Letzshop orders are stored in the `orders` table with `channel='letzshop'`. """ import logging from datetime import UTC, datetime from typing import Any, Callable from sqlalchemy import String, and_, func, or_ from sqlalchemy.orm import Session from app.services.order_service import order_service as unified_order_service from app.services.subscription_service import subscription_service from models.database.letzshop import ( LetzshopFulfillmentQueue, LetzshopHistoricalImportJob, LetzshopSyncLog, VendorLetzshopCredentials, ) from models.database.marketplace_import_job import MarketplaceImportJob from app.modules.orders.models import Order, OrderItem from models.database.product import Product 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 an order is not found.""" class LetzshopOrderService: """Service for Letzshop order database operations using unified Order model.""" 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). """ query = self.db.query(Vendor).filter(Vendor.is_active == True) # noqa: E712 if configured_only: query = query.join( VendorLetzshopCredentials, Vendor.id == VendorLetzshopCredentials.vendor_id, ) total = query.count() vendors = query.order_by(Vendor.name).offset(skip).limit(limit).all() vendor_overviews = [] for vendor in vendors: credentials = ( self.db.query(VendorLetzshopCredentials) .filter(VendorLetzshopCredentials.vendor_id == vendor.id) .first() ) # Count Letzshop orders from unified orders table pending_orders = 0 total_orders = 0 if credentials: pending_orders = ( self.db.query(func.count(Order.id)) .filter( Order.vendor_id == vendor.id, Order.channel == "letzshop", Order.status == "pending", ) .scalar() or 0 ) total_orders = ( self.db.query(func.count(Order.id)) .filter( Order.vendor_id == vendor.id, Order.channel == "letzshop", ) .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 (using unified Order model) # ========================================================================= def get_order(self, vendor_id: int, order_id: int) -> Order | None: """Get a Letzshop order by ID for a specific vendor.""" return ( self.db.query(Order) .filter( Order.id == order_id, Order.vendor_id == vendor_id, Order.channel == "letzshop", ) .first() ) def get_order_or_raise(self, vendor_id: int, order_id: int) -> Order: """Get a Letzshop order or raise OrderNotFoundError.""" order = self.get_order(vendor_id, order_id) if order is None: raise OrderNotFoundError(f"Order {order_id} not found") return order def get_order_by_shipment_id( self, vendor_id: int, shipment_id: str ) -> Order | None: """Get a Letzshop order by external shipment ID.""" return ( self.db.query(Order) .filter( Order.vendor_id == vendor_id, Order.channel == "letzshop", Order.external_shipment_id == shipment_id, ) .first() ) def get_order_by_id(self, order_id: int) -> Order | None: """Get a Letzshop order by its database ID.""" return ( self.db.query(Order) .filter( Order.id == order_id, Order.channel == "letzshop", ) .first() ) def list_orders( self, vendor_id: int | None = None, skip: int = 0, limit: int = 50, status: str | None = None, has_declined_items: bool | None = None, search: str | None = None, ) -> tuple[list[Order], int]: """ List Letzshop orders for a vendor (or all vendors). Args: vendor_id: Vendor ID to filter by. If None, returns all vendors. skip: Number of records to skip. limit: Maximum number of records to return. status: Filter by order status (pending, processing, shipped, etc.) has_declined_items: If True, only return orders with declined items. search: Search by order number, customer name, or email. Returns a tuple of (orders, total_count). """ query = self.db.query(Order).filter( Order.channel == "letzshop", ) # Filter by vendor if specified if vendor_id is not None: query = query.filter(Order.vendor_id == vendor_id) if status: query = query.filter(Order.status == status) if search: search_term = f"%{search}%" query = query.filter( or_( Order.order_number.ilike(search_term), Order.external_order_number.ilike(search_term), Order.customer_email.ilike(search_term), Order.customer_first_name.ilike(search_term), Order.customer_last_name.ilike(search_term), ) ) # Filter for orders with declined items if has_declined_items is True: # Subquery to find orders with declined items declined_order_ids = ( self.db.query(OrderItem.order_id) .filter(OrderItem.item_state == "confirmed_unavailable") .subquery() ) query = query.filter(Order.id.in_(declined_order_ids)) total = query.count() orders = ( query.order_by(Order.order_date.desc()) .offset(skip) .limit(limit) .all() ) return orders, total def get_order_stats(self, vendor_id: int | None = None) -> dict[str, int]: """ Get order counts by status for Letzshop orders. Args: vendor_id: Vendor ID to filter by. If None, returns stats for all vendors. Returns: Dict with counts for each status. """ query = self.db.query( Order.status, func.count(Order.id).label("count"), ).filter(Order.channel == "letzshop") if vendor_id is not None: query = query.filter(Order.vendor_id == vendor_id) status_counts = query.group_by(Order.status).all() stats = { "pending": 0, "processing": 0, "shipped": 0, "delivered": 0, "cancelled": 0, "refunded": 0, "total": 0, } for status, count in status_counts: if status in stats: stats[status] = count stats["total"] += count # Count orders with declined items declined_query = ( self.db.query(func.count(func.distinct(OrderItem.order_id))) .join(Order, OrderItem.order_id == Order.id) .filter( Order.channel == "letzshop", OrderItem.item_state == "confirmed_unavailable", ) ) if vendor_id is not None: declined_query = declined_query.filter(Order.vendor_id == vendor_id) stats["has_declined_items"] = declined_query.scalar() or 0 return stats def create_order( self, vendor_id: int, shipment_data: dict[str, Any], ) -> Order: """ Create a new Letzshop order from shipment data. Uses the unified order service to create the order. """ return unified_order_service.create_letzshop_order( db=self.db, vendor_id=vendor_id, shipment_data=shipment_data, ) def update_order_from_shipment( self, order: Order, shipment_data: dict[str, Any], ) -> Order: """Update an existing order from shipment data.""" order_data = shipment_data.get("order", {}) # Map Letzshop state to status letzshop_state = shipment_data.get("state", "unconfirmed") state_mapping = { "unconfirmed": "pending", "confirmed": "processing", "declined": "cancelled", } new_status = state_mapping.get(letzshop_state, "processing") # Update status if changed if order.status != new_status: order.status = new_status now = datetime.now(UTC) if new_status == "processing": order.confirmed_at = now elif new_status == "cancelled": order.cancelled_at = now # Update external data order.external_data = shipment_data # Update locale if not set if not order.customer_locale and order_data.get("locale"): order.customer_locale = order_data.get("locale") # Update order_date if not set if not order.order_date: completed_at_str = order_data.get("completedAt") if completed_at_str: try: if completed_at_str.endswith("Z"): completed_at_str = completed_at_str[:-1] + "+00:00" order.order_date = datetime.fromisoformat(completed_at_str) except (ValueError, TypeError): pass # Update inventory unit states in order items inventory_units_data = shipment_data.get("inventoryUnits", []) if isinstance(inventory_units_data, dict): inventory_units_data = inventory_units_data.get("nodes", []) for unit in inventory_units_data: unit_id = unit.get("id") unit_state = unit.get("state") if unit_id and unit_state: # Find and update the corresponding order item item = ( self.db.query(OrderItem) .filter( OrderItem.order_id == order.id, OrderItem.external_item_id == unit_id, ) .first() ) if item: item.item_state = unit_state order.updated_at = datetime.now(UTC) return order def mark_order_confirmed(self, order: Order) -> Order: """Mark an order as confirmed (processing).""" order.confirmed_at = datetime.now(UTC) order.status = "processing" order.updated_at = datetime.now(UTC) return order def mark_order_rejected(self, order: Order) -> Order: """Mark an order as rejected (cancelled).""" order.cancelled_at = datetime.now(UTC) order.status = "cancelled" order.updated_at = datetime.now(UTC) return order def update_inventory_unit_state( self, order: Order, item_id: str, state: str ) -> Order: """ Update the state of a single order item. Args: order: The order containing the item. item_id: The external item ID (Letzshop inventory unit ID). state: The new state (confirmed_available, confirmed_unavailable). Returns: The updated order. """ # Find and update the item item = ( self.db.query(OrderItem) .filter( OrderItem.order_id == order.id, OrderItem.external_item_id == item_id, ) .first() ) if item: item.item_state = state item.updated_at = datetime.now(UTC) # Check if all items are now processed all_items = ( self.db.query(OrderItem) .filter(OrderItem.order_id == order.id) .all() ) all_confirmed = all( i.item_state in ("confirmed_available", "confirmed_unavailable", "returned") for i in all_items ) if all_confirmed: has_available = any( i.item_state == "confirmed_available" for i in all_items ) all_unavailable = all( i.item_state == "confirmed_unavailable" for i in all_items ) now = datetime.now(UTC) if all_unavailable: order.status = "cancelled" order.cancelled_at = now elif has_available: order.status = "processing" order.confirmed_at = now order.updated_at = now return order def set_order_tracking( self, order: Order, tracking_number: str, tracking_provider: str, ) -> Order: """Set tracking information for an order.""" order.tracking_number = tracking_number order.tracking_provider = tracking_provider order.shipped_at = datetime.now(UTC) order.status = "shipped" order.updated_at = datetime.now(UTC) return order def get_orders_without_tracking( self, vendor_id: int, limit: int = 100, ) -> list[Order]: """Get orders that have been confirmed but don't have tracking info.""" return ( self.db.query(Order) .filter( Order.vendor_id == vendor_id, Order.channel == "letzshop", Order.status == "processing", # Confirmed orders Order.tracking_number.is_(None), Order.external_shipment_id.isnot(None), # Has shipment ID ) .limit(limit) .all() ) def update_tracking_from_shipment_data( self, order: Order, shipment_data: dict[str, Any], ) -> bool: """ Update order tracking from Letzshop shipment data. Args: order: The order to update. shipment_data: Raw shipment data from Letzshop API. Returns: True if tracking was updated, False otherwise. """ tracking_data = shipment_data.get("tracking") or {} tracking_number = tracking_data.get("code") or tracking_data.get("number") if not tracking_number: return False tracking_provider = tracking_data.get("provider") # Handle carrier object format: tracking { carrier { name code } } if not tracking_provider and tracking_data.get("carrier"): carrier = tracking_data.get("carrier", {}) tracking_provider = carrier.get("code") or carrier.get("name") order.tracking_number = tracking_number order.tracking_provider = tracking_provider order.updated_at = datetime.now(UTC) logger.info( f"Updated tracking for order {order.order_number}: " f"{tracking_provider} {tracking_number}" ) return True def get_order_items(self, order: Order) -> list[OrderItem]: """Get all items for an order.""" return ( self.db.query(OrderItem) .filter(OrderItem.order_id == order.id) .all() ) # ========================================================================= # 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.""" 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.""" 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 def add_to_fulfillment_queue( self, vendor_id: int, order_id: int, operation: str, payload: dict[str, Any], ) -> LetzshopFulfillmentQueue: """Add an operation to the fulfillment queue.""" queue_item = LetzshopFulfillmentQueue( vendor_id=vendor_id, order_id=order_id, operation=operation, payload=payload, status="pending", ) self.db.add(queue_item) return queue_item # ========================================================================= # Unified Jobs Operations # ========================================================================= def list_letzshop_jobs( self, vendor_id: int | None = None, job_type: str | None = None, status: str | None = None, skip: int = 0, limit: int = 20, ) -> tuple[list[dict[str, Any]], int]: """ List unified Letzshop-related jobs for a vendor or all vendors. Combines product imports, historical order imports, and order syncs. If vendor_id is None, returns jobs across all vendors. """ jobs = [] # Fetch vendor info - for single vendor or build lookup for all vendors if vendor_id: vendor = self.get_vendor(vendor_id) vendor_lookup = {vendor_id: (vendor.name if vendor else None, vendor.vendor_code if vendor else None)} else: # Build lookup for all vendors when showing all jobs from models.database.vendor import Vendor vendors = self.db.query(Vendor.id, Vendor.name, Vendor.vendor_code).all() vendor_lookup = {v.id: (v.name, v.vendor_code) for v in vendors} # Historical order imports from letzshop_historical_import_jobs if job_type in (None, "historical_import"): hist_query = self.db.query(LetzshopHistoricalImportJob) if vendor_id: hist_query = hist_query.filter( LetzshopHistoricalImportJob.vendor_id == vendor_id, ) if status: hist_query = hist_query.filter( LetzshopHistoricalImportJob.status == status ) hist_jobs = hist_query.order_by( LetzshopHistoricalImportJob.created_at.desc() ).all() for job in hist_jobs: v_name, v_code = vendor_lookup.get(job.vendor_id, (None, None)) jobs.append( { "id": job.id, "type": "historical_import", "status": job.status, "created_at": job.created_at, "started_at": job.started_at, "completed_at": job.completed_at, "records_processed": job.orders_processed or 0, "records_succeeded": (job.orders_imported or 0) + (job.orders_updated or 0), "records_failed": job.orders_skipped or 0, "vendor_id": job.vendor_id, "vendor_name": v_name, "vendor_code": v_code, "current_phase": job.current_phase, "error_message": job.error_message, } ) # Product imports from marketplace_import_jobs if job_type in (None, "import"): import_query = self.db.query(MarketplaceImportJob).filter( MarketplaceImportJob.marketplace == "Letzshop", ) if vendor_id: import_query = import_query.filter( MarketplaceImportJob.vendor_id == vendor_id, ) if status: import_query = import_query.filter( MarketplaceImportJob.status == status ) import_jobs = import_query.order_by( MarketplaceImportJob.created_at.desc() ).all() for job in import_jobs: v_name, v_code = vendor_lookup.get(job.vendor_id, (None, None)) jobs.append( { "id": job.id, "type": "import", "status": job.status, "created_at": job.created_at, "started_at": job.started_at, "completed_at": job.completed_at, "records_processed": job.total_processed or 0, "records_succeeded": (job.imported_count or 0) + (job.updated_count or 0), "records_failed": job.error_count or 0, "vendor_id": job.vendor_id, "vendor_name": v_name, "vendor_code": v_code, } ) # Order syncs from letzshop_sync_logs if job_type in (None, "order_sync"): sync_query = self.db.query(LetzshopSyncLog).filter( LetzshopSyncLog.operation_type == "order_import", ) if vendor_id: sync_query = sync_query.filter(LetzshopSyncLog.vendor_id == vendor_id) if status: sync_query = sync_query.filter(LetzshopSyncLog.status == status) sync_logs = sync_query.order_by(LetzshopSyncLog.created_at.desc()).all() for log in sync_logs: v_name, v_code = vendor_lookup.get(log.vendor_id, (None, None)) jobs.append( { "id": log.id, "type": "order_sync", "status": log.status, "created_at": log.created_at, "started_at": log.started_at, "completed_at": log.completed_at, "records_processed": log.records_processed or 0, "records_succeeded": log.records_succeeded or 0, "records_failed": log.records_failed or 0, "vendor_id": log.vendor_id, "vendor_name": v_name, "vendor_code": v_code, "error_details": log.error_details, } ) # Product exports from letzshop_sync_logs if job_type in (None, "export"): export_query = self.db.query(LetzshopSyncLog).filter( LetzshopSyncLog.operation_type == "product_export", ) if vendor_id: export_query = export_query.filter(LetzshopSyncLog.vendor_id == vendor_id) if status: export_query = export_query.filter(LetzshopSyncLog.status == status) export_logs = export_query.order_by( LetzshopSyncLog.created_at.desc() ).all() for log in export_logs: v_name, v_code = vendor_lookup.get(log.vendor_id, (None, None)) jobs.append( { "id": log.id, "type": "export", "status": log.status, "created_at": log.created_at, "started_at": log.started_at, "completed_at": log.completed_at, "records_processed": log.records_processed or 0, "records_succeeded": log.records_succeeded or 0, "records_failed": log.records_failed or 0, "vendor_id": log.vendor_id, "vendor_name": v_name, "vendor_code": v_code, "error_details": log.error_details, } ) # Sort all jobs by created_at descending jobs.sort(key=lambda x: x["created_at"], reverse=True) total = len(jobs) jobs = jobs[skip : skip + limit] return jobs, total # ========================================================================= # Historical Import Operations # ========================================================================= def import_historical_shipments( self, vendor_id: int, shipments: list[dict[str, Any]], match_products: bool = True, progress_callback: Callable[[int, int, int, int], None] | None = None, ) -> dict[str, Any]: """ Import historical shipments into the unified orders table. Args: vendor_id: Vendor ID to import for. shipments: List of shipment data from Letzshop API. match_products: Whether to match GTIN to local products. progress_callback: Optional callback(processed, imported, updated, skipped) Returns: Dict with import statistics. """ stats = { "total": len(shipments), "imported": 0, "updated": 0, "skipped": 0, "errors": 0, "limit_exceeded": 0, "products_matched": 0, "products_not_found": 0, "eans_processed": set(), "eans_matched": set(), "eans_not_found": set(), "error_messages": [], } # Get subscription usage upfront for batch efficiency usage = subscription_service.get_usage(self.db, vendor_id) orders_remaining = usage.orders_remaining # None = unlimited for i, shipment in enumerate(shipments): shipment_id = shipment.get("id") if not shipment_id: continue # Check if order already exists existing_order = self.get_order_by_shipment_id(vendor_id, shipment_id) if existing_order: # Check if we need to update letzshop_state = shipment.get("state") state_mapping = { "unconfirmed": "pending", "confirmed": "processing", "declined": "cancelled", } expected_status = state_mapping.get(letzshop_state, "processing") needs_update = False if existing_order.status != expected_status: self.update_order_from_shipment(existing_order, shipment) needs_update = True # Update order_date if missing if not existing_order.order_date: order_data = shipment.get("order", {}) completed_at_str = order_data.get("completedAt") if completed_at_str: try: if completed_at_str.endswith("Z"): completed_at_str = completed_at_str[:-1] + "+00:00" existing_order.order_date = datetime.fromisoformat( completed_at_str ) needs_update = True except (ValueError, TypeError): pass if needs_update: self.db.commit() # noqa: SVC-006 - background task needs incremental commits stats["updated"] += 1 else: stats["skipped"] += 1 else: # Check tier limit before creating order if orders_remaining is not None and orders_remaining <= 0: stats["limit_exceeded"] += 1 stats["error_messages"].append( f"Shipment {shipment_id}: Order limit reached" ) continue # Create new order using unified service try: self.create_order(vendor_id, shipment) self.db.commit() # noqa: SVC-006 - background task needs incremental commits stats["imported"] += 1 # Decrement remaining count for batch efficiency if orders_remaining is not None: orders_remaining -= 1 except Exception as e: self.db.rollback() # Rollback failed order stats["errors"] += 1 stats["error_messages"].append( f"Shipment {shipment_id}: {str(e)}" ) logger.error(f"Error importing shipment {shipment_id}: {e}") # Process GTINs for matching if match_products: inventory_units = shipment.get("inventoryUnits", []) for unit in inventory_units: variant = unit.get("variant", {}) or {} trade_id = variant.get("tradeId") or {} gtin = trade_id.get("number") if gtin: stats["eans_processed"].add(gtin) # Report progress if progress_callback and ((i + 1) % 10 == 0 or i == len(shipments) - 1): progress_callback( i + 1, stats["imported"], stats["updated"], stats["skipped"], ) # Match GTINs to local products if match_products and stats["eans_processed"]: matched, not_found = self._match_gtins_to_products( vendor_id, list(stats["eans_processed"]) ) stats["eans_matched"] = matched stats["eans_not_found"] = not_found stats["products_matched"] = len(matched) stats["products_not_found"] = len(not_found) # Convert sets to lists for JSON serialization stats["eans_processed"] = list(stats["eans_processed"]) stats["eans_matched"] = list(stats["eans_matched"]) stats["eans_not_found"] = list(stats["eans_not_found"]) return stats def _match_gtins_to_products( self, vendor_id: int, gtins: list[str], ) -> tuple[set[str], set[str]]: """Match GTIN codes to local products.""" if not gtins: return set(), set() products = ( self.db.query(Product) .filter( Product.vendor_id == vendor_id, Product.gtin.in_(gtins), ) .all() ) matched_gtins = {p.gtin for p in products if p.gtin} not_found_gtins = set(gtins) - matched_gtins logger.info( f"GTIN matching: {len(matched_gtins)} matched, " f"{len(not_found_gtins)} not found" ) return matched_gtins, not_found_gtins def get_products_by_gtins( self, vendor_id: int, gtins: list[str], ) -> dict[str, Product]: """Get products by their GTIN codes.""" if not gtins: return {} products = ( self.db.query(Product) .filter( Product.vendor_id == vendor_id, Product.gtin.in_(gtins), ) .all() ) return {p.gtin: p for p in products if p.gtin} def get_historical_import_summary( self, vendor_id: int, ) -> dict[str, Any]: """Get summary of Letzshop orders for a vendor.""" # Count orders by status status_counts = ( self.db.query( Order.status, func.count(Order.id).label("count"), ) .filter( Order.vendor_id == vendor_id, Order.channel == "letzshop", ) .group_by(Order.status) .all() ) # Count orders by locale locale_counts = ( self.db.query( Order.customer_locale, func.count(Order.id).label("count"), ) .filter( Order.vendor_id == vendor_id, Order.channel == "letzshop", ) .group_by(Order.customer_locale) .all() ) # Count orders by country country_counts = ( self.db.query( Order.ship_country_iso, func.count(Order.id).label("count"), ) .filter( Order.vendor_id == vendor_id, Order.channel == "letzshop", ) .group_by(Order.ship_country_iso) .all() ) # Total orders total_orders = ( self.db.query(func.count(Order.id)) .filter( Order.vendor_id == vendor_id, Order.channel == "letzshop", ) .scalar() or 0 ) # Unique customers unique_customers = ( self.db.query(func.count(func.distinct(Order.customer_email))) .filter( Order.vendor_id == vendor_id, Order.channel == "letzshop", ) .scalar() or 0 ) return { "total_orders": total_orders, "unique_customers": unique_customers, "orders_by_status": {status: count for status, count in status_counts}, "orders_by_locale": { locale or "unknown": count for locale, count in locale_counts }, "orders_by_country": { country or "unknown": count for country, count in country_counts }, } # ========================================================================= # Historical Import Job Operations # ========================================================================= def get_running_historical_import_job( self, vendor_id: int, ) -> LetzshopHistoricalImportJob | None: """Get any running historical import job for a vendor.""" return ( self.db.query(LetzshopHistoricalImportJob) .filter( LetzshopHistoricalImportJob.vendor_id == vendor_id, LetzshopHistoricalImportJob.status.in_( ["pending", "fetching", "processing"] ), ) .first() ) def create_historical_import_job( self, vendor_id: int, user_id: int, ) -> LetzshopHistoricalImportJob: """Create a new historical import job.""" job = LetzshopHistoricalImportJob( vendor_id=vendor_id, user_id=user_id, status="pending", ) self.db.add(job) self.db.commit() # noqa: SVC-006 - job must be visible immediately before background task starts self.db.refresh(job) return job def get_historical_import_job_by_id( self, vendor_id: int, job_id: int, ) -> LetzshopHistoricalImportJob | None: """Get a historical import job by ID.""" return ( self.db.query(LetzshopHistoricalImportJob) .filter( LetzshopHistoricalImportJob.id == job_id, LetzshopHistoricalImportJob.vendor_id == vendor_id, ) .first() ) def update_job_celery_task_id( self, job_id: int, celery_task_id: str, ) -> bool: """ Update the Celery task ID for a historical import job. Args: job_id: The job ID to update. celery_task_id: The Celery task ID to set. Returns: True if updated successfully, False if job not found. """ job = ( self.db.query(LetzshopHistoricalImportJob) .filter(LetzshopHistoricalImportJob.id == job_id) .first() ) if job: job.celery_task_id = celery_task_id self.db.commit() # noqa: SVC-006 - Called from API endpoint return True return False