""" Background work that shouldn't run on the GUI thread. FetchOrdersWorker runs a service's fetch_orders() call off the main thread and reports back via signals. As we add more long-running operations (creating ShipStation labels, pushing to Odoo, etc.) they should follow this same pattern rather than blocking the UI. """ from __future__ import annotations from typing import List from PyQt6.QtCore import QThread, pyqtSignal from sqlalchemy import select from app.database import get_session from app.models import Order from app.services.base import OrderService, NormalizedOrder class FetchOrdersWorker(QThread): """Fetches orders from a given service and saves new/updated ones to the DB.""" finished_ok = pyqtSignal(int, int) # (new_count, updated_count) failed = pyqtSignal(str) def __init__(self, service: OrderService, parent=None): super().__init__(parent) self.service = service def run(self) -> None: try: orders = self.service.fetch_orders() except Exception as exc: # noqa: BLE001 - surface any failure to the UI self.failed.emit(str(exc)) return try: new_count, updated_count = save_orders(orders) except Exception as exc: # noqa: BLE001 self.failed.emit(f"Fetched {len(orders)} orders but failed to save them: {exc}") return self.finished_ok.emit(new_count, updated_count) def save_orders(orders: List[NormalizedOrder]) -> tuple[int, int]: """Insert new orders / update existing ones (matched by source + external_id).""" session = get_session() new_count = 0 updated_count = 0 try: for order in orders: existing = session.execute( select(Order).where( Order.source == order["source"], Order.external_id == order["external_id"], ) ).scalar_one_or_none() if existing is None: session.add( Order( source=order["source"], external_id=order["external_id"], ticket_number=order.get("ticket_number"), company=order.get("company", "Unknown"), skus=order.get("skus", []), summary=order["summary"], status=order["status"], source_created_at=order["source_created_at"], raw_data=order["raw_data"], ) ) new_count += 1 else: existing.ticket_number = order.get("ticket_number") existing.company = order.get("company", "Unknown") existing.skus = order.get("skus", []) existing.summary = order["summary"] existing.status = order["status"] existing.source_created_at = order["source_created_at"] existing.raw_data = order["raw_data"] updated_count += 1 session.commit() finally: session.close() return new_count, updated_count def load_all_orders() -> List[Order]: session = get_session() try: return list( session.execute(select(Order).order_by(Order.source_created_at.desc())).scalars() ) finally: session.close() def get_dashboard_stats() -> dict: """ Counts for the dashboard tab: totals by company and by source, plus how many orders are only in one source so far (imported from JIRA but not yet seen in ShipStation, or vice versa) - useful as an at-a-glance "what's still missing" signal since the two sources are pulled independently. """ orders = load_all_orders() by_company: dict[str, int] = {} by_source: dict[str, int] = {} tickets_by_source: dict[str, set] = {} for order in orders: by_company[order.company] = by_company.get(order.company, 0) + 1 by_source[order.source] = by_source.get(order.source, 0) + 1 if order.ticket_number: tickets_by_source.setdefault(order.source, set()).add(order.ticket_number) jira_tickets = tickets_by_source.get("jira", set()) shipstation_tickets = tickets_by_source.get("shipstation", set()) return { "total": len(orders), "by_company": by_company, "by_source": by_source, "jira_only_count": len(jira_tickets - shipstation_tickets), "shipstation_only_count": len(shipstation_tickets - jira_tickets), "matched_count": len(jira_tickets & shipstation_tickets), }