From f6d283b74381586d2248ed261daef66500b9ec86 Mon Sep 17 00:00:00 2001 From: Claude Agent Date: Wed, 22 Apr 2026 08:45:32 +0000 Subject: [PATCH] refactor(status): introduce OrderStatus enum, replace string literals MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Centralized order status values in api/app/constants.py via a str-valued Enum so comparisons keep working. Replaced literals in: - services: sync_service, sqlite_service, retry_service - routers: sync, dashboard - templates: dashboard.html, logs.html - static JS: shared (ORDER_STATUS mirror), dashboard, logs - tests: requirements, order_items_overwrite, business_rules MALFORMED intentionally NOT added — introduced in follow-up PR2 (per-order failure isolation). Full test suite: 231 unit + 33 e2e pass. Co-Authored-By: Claude Opus 4.7 (1M context) --- api/app/constants.py | 17 ++++++ api/app/routers/dashboard.py | 2 + api/app/routers/sync.py | 18 +++--- api/app/services/retry_service.py | 18 +++--- api/app/services/sqlite_service.py | 75 +++++++++++++------------ api/app/services/sync_service.py | 25 +++++---- api/app/static/js/dashboard.js | 20 +++---- api/app/static/js/logs.js | 28 ++++----- api/app/static/js/shared.js | 42 ++++++++------ api/app/templates/dashboard.html | 8 +-- api/app/templates/logs.html | 8 +-- api/tests/test_business_rules.py | 13 +++-- api/tests/test_order_items_overwrite.py | 5 +- api/tests/test_requirements.py | 25 +++++---- 14 files changed, 171 insertions(+), 133 deletions(-) create mode 100644 api/app/constants.py diff --git a/api/app/constants.py b/api/app/constants.py new file mode 100644 index 0000000..ecb9f2f --- /dev/null +++ b/api/app/constants.py @@ -0,0 +1,17 @@ +"""Application-wide constants shared across services, routers, and tests.""" +from enum import Enum + + +class OrderStatus(str, Enum): + """Order status values stored in SQLite `orders.status` column. + + Inherits from `str` so existing string comparisons (==, in, dict.get) + keep working. Always use `.value` when passing to SQL queries or JSON + payloads to avoid Python-version-specific str(enum) surprises. + """ + IMPORTED = "IMPORTED" + ALREADY_IMPORTED = "ALREADY_IMPORTED" + SKIPPED = "SKIPPED" + ERROR = "ERROR" + CANCELLED = "CANCELLED" + DELETED_IN_ROA = "DELETED_IN_ROA" diff --git a/api/app/routers/dashboard.py b/api/app/routers/dashboard.py index c012e0f..98e2ecf 100644 --- a/api/app/routers/dashboard.py +++ b/api/app/routers/dashboard.py @@ -4,9 +4,11 @@ from fastapi.responses import HTMLResponse from pathlib import Path from ..services import sqlite_service +from ..constants import OrderStatus router = APIRouter() templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates")) +templates.env.globals["OrderStatus"] = OrderStatus @router.get("/", response_class=HTMLResponse) async def dashboard(request: Request): diff --git a/api/app/routers/sync.py b/api/app/routers/sync.py index 3769afc..fcbbb66 100644 --- a/api/app/routers/sync.py +++ b/api/app/routers/sync.py @@ -14,9 +14,11 @@ from typing import Optional from ..services import sync_service, scheduler_service, sqlite_service, invoice_service from .. import database +from ..constants import OrderStatus router = APIRouter(tags=["sync"]) templates = Jinja2Templates(directory=str(Path(__file__).parent.parent / "templates")) +templates.env.globals["OrderStatus"] = OrderStatus async def _enrich_items_with_codmat(items: list) -> None: @@ -231,13 +233,13 @@ def _format_text_log_from_detail(detail: dict) -> str: customer = o.get("customer_name", "?") order_date = o.get("order_date") or "?" - if status == "IMPORTED": + if status == OrderStatus.IMPORTED.value: id_cmd = o.get("id_comanda", "?") lines.append(f"#{number} [{order_date}] {customer} → IMPORTAT (ID: {id_cmd})") - elif status == "ALREADY_IMPORTED": + elif status == OrderStatus.ALREADY_IMPORTED.value: id_cmd = o.get("id_comanda", "?") lines.append(f"#{number} [{order_date}] {customer} → DEJA IMPORTAT (ID: {id_cmd})") - elif status == "SKIPPED": + elif status == OrderStatus.SKIPPED.value: missing = o.get("missing_skus", "") if isinstance(missing, str): try: @@ -246,7 +248,7 @@ def _format_text_log_from_detail(detail: dict) -> str: missing = [missing] if missing else [] skus_str = ", ".join(missing) if isinstance(missing, list) else str(missing) lines.append(f"#{number} [{order_date}] {customer} → OMIS (lipsa: {skus_str})") - elif status == "ERROR": + elif status == OrderStatus.ERROR.value: err = o.get("error_message", "necunoscuta") lines.append(f"#{number} [{order_date}] {customer} → EROARE: {err}") @@ -618,7 +620,7 @@ async def dashboard_orders(page: int = 1, per_page: int = 50, is_invoiced_filter = (status == "INVOICED") # For UNINVOICED/INVOICED: fetch all IMPORTED orders, then filter post-invoice-check - fetch_status = "IMPORTED" if (is_uninvoiced_filter or is_invoiced_filter) else status + fetch_status = OrderStatus.IMPORTED.value if (is_uninvoiced_filter or is_invoiced_filter) else status fetch_per_page = 10000 if (is_uninvoiced_filter or is_invoiced_filter) else per_page fetch_page = 1 if (is_uninvoiced_filter or is_invoiced_filter) else page @@ -687,7 +689,7 @@ async def dashboard_orders(page: int = 1, per_page: int = 50, newly_invoiced = sum(1 for o in uncached_orders if o.get("invoice") and o["invoice"].get("facturat")) uninvoiced_base = counts.get("uninvoiced_sqlite", sum( 1 for o in all_orders - if o.get("status") in ("IMPORTED", "ALREADY_IMPORTED") and not o.get("invoice") + if o.get("status") in (OrderStatus.IMPORTED.value, OrderStatus.ALREADY_IMPORTED.value) and not o.get("invoice") )) counts["nefacturate"] = max(0, uninvoiced_base - newly_invoiced) imported_total = counts.get("imported_all") or counts.get("imported", 0) @@ -713,7 +715,7 @@ async def dashboard_orders(page: int = 1, per_page: int = 50, # For UNINVOICED filter: apply server-side filtering + pagination if is_uninvoiced_filter: - filtered = [o for o in all_orders if o.get("status") in ("IMPORTED", "ALREADY_IMPORTED") and not o.get("invoice")] + filtered = [o for o in all_orders if o.get("status") in (OrderStatus.IMPORTED.value, OrderStatus.ALREADY_IMPORTED.value) and not o.get("invoice")] total = len(filtered) offset = (page - 1) * per_page result["orders"] = filtered[offset:offset + per_page] @@ -722,7 +724,7 @@ async def dashboard_orders(page: int = 1, per_page: int = 50, result["per_page"] = per_page result["pages"] = (total + per_page - 1) // per_page if total > 0 else 0 elif is_invoiced_filter: - filtered = [o for o in all_orders if o.get("status") in ("IMPORTED", "ALREADY_IMPORTED") and o.get("invoice")] + filtered = [o for o in all_orders if o.get("status") in (OrderStatus.IMPORTED.value, OrderStatus.ALREADY_IMPORTED.value) and o.get("invoice")] total = len(filtered) offset = (page - 1) * per_page result["orders"] = filtered[offset:offset + per_page] diff --git a/api/app/services/retry_service.py b/api/app/services/retry_service.py index a7e779d..b385121 100644 --- a/api/app/services/retry_service.py +++ b/api/app/services/retry_service.py @@ -4,6 +4,8 @@ import logging import tempfile from datetime import datetime, timedelta +from ..constants import OrderStatus + logger = logging.getLogger(__name__) @@ -70,7 +72,7 @@ async def _download_and_reimport(order_number: str, order_date_str: str, custome order_number=order_number, order_date=order_date_str, customer_name=customer_name, - status="ERROR", + status=OrderStatus.ERROR.value, error_message=f"Retry failed: {e}", ) return {"success": False, "message": f"Eroare import: {e}"} @@ -103,7 +105,7 @@ async def _download_and_reimport(order_number: str, order_date_str: str, custome order_number=order_number, order_date=order_date_str, customer_name=customer_name, - status="IMPORTED", + status=OrderStatus.IMPORTED.value, id_comanda=result.get("id_comanda"), id_partener=result.get("id_partener"), error_message=None, @@ -116,7 +118,7 @@ async def _download_and_reimport(order_number: str, order_date_str: str, custome ) await sqlite_service.add_order_items(order_number, order_items_data) logger.info(f"Retry successful for order {order_number} → IMPORTED ({len(order_items_data)} items)") - return {"success": True, "message": "Comanda reimportata cu succes", "status": "IMPORTED"} + return {"success": True, "message": "Comanda reimportata cu succes", "status": OrderStatus.IMPORTED.value} else: error = result.get("error", "Unknown error") await sqlite_service.upsert_order( @@ -124,11 +126,11 @@ async def _download_and_reimport(order_number: str, order_date_str: str, custome order_number=order_number, order_date=order_date_str, customer_name=customer_name, - status="ERROR", + status=OrderStatus.ERROR.value, error_message=f"Retry: {error}", ) await sqlite_service.add_order_items(order_number, order_items_data) - return {"success": False, "message": f"Import esuat: {error}", "status": "ERROR"} + return {"success": False, "message": f"Import esuat: {error}", "status": OrderStatus.ERROR.value} async def retry_single_order(order_number: str, app_settings: dict) -> dict: @@ -157,7 +159,7 @@ async def retry_single_order(order_number: str, app_settings: dict) -> dict: order_data = detail["order"] status = order_data.get("status", "") - if status not in ("ERROR", "SKIPPED", "DELETED_IN_ROA"): + if status not in (OrderStatus.ERROR.value, OrderStatus.SKIPPED.value, OrderStatus.DELETED_IN_ROA.value): return {"success": False, "message": f"Retry permis doar pentru ERROR/SKIPPED/DELETED_IN_ROA (status actual: {status})"} order_date_str = order_data.get("order_date", "") @@ -196,7 +198,7 @@ async def resync_single_order(order_number: str, app_settings: dict) -> dict: status = order_data.get("status", "") id_comanda = order_data.get("id_comanda") - if status not in ("IMPORTED", "ALREADY_IMPORTED") or not id_comanda: + if status not in (OrderStatus.IMPORTED.value, OrderStatus.ALREADY_IMPORTED.value) or not id_comanda: return {"success": False, "message": f"Resync permis doar pentru IMPORTED/ALREADY_IMPORTED cu id_comanda (status actual: {status})"} # Invoice safety gate @@ -269,7 +271,7 @@ async def delete_single_order(order_number: str) -> dict: status = order_data.get("status", "") id_comanda = order_data.get("id_comanda") - if status not in ("IMPORTED", "ALREADY_IMPORTED") or not id_comanda: + if status not in (OrderStatus.IMPORTED.value, OrderStatus.ALREADY_IMPORTED.value) or not id_comanda: return {"success": False, "message": f"Stergere permisa doar pentru IMPORTED/ALREADY_IMPORTED cu id_comanda (status actual: {status})"} # Invoice safety gate diff --git a/api/app/services/sqlite_service.py b/api/app/services/sqlite_service.py index cac5317..c07f174 100644 --- a/api/app/services/sqlite_service.py +++ b/api/app/services/sqlite_service.py @@ -3,6 +3,7 @@ import logging from datetime import datetime from zoneinfo import ZoneInfo from ..database import get_sqlite, get_sqlite_sync +from ..constants import OrderStatus # Re-export so other services can import get_sqlite from sqlite_service __all__ = ["get_sqlite", "get_sqlite_sync"] @@ -68,7 +69,7 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str, """Upsert a single order — one row per order_number, status updated in place.""" db = await get_sqlite() try: - await db.execute(""" + await db.execute(f""" INSERT INTO orders (order_number, order_date, customer_name, status, id_comanda, id_partener, error_message, missing_skus, items_count, @@ -79,7 +80,7 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str, ON CONFLICT(order_number) DO UPDATE SET customer_name = excluded.customer_name, status = CASE - WHEN orders.status = 'IMPORTED' AND excluded.status = 'ALREADY_IMPORTED' + WHEN orders.status = '{OrderStatus.IMPORTED.value}' AND excluded.status = '{OrderStatus.ALREADY_IMPORTED.value}' THEN orders.status ELSE excluded.status END, @@ -88,7 +89,7 @@ async def upsert_order(sync_run_id: str, order_number: str, order_date: str, items_count = excluded.items_count, id_comanda = COALESCE(excluded.id_comanda, orders.id_comanda), id_partener = COALESCE(excluded.id_partener, orders.id_partener), - times_skipped = CASE WHEN excluded.status = 'SKIPPED' + times_skipped = CASE WHEN excluded.status = '{OrderStatus.SKIPPED.value}' THEN orders.times_skipped + 1 ELSE orders.times_skipped END, last_sync_run_id = excluded.last_sync_run_id, @@ -140,7 +141,7 @@ async def save_orders_batch(orders_data: list[dict]): db = await get_sqlite() try: # 1. Upsert orders - await db.executemany(""" + await db.executemany(f""" INSERT INTO orders (order_number, order_date, customer_name, status, id_comanda, id_partener, error_message, missing_skus, items_count, @@ -151,7 +152,7 @@ async def save_orders_batch(orders_data: list[dict]): ON CONFLICT(order_number) DO UPDATE SET customer_name = excluded.customer_name, status = CASE - WHEN orders.status = 'IMPORTED' AND excluded.status = 'ALREADY_IMPORTED' + WHEN orders.status = '{OrderStatus.IMPORTED.value}' AND excluded.status = '{OrderStatus.ALREADY_IMPORTED.value}' THEN orders.status ELSE excluded.status END, @@ -160,7 +161,7 @@ async def save_orders_batch(orders_data: list[dict]): items_count = excluded.items_count, id_comanda = COALESCE(excluded.id_comanda, orders.id_comanda), id_partener = COALESCE(excluded.id_partener, orders.id_partener), - times_skipped = CASE WHEN excluded.status = 'SKIPPED' + times_skipped = CASE WHEN excluded.status = '{OrderStatus.SKIPPED.value}' THEN orders.times_skipped + 1 ELSE orders.times_skipped END, last_sync_run_id = excluded.last_sync_run_id, @@ -400,17 +401,17 @@ async def get_dashboard_stats(): db = await get_sqlite() try: cursor = await db.execute( - "SELECT COUNT(*) FROM orders WHERE status = 'IMPORTED'" + f"SELECT COUNT(*) FROM orders WHERE status = '{OrderStatus.IMPORTED.value}'" ) imported = (await cursor.fetchone())[0] cursor = await db.execute( - "SELECT COUNT(*) FROM orders WHERE status = 'SKIPPED'" + f"SELECT COUNT(*) FROM orders WHERE status = '{OrderStatus.SKIPPED.value}'" ) skipped = (await cursor.fetchone())[0] cursor = await db.execute( - "SELECT COUNT(*) FROM orders WHERE status = 'ERROR'" + f"SELECT COUNT(*) FROM orders WHERE status = '{OrderStatus.ERROR.value}'" ) errors = (await cursor.fetchone())[0] @@ -694,11 +695,11 @@ async def get_run_orders_filtered(run_id: str, status_filter: str = "all", "per_page": per_page, "pages": (total + per_page - 1) // per_page if total > 0 else 0, "counts": { - "imported": status_counts.get("IMPORTED", 0), - "skipped": status_counts.get("SKIPPED", 0), - "error": status_counts.get("ERROR", 0), - "already_imported": status_counts.get("ALREADY_IMPORTED", 0), - "cancelled": status_counts.get("CANCELLED", 0), + "imported": status_counts.get(OrderStatus.IMPORTED.value, 0), + "skipped": status_counts.get(OrderStatus.SKIPPED.value, 0), + "error": status_counts.get(OrderStatus.ERROR.value, 0), + "already_imported": status_counts.get(OrderStatus.ALREADY_IMPORTED.value, 0), + "cancelled": status_counts.get(OrderStatus.CANCELLED.value, 0), "total": sum(status_counts.values()) } } @@ -738,8 +739,8 @@ async def get_orders(page: int = 1, per_page: int = 50, data_params = list(base_params) if status_filter and status_filter not in ("all", "UNINVOICED"): - if status_filter.upper() == "IMPORTED": - data_clauses.append("UPPER(status) IN ('IMPORTED', 'ALREADY_IMPORTED')") + if status_filter.upper() == OrderStatus.IMPORTED.value: + data_clauses.append(f"UPPER(status) IN ('{OrderStatus.IMPORTED.value}', '{OrderStatus.ALREADY_IMPORTED.value}')") elif status_filter.upper() == "DIFFS": data_clauses.append( "(anaf_cod_fiscal_adjusted = 1 OR anaf_denumire_mismatch = 1" @@ -785,7 +786,7 @@ async def get_orders(page: int = 1, per_page: int = 50, # Uninvoiced count: IMPORTED/ALREADY_IMPORTED with no cached invoice, same period+search uninv_clauses = list(base_clauses) + [ - "UPPER(status) IN ('IMPORTED', 'ALREADY_IMPORTED')", + f"UPPER(status) IN ('{OrderStatus.IMPORTED.value}', '{OrderStatus.ALREADY_IMPORTED.value}')", "(factura_numar IS NULL OR factura_numar = '')", ] uninv_where = "WHERE " + " AND ".join(uninv_clauses) @@ -794,7 +795,7 @@ async def get_orders(page: int = 1, per_page: int = 50, # Uninvoiced > 3 days old uninv_old_clauses = list(base_clauses) + [ - "UPPER(status) IN ('IMPORTED', 'ALREADY_IMPORTED')", + f"UPPER(status) IN ('{OrderStatus.IMPORTED.value}', '{OrderStatus.ALREADY_IMPORTED.value}')", "(factura_numar IS NULL OR factura_numar = '')", "order_date < datetime('now', '-3 days')", ] @@ -828,12 +829,12 @@ async def get_orders(page: int = 1, per_page: int = 50, "per_page": per_page, "pages": (total + per_page - 1) // per_page if total > 0 else 0, "counts": { - "imported": status_counts.get("IMPORTED", 0), - "already_imported": status_counts.get("ALREADY_IMPORTED", 0), - "imported_all": status_counts.get("IMPORTED", 0) + status_counts.get("ALREADY_IMPORTED", 0), - "skipped": status_counts.get("SKIPPED", 0), - "error": status_counts.get("ERROR", 0), - "cancelled": status_counts.get("CANCELLED", 0), + "imported": status_counts.get(OrderStatus.IMPORTED.value, 0), + "already_imported": status_counts.get(OrderStatus.ALREADY_IMPORTED.value, 0), + "imported_all": status_counts.get(OrderStatus.IMPORTED.value, 0) + status_counts.get(OrderStatus.ALREADY_IMPORTED.value, 0), + "skipped": status_counts.get(OrderStatus.SKIPPED.value, 0), + "error": status_counts.get(OrderStatus.ERROR.value, 0), + "cancelled": status_counts.get(OrderStatus.CANCELLED.value, 0), "total": sum(status_counts.values()), "uninvoiced_sqlite": uninvoiced_sqlite, "uninvoiced_old": uninvoiced_old, @@ -869,9 +870,9 @@ async def get_uninvoiced_imported_orders() -> list: """Get all imported orders that don't yet have invoice data cached.""" db = await get_sqlite() try: - cursor = await db.execute(""" + cursor = await db.execute(f""" SELECT order_number, id_comanda FROM orders - WHERE status IN ('IMPORTED', 'ALREADY_IMPORTED') + WHERE status IN ('{OrderStatus.IMPORTED.value}', '{OrderStatus.ALREADY_IMPORTED.value}') AND id_comanda IS NOT NULL AND factura_numar IS NULL """) @@ -923,9 +924,9 @@ async def get_invoiced_imported_orders() -> list: """Get imported orders that HAVE cached invoice data (for re-verification).""" db = await get_sqlite() try: - cursor = await db.execute(""" + cursor = await db.execute(f""" SELECT order_number, id_comanda FROM orders - WHERE status IN ('IMPORTED', 'ALREADY_IMPORTED') + WHERE status IN ('{OrderStatus.IMPORTED.value}', '{OrderStatus.ALREADY_IMPORTED.value}') AND id_comanda IS NOT NULL AND factura_numar IS NOT NULL AND factura_numar != '' """) @@ -939,9 +940,9 @@ async def get_all_imported_orders() -> list: """Get ALL imported orders with id_comanda (for checking if deleted in ROA).""" db = await get_sqlite() try: - cursor = await db.execute(""" + cursor = await db.execute(f""" SELECT order_number, id_comanda FROM orders - WHERE status IN ('IMPORTED', 'ALREADY_IMPORTED') + WHERE status IN ('{OrderStatus.IMPORTED.value}', '{OrderStatus.ALREADY_IMPORTED.value}') AND id_comanda IS NOT NULL """) rows = await cursor.fetchall() @@ -976,9 +977,9 @@ async def mark_order_deleted_in_roa(order_number: str): db = await get_sqlite() try: await db.execute("DELETE FROM order_items WHERE order_number = ?", (order_number,)) - await db.execute(""" + await db.execute(f""" UPDATE orders SET - status = 'DELETED_IN_ROA', + status = '{OrderStatus.DELETED_IN_ROA.value}', id_comanda = NULL, id_partener = NULL, factura_serie = NULL, @@ -1001,9 +1002,9 @@ async def mark_order_cancelled(order_number: str, web_status: str = "Anulata"): """Mark an order as cancelled from GoMag. Clears id_comanda and invoice cache.""" db = await get_sqlite() try: - await db.execute(""" + await db.execute(f""" UPDATE orders SET - status = 'CANCELLED', + status = '{OrderStatus.CANCELLED.value}', id_comanda = NULL, id_partener = NULL, factura_serie = NULL, @@ -1055,11 +1056,11 @@ async def get_skipped_orders_with_sku(sku: str) -> list[str]: """Get order_numbers of SKIPPED orders that contain the given SKU.""" db = await get_sqlite() try: - cursor = await db.execute(""" + cursor = await db.execute(f""" SELECT DISTINCT oi.order_number FROM order_items oi JOIN orders o ON o.order_number = oi.order_number - WHERE oi.sku = ? AND o.status = 'SKIPPED' + WHERE oi.sku = ? AND o.status = '{OrderStatus.SKIPPED.value}' """, (sku,)) rows = await cursor.fetchall() return [row[0] for row in rows] @@ -1314,7 +1315,7 @@ async def get_orders_missing_anaf() -> list[dict]: WHERE cod_fiscal_roa IS NOT NULL AND cod_fiscal_roa != '' AND anaf_platitor_tva IS NULL - AND status IN ('IMPORTED', 'ALREADY_IMPORTED') + AND status IN ('{OrderStatus.IMPORTED.value}', '{OrderStatus.ALREADY_IMPORTED.value}') """) rows = await cursor.fetchall() return [dict(r) for r in rows] diff --git a/api/app/services/sync_service.py b/api/app/services/sync_service.py index 42b56b5..f05b5f1 100644 --- a/api/app/services/sync_service.py +++ b/api/app/services/sync_service.py @@ -16,6 +16,7 @@ def _now(): from . import order_reader, validation_service, import_service, sqlite_service, invoice_service, gomag_client, anaf_service from ..config import settings from .. import database +from ..constants import OrderStatus logger = logging.getLogger(__name__) @@ -166,20 +167,20 @@ async def _fix_stale_error_orders(existing_map: dict, run_id: str): db = await get_sqlite() try: cursor = await db.execute( - "SELECT order_number FROM orders WHERE status = 'ERROR'" + f"SELECT order_number FROM orders WHERE status = '{OrderStatus.ERROR.value}'" ) error_orders = [row["order_number"] for row in await cursor.fetchall()] fixed = 0 for order_number in error_orders: if order_number in existing_map: id_comanda = existing_map[order_number] - await db.execute(""" + await db.execute(f""" UPDATE orders SET - status = 'ALREADY_IMPORTED', + status = '{OrderStatus.ALREADY_IMPORTED.value}', id_comanda = ?, error_message = NULL, updated_at = datetime('now') - WHERE order_number = ? AND status = 'ERROR' + WHERE order_number = ? AND status = '{OrderStatus.ERROR.value}' """, (id_comanda, order_number)) fixed += 1 _log_line(run_id, f"#{order_number} → status corectat ERROR → ALREADY_IMPORTED (ID: {id_comanda})") @@ -293,7 +294,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None cancelled_batch.append({ "sync_run_id": run_id, "order_number": order.number, "order_date": order.date, "customer_name": customer, - "status": "CANCELLED", "status_at_run": "CANCELLED", + "status": OrderStatus.CANCELLED.value, "status_at_run": OrderStatus.CANCELLED.value, "id_comanda": None, "id_partener": None, "error_message": "Comanda anulata in GoMag", "missing_skus": None, @@ -320,7 +321,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None SELECT order_number, id_comanda FROM orders WHERE order_number IN ({placeholders}) AND id_comanda IS NOT NULL - AND status = 'CANCELLED' + AND status = '{OrderStatus.CANCELLED.value}' """, cancelled_numbers) previously_imported = [dict(r) for r in await cursor.fetchall()] finally: @@ -624,7 +625,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None already_batch.append({ "sync_run_id": run_id, "order_number": order.number, "order_date": order.date, "customer_name": customer, - "status": "ALREADY_IMPORTED", "status_at_run": "ALREADY_IMPORTED", + "status": OrderStatus.ALREADY_IMPORTED.value, "status_at_run": OrderStatus.ALREADY_IMPORTED.value, "id_comanda": id_comanda_roa, "id_partener": None, "error_message": None, "missing_skus": None, "items_count": len(order.items), @@ -736,7 +737,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None skipped_batch.append({ "sync_run_id": run_id, "order_number": order.number, "order_date": order.date, "customer_name": customer, - "status": "SKIPPED", "status_at_run": "SKIPPED", + "status": OrderStatus.SKIPPED.value, "status_at_run": OrderStatus.SKIPPED.value, "id_comanda": None, "id_partener": None, "error_message": None, "missing_skus": missing_skus, "items_count": len(order.items), @@ -901,7 +902,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None order_number=order.number, order_date=order.date, customer_name=customer, - status="IMPORTED", + status=OrderStatus.IMPORTED.value, id_comanda=result["id_comanda"], id_partener=result["id_partener"], items_count=len(order.items), @@ -915,7 +916,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None web_status=order.status or None, discount_split=discount_split_json, ) - await sqlite_service.add_sync_run_order(run_id, order.number, "IMPORTED") + await sqlite_service.add_sync_run_order(run_id, order.number, OrderStatus.IMPORTED.value) # Store ROA address IDs (R9) await sqlite_service.update_import_order_addresses( order.number, @@ -968,7 +969,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None order_number=order.number, order_date=order.date, customer_name=customer, - status="ERROR", + status=OrderStatus.ERROR.value, id_partener=result.get("id_partener"), error_message=result["error"], items_count=len(order.items), @@ -982,7 +983,7 @@ async def run_sync(id_pol: int = None, id_sectie: int = None, run_id: str = None web_status=order.status or None, discount_split=discount_split_json, ) - await sqlite_service.add_sync_run_order(run_id, order.number, "ERROR") + await sqlite_service.add_sync_run_order(run_id, order.number, OrderStatus.ERROR.value) await sqlite_service.add_order_items(order.number, order_items_data) _log_line(run_id, f"#{order.number} [{order.date or '?'}] {customer} → EROARE: {result['error']}") diff --git a/api/app/static/js/dashboard.js b/api/app/static/js/dashboard.js index b3267ce..b8c7c92 100644 --- a/api/app/static/js/dashboard.js +++ b/api/app/static/js/dashboard.js @@ -376,7 +376,7 @@ async function loadDashOrders() { ${fmtCost(o.delivery_cost)} ${fmtCost(o.discount_total)} ${orderTotal} - ${(o.status === 'IMPORTED' || o.status === 'ALREADY_IMPORTED') && !(o.invoice && o.invoice.facturat) ? '' : ''} + ${(o.status === ORDER_STATUS.IMPORTED || o.status === ORDER_STATUS.ALREADY_IMPORTED) && !(o.invoice && o.invoice.facturat) ? '' : ''} `; }).join(''); } @@ -409,12 +409,12 @@ async function loadDashOrders() { // Mobile segmented control renderMobileSegmented('dashMobileSeg', [ { label: 'Toate', count: c.total || 0, value: 'all', active: (activeStatus || 'all') === 'all', colorClass: 'fc-neutral' }, - { label: 'Imp.', count: c.imported_all || c.imported || 0, value: 'IMPORTED', active: activeStatus === 'IMPORTED', colorClass: 'fc-green' }, - { label: 'Omise', count: c.skipped || 0, value: 'SKIPPED', active: activeStatus === 'SKIPPED', colorClass: 'fc-yellow' }, - { label: 'Erori', count: c.error || c.errors || 0, value: 'ERROR', active: activeStatus === 'ERROR', colorClass: 'fc-red' }, + { label: 'Imp.', count: c.imported_all || c.imported || 0, value: ORDER_STATUS.IMPORTED, active: activeStatus === ORDER_STATUS.IMPORTED, colorClass: 'fc-green' }, + { label: 'Omise', count: c.skipped || 0, value: ORDER_STATUS.SKIPPED, active: activeStatus === ORDER_STATUS.SKIPPED, colorClass: 'fc-yellow' }, + { label: 'Erori', count: c.error || c.errors || 0, value: ORDER_STATUS.ERROR, active: activeStatus === ORDER_STATUS.ERROR, colorClass: 'fc-red' }, { label: 'Fact.', count: c.facturate || 0, value: 'INVOICED', active: activeStatus === 'INVOICED', colorClass: 'fc-green' }, { label: 'Nefact.', count: c.nefacturate || c.uninvoiced || 0, value: 'UNINVOICED', active: activeStatus === 'UNINVOICED', colorClass: 'fc-red' }, - { label: 'Anulate', count: c.cancelled || 0, value: 'CANCELLED', active: activeStatus === 'CANCELLED', colorClass: 'fc-dark' }, + { label: 'Anulate', count: c.cancelled || 0, value: ORDER_STATUS.CANCELLED, active: activeStatus === ORDER_STATUS.CANCELLED, colorClass: 'fc-dark' }, { label: 'Dif.', count: c.diffs || 0, value: 'DIFFS', active: activeStatus === 'DIFFS', colorClass: 'fc-orange' } ], (val) => { document.querySelectorAll('.filter-pill[data-status]').forEach(b => b.classList.remove('active')); @@ -496,10 +496,10 @@ function escHtml(s) { function statusLabelText(status) { switch ((status || '').toUpperCase()) { - case 'IMPORTED': return 'Importat'; - case 'ALREADY_IMPORTED': return 'Deja imp.'; - case 'SKIPPED': return 'Omis'; - case 'ERROR': return 'Eroare'; + case ORDER_STATUS.IMPORTED: return 'Importat'; + case ORDER_STATUS.ALREADY_IMPORTED: return 'Deja imp.'; + case ORDER_STATUS.SKIPPED: return 'Omis'; + case ORDER_STATUS.ERROR: return 'Eroare'; default: return esc(status); } } @@ -523,7 +523,7 @@ function diffDots(o, mobile) { } function invoiceDot(order) { - if (order.status !== 'IMPORTED' && order.status !== 'ALREADY_IMPORTED') return '–'; + if (order.status !== ORDER_STATUS.IMPORTED && order.status !== ORDER_STATUS.ALREADY_IMPORTED) return '–'; if (order.invoice && order.invoice.facturat) return ''; return ''; } diff --git a/api/app/static/js/logs.js b/api/app/static/js/logs.js index 7ac2e55..b0d3750 100644 --- a/api/app/static/js/logs.js +++ b/api/app/static/js/logs.js @@ -28,10 +28,10 @@ function runStatusBadge(status) { function logStatusText(status) { switch ((status || '').toUpperCase()) { - case 'IMPORTED': return 'Importat'; - case 'ALREADY_IMPORTED': return 'Deja imp.'; - case 'SKIPPED': return 'Omis'; - case 'ERROR': return 'Eroare'; + case ORDER_STATUS.IMPORTED: return 'Importat'; + case ORDER_STATUS.ALREADY_IMPORTED: return 'Deja imp.'; + case ORDER_STATUS.SKIPPED: return 'Omis'; + case ORDER_STATUS.ERROR: return 'Eroare'; default: return esc(status); } } @@ -144,9 +144,9 @@ async function loadRunOrders(runId, statusFilter, page) { if (orders.length === 0) { tbody.innerHTML = 'Nicio comanda'; } else { - const problemOrders = orders.filter(o => ['ERROR', 'SKIPPED'].includes(o.status)); - const okOrders = orders.filter(o => ['IMPORTED', 'ALREADY_IMPORTED'].includes(o.status)); - const otherOrders = orders.filter(o => !['ERROR', 'SKIPPED', 'IMPORTED', 'ALREADY_IMPORTED'].includes(o.status)); + const problemOrders = orders.filter(o => [ORDER_STATUS.ERROR, ORDER_STATUS.SKIPPED].includes(o.status)); + const okOrders = orders.filter(o => [ORDER_STATUS.IMPORTED, ORDER_STATUS.ALREADY_IMPORTED].includes(o.status)); + const otherOrders = orders.filter(o => ![ORDER_STATUS.ERROR, ORDER_STATUS.SKIPPED, ORDER_STATUS.IMPORTED, ORDER_STATUS.ALREADY_IMPORTED].includes(o.status)); function orderRow(o, i) { const dateStr = fmtDate(o.order_date); @@ -195,9 +195,9 @@ async function loadRunOrders(runId, statusFilter, page) { if (orders.length === 0) { mobileList.innerHTML = '
Nicio comanda
'; } else { - const problemOrders = orders.filter(o => ['ERROR', 'SKIPPED'].includes(o.status)); - const okOrders = orders.filter(o => ['IMPORTED', 'ALREADY_IMPORTED'].includes(o.status)); - const otherOrders = orders.filter(o => !['ERROR', 'SKIPPED', 'IMPORTED', 'ALREADY_IMPORTED'].includes(o.status)); + const problemOrders = orders.filter(o => [ORDER_STATUS.ERROR, ORDER_STATUS.SKIPPED].includes(o.status)); + const okOrders = orders.filter(o => [ORDER_STATUS.IMPORTED, ORDER_STATUS.ALREADY_IMPORTED].includes(o.status)); + const otherOrders = orders.filter(o => ![ORDER_STATUS.ERROR, ORDER_STATUS.SKIPPED, ORDER_STATUS.IMPORTED, ORDER_STATUS.ALREADY_IMPORTED].includes(o.status)); function mobileRow(o) { const d = o.order_date || ''; @@ -235,10 +235,10 @@ async function loadRunOrders(runId, statusFilter, page) { // Mobile segmented control renderMobileSegmented('logsMobileSeg', [ { label: 'Toate', count: counts.total || 0, value: 'all', active: currentFilter === 'all', colorClass: 'fc-neutral' }, - { label: 'Imp.', count: counts.imported || 0, value: 'IMPORTED', active: currentFilter === 'IMPORTED', colorClass: 'fc-green' }, - { label: 'Deja', count: counts.already_imported || 0, value: 'ALREADY_IMPORTED', active: currentFilter === 'ALREADY_IMPORTED', colorClass: 'fc-blue' }, - { label: 'Omise', count: counts.skipped || 0, value: 'SKIPPED', active: currentFilter === 'SKIPPED', colorClass: 'fc-yellow' }, - { label: 'Erori', count: counts.error || 0, value: 'ERROR', active: currentFilter === 'ERROR', colorClass: 'fc-red' } + { label: 'Imp.', count: counts.imported || 0, value: ORDER_STATUS.IMPORTED, active: currentFilter === ORDER_STATUS.IMPORTED, colorClass: 'fc-green' }, + { label: 'Deja', count: counts.already_imported || 0, value: ORDER_STATUS.ALREADY_IMPORTED, active: currentFilter === ORDER_STATUS.ALREADY_IMPORTED, colorClass: 'fc-blue' }, + { label: 'Omise', count: counts.skipped || 0, value: ORDER_STATUS.SKIPPED, active: currentFilter === ORDER_STATUS.SKIPPED, colorClass: 'fc-yellow' }, + { label: 'Erori', count: counts.error || 0, value: ORDER_STATUS.ERROR, active: currentFilter === ORDER_STATUS.ERROR, colorClass: 'fc-red' } ], (val) => filterOrders(val)); // Orders pagination diff --git a/api/app/static/js/shared.js b/api/app/static/js/shared.js index 04e7a92..344faed 100644 --- a/api/app/static/js/shared.js +++ b/api/app/static/js/shared.js @@ -11,6 +11,16 @@ }; })(); +// ── Order status constants (mirror of Python OrderStatus enum) ──────────── +const ORDER_STATUS = Object.freeze({ + IMPORTED: 'IMPORTED', + ALREADY_IMPORTED: 'ALREADY_IMPORTED', + SKIPPED: 'SKIPPED', + ERROR: 'ERROR', + CANCELLED: 'CANCELLED', + DELETED_IN_ROA: 'DELETED_IN_ROA', +}); + // ── HTML escaping ───────────────────────────────── function esc(s) { if (s == null) return ''; @@ -503,12 +513,12 @@ function fmtNum(v) { function orderStatusBadge(status) { switch ((status || '').toUpperCase()) { - case 'IMPORTED': return 'Importat'; - case 'ALREADY_IMPORTED': return 'Deja importat'; - case 'SKIPPED': return 'Omis'; - case 'ERROR': return 'Eroare'; - case 'CANCELLED': return 'Anulat'; - case 'DELETED_IN_ROA': return 'Sters din ROA'; + case ORDER_STATUS.IMPORTED: return 'Importat'; + case ORDER_STATUS.ALREADY_IMPORTED: return 'Deja importat'; + case ORDER_STATUS.SKIPPED: return 'Omis'; + case ORDER_STATUS.ERROR: return 'Eroare'; + case ORDER_STATUS.CANCELLED: return 'Anulat'; + case ORDER_STATUS.DELETED_IN_ROA: return 'Sters din ROA'; default: return `${esc(status)}`; } } @@ -844,7 +854,7 @@ async function renderOrderDetailModal(orderNumber, opts) { // Retry button (only for ERROR/SKIPPED orders) const retryBtn = document.getElementById('detailRetryBtn'); if (retryBtn) { - const canRetry = ['ERROR', 'SKIPPED', 'DELETED_IN_ROA'].includes((order.status || '').toUpperCase()); + const canRetry = [ORDER_STATUS.ERROR, ORDER_STATUS.SKIPPED, ORDER_STATUS.DELETED_IN_ROA].includes((order.status || '').toUpperCase()); retryBtn.style.display = canRetry ? '' : 'none'; if (canRetry) { retryBtn.onclick = async () => { @@ -879,7 +889,7 @@ async function renderOrderDetailModal(orderNumber, opts) { // Resync button (IMPORTED/ALREADY_IMPORTED only) const resyncBtn = document.getElementById('detailResyncBtn'); if (resyncBtn) { - const canResync = ['IMPORTED', 'ALREADY_IMPORTED'].includes((order.status || '').toUpperCase()); + const canResync = [ORDER_STATUS.IMPORTED, ORDER_STATUS.ALREADY_IMPORTED].includes((order.status || '').toUpperCase()); resyncBtn.style.display = canResync ? '' : 'none'; if (canResync) { const isInvoiced = !!(order.factura_numar); @@ -930,7 +940,7 @@ async function renderOrderDetailModal(orderNumber, opts) { // Delete button (IMPORTED/ALREADY_IMPORTED only) const deleteBtn = document.getElementById('detailDeleteBtn'); if (deleteBtn) { - const canDelete = ['IMPORTED', 'ALREADY_IMPORTED'].includes((order.status || '').toUpperCase()); + const canDelete = [ORDER_STATUS.IMPORTED, ORDER_STATUS.ALREADY_IMPORTED].includes((order.status || '').toUpperCase()); deleteBtn.style.display = canDelete ? '' : 'none'; if (canDelete) { const isInvoiced = !!(order.factura_numar); @@ -1015,20 +1025,20 @@ function inlineConfirmAction(btn, confirmText, actionFn, opts) { // ── Dot helper ──────────────────────────────────── function statusDot(status) { switch ((status || '').toUpperCase()) { - case 'IMPORTED': - case 'ALREADY_IMPORTED': + case ORDER_STATUS.IMPORTED: + case ORDER_STATUS.ALREADY_IMPORTED: case 'COMPLETED': case 'RESOLVED': return ''; - case 'SKIPPED': + case ORDER_STATUS.SKIPPED: case 'UNRESOLVED': case 'INCOMPLETE': return ''; - case 'ERROR': + case ORDER_STATUS.ERROR: case 'FAILED': return ''; - case 'CANCELLED': - case 'DELETED_IN_ROA': + case ORDER_STATUS.CANCELLED: + case ORDER_STATUS.DELETED_IN_ROA: return ''; default: return ''; @@ -1168,7 +1178,7 @@ function _renderHeaderInfo(order) { } // ERROR orders: muted dashes for ROA fields - if (order.status === 'ERROR' && !order.id_comanda) { + if (order.status === ORDER_STATUS.ERROR && !order.id_comanda) { document.getElementById('detailIdComanda').innerHTML = '\u2014'; document.getElementById('detailIdPartener').innerHTML = '\u2014'; } diff --git a/api/app/templates/dashboard.html b/api/app/templates/dashboard.html index b1b1e61..23f4577 100644 --- a/api/app/templates/dashboard.html +++ b/api/app/templates/dashboard.html @@ -70,12 +70,12 @@ - - - + + + - + diff --git a/api/app/templates/logs.html b/api/app/templates/logs.html index 0ab5567..6128441 100644 --- a/api/app/templates/logs.html +++ b/api/app/templates/logs.html @@ -59,10 +59,10 @@
- - - - + + + +
diff --git a/api/tests/test_business_rules.py b/api/tests/test_business_rules.py index 9ef2182..5cf35b0 100644 --- a/api/tests/test_business_rules.py +++ b/api/tests/test_business_rules.py @@ -36,6 +36,7 @@ from unittest.mock import MagicMock, patch from app.services.import_service import build_articles_json, compute_discount_split from app.services.order_reader import OrderData, OrderItem +from app.constants import OrderStatus # --------------------------------------------------------------------------- @@ -857,14 +858,14 @@ class TestRefreshOrderAddress: def test_null_address_ids_returns_422(self, client, db): """Orders without Oracle address IDs return 422.""" - db.execute("INSERT OR IGNORE INTO orders (order_number, status) VALUES ('test-no-addr', 'IMPORTED')") + db.execute(f"INSERT OR IGNORE INTO orders (order_number, status) VALUES ('test-no-addr', '{OrderStatus.IMPORTED.value}')") db.commit() res = client.post("/api/orders/test-no-addr/refresh-address") assert res.status_code == 422 def test_oracle_unavailable_returns_503(self, client, db, monkeypatch): """Oracle connection failure returns 503.""" - db.execute("INSERT OR IGNORE INTO orders (order_number, status, id_adresa_livrare) VALUES ('test-oracle-fail', 'IMPORTED', 4116)") + db.execute(f"INSERT OR IGNORE INTO orders (order_number, status, id_adresa_livrare) VALUES ('test-oracle-fail', '{OrderStatus.IMPORTED.value}', 4116)") db.commit() import asyncio as _asyncio @@ -878,7 +879,7 @@ class TestRefreshOrderAddress: def test_refresh_returns_8_fields(self, client, db, monkeypatch): """Successful refresh returns 8-field address dict.""" - db.execute("INSERT OR IGNORE INTO orders (order_number, status, id_adresa_livrare) VALUES ('test-refresh-ok', 'IMPORTED', 4116)") + db.execute(f"INSERT OR IGNORE INTO orders (order_number, status, id_adresa_livrare) VALUES ('test-refresh-ok', '{OrderStatus.IMPORTED.value}', 4116)") db.commit() mock_result = ( @@ -908,7 +909,7 @@ class TestRefreshOrderAddress: from unittest.mock import AsyncMock # noqa: E402 (already imported MagicMock/patch above) -def _make_order_detail(status='IMPORTED', id_comanda=12345, factura_numar=None): +def _make_order_detail(status=OrderStatus.IMPORTED.value, id_comanda=12345, factura_numar=None): return { "order": { "order_number": "1001", @@ -983,7 +984,7 @@ class TestResyncDeleteSafetyGates: from app.services import retry_service with patch('app.services.sqlite_service.get_order_detail', - new=AsyncMock(return_value=_make_order_detail(status='ERROR'))), \ + new=AsyncMock(return_value=_make_order_detail(status=OrderStatus.ERROR.value))), \ patch('app.services.sync_service._sync_lock', new=_unlocked_lock()): result = await retry_service.resync_single_order("1001", {}) @@ -1051,7 +1052,7 @@ class TestResyncDeleteHappyPaths: from app.services import retry_service with patch('app.services.sqlite_service.get_order_detail', - new=AsyncMock(return_value=_make_order_detail(status='DELETED_IN_ROA'))), \ + new=AsyncMock(return_value=_make_order_detail(status=OrderStatus.DELETED_IN_ROA.value))), \ patch('app.services.sync_service._sync_lock', new=_unlocked_lock()), \ patch('app.services.retry_service._download_and_reimport', new=AsyncMock(return_value={"success": True, "message": "ok"})): diff --git a/api/tests/test_order_items_overwrite.py b/api/tests/test_order_items_overwrite.py index 2889e68..fb47692 100644 --- a/api/tests/test_order_items_overwrite.py +++ b/api/tests/test_order_items_overwrite.py @@ -35,6 +35,7 @@ if _api_dir not in sys.path: from app import database from app.services import sqlite_service +from app.constants import OrderStatus @pytest.fixture(autouse=True) @@ -69,7 +70,7 @@ async def _seed_order(order_number="TEST-001"): order_number=order_number, order_date="2026-01-01", customer_name="Test", - status="IMPORTED", + status=OrderStatus.IMPORTED.value, ) @@ -192,5 +193,5 @@ async def test_mark_order_deleted_removes_items(): finally: await db.close() assert row is not None - assert row["status"] == "DELETED_IN_ROA" + assert row["status"] == OrderStatus.DELETED_IN_ROA.value assert row["id_comanda"] is None diff --git a/api/tests/test_requirements.py b/api/tests/test_requirements.py index 1f6e248..c98542a 100644 --- a/api/tests/test_requirements.py +++ b/api/tests/test_requirements.py @@ -36,6 +36,7 @@ import pytest_asyncio from app.database import init_sqlite from app.services import sqlite_service +from app.constants import OrderStatus # Initialize SQLite once before any tests run init_sqlite() @@ -70,10 +71,10 @@ def seed_baseline_data(): # Add the first order (IMPORTED) with items await sqlite_service.upsert_order( - "RUN001", "ORD001", "2025-01-15", "Test Client", "IMPORTED", + "RUN001", "ORD001", "2025-01-15", "Test Client", OrderStatus.IMPORTED.value, id_comanda=100, id_partener=200, items_count=2 ) - await sqlite_service.add_sync_run_order("RUN001", "ORD001", "IMPORTED") + await sqlite_service.add_sync_run_order("RUN001", "ORD001", OrderStatus.IMPORTED.value) items = [ { @@ -103,15 +104,15 @@ def seed_baseline_data(): # Add more orders for filter tests await sqlite_service.upsert_order( - "RUN001", "ORD002", "2025-01-16", "Client 2", "SKIPPED", + "RUN001", "ORD002", "2025-01-16", "Client 2", OrderStatus.SKIPPED.value, missing_skus=["SKU99"], items_count=1 ) - await sqlite_service.add_sync_run_order("RUN001", "ORD002", "SKIPPED") + await sqlite_service.add_sync_run_order("RUN001", "ORD002", OrderStatus.SKIPPED.value) await sqlite_service.upsert_order( - "RUN001", "ORD003", "2025-01-17", "Client 3", "ERROR", + "RUN001", "ORD003", "2025-01-17", "Client 3", OrderStatus.ERROR.value, error_message="Test error", items_count=3 ) - await sqlite_service.add_sync_run_order("RUN001", "ORD003", "ERROR") + await sqlite_service.add_sync_run_order("RUN001", "ORD003", OrderStatus.ERROR.value) asyncio.run(_seed()) yield @@ -212,7 +213,7 @@ async def test_get_order_detail_not_found(): async def test_get_order_detail_status(): """Seeded ORD001 should have IMPORTED status.""" detail = await sqlite_service.get_order_detail("ORD001") - assert detail["order"]["status"] == "IMPORTED" + assert detail["order"]["status"] == OrderStatus.IMPORTED.value # --------------------------------------------------------------------------- @@ -232,7 +233,7 @@ async def test_get_run_orders_filtered_all(): @pytest.mark.asyncio async def test_get_run_orders_filtered_imported(): """Filter IMPORTED should return only ORD001.""" - result = await sqlite_service.get_run_orders_filtered("RUN001", "IMPORTED", 1, 50) + result = await sqlite_service.get_run_orders_filtered("RUN001", OrderStatus.IMPORTED.value, 1, 50) assert result["total"] == 1 assert result["orders"][0]["order_number"] == "ORD001" @@ -240,7 +241,7 @@ async def test_get_run_orders_filtered_imported(): @pytest.mark.asyncio async def test_get_run_orders_filtered_skipped(): """Filter SKIPPED should return only ORD002.""" - result = await sqlite_service.get_run_orders_filtered("RUN001", "SKIPPED", 1, 50) + result = await sqlite_service.get_run_orders_filtered("RUN001", OrderStatus.SKIPPED.value, 1, 50) assert result["total"] == 1 assert result["orders"][0]["order_number"] == "ORD002" @@ -248,7 +249,7 @@ async def test_get_run_orders_filtered_skipped(): @pytest.mark.asyncio async def test_get_run_orders_filtered_error(): """Filter ERROR should return only ORD003.""" - result = await sqlite_service.get_run_orders_filtered("RUN001", "ERROR", 1, 50) + result = await sqlite_service.get_run_orders_filtered("RUN001", OrderStatus.ERROR.value, 1, 50) assert result["total"] == 1 assert result["orders"][0]["order_number"] == "ORD003" @@ -360,10 +361,10 @@ def test_api_sync_run_orders(client): def test_api_sync_run_orders_filtered(client): """R1: Filtering by status=IMPORTED returns only IMPORTED orders.""" - resp = client.get("/api/sync/run/RUN001/orders?status=IMPORTED") + resp = client.get(f"/api/sync/run/RUN001/orders?status={OrderStatus.IMPORTED.value}") assert resp.status_code == 200 data = resp.json() - assert all(o["status"] == "IMPORTED" for o in data["orders"]) + assert all(o["status"] == OrderStatus.IMPORTED.value for o in data["orders"]) def test_api_sync_run_orders_pagination_fields(client):