feat: add FastAPI admin dashboard with sync orchestration and test suite
Replace Flask admin with FastAPI app (api/app/) featuring: - Dashboard with stat cards, sync control, and history - Mappings CRUD for ARTICOLE_TERTI with CSV import/export - Article autocomplete from NOM_ARTICOLE - SKU pre-validation before import - Sync orchestration: read JSONs -> validate -> import -> log to SQLite - APScheduler for periodic sync from UI - File logging to logs/sync_comenzi_YYYYMMDD_HHMMSS.log - Oracle pool None guard (503 vs 500 on unavailable) Test suite: - test_app_basic.py: 30 tests (imports + routes) without Oracle - test_integration.py: 9 integration tests with Oracle Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
206
api/app/services/sqlite_service.py
Normal file
206
api/app/services/sqlite_service.py
Normal file
@@ -0,0 +1,206 @@
|
||||
import json
|
||||
import logging
|
||||
from datetime import datetime
|
||||
from ..database import get_sqlite, get_sqlite_sync
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
async def create_sync_run(run_id: str, json_files: int = 0):
|
||||
"""Create a new sync run record."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
await db.execute("""
|
||||
INSERT INTO sync_runs (run_id, started_at, status, json_files)
|
||||
VALUES (?, datetime('now'), 'running', ?)
|
||||
""", (run_id, json_files))
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def update_sync_run(run_id: str, status: str, total_orders: int = 0,
|
||||
imported: int = 0, skipped: int = 0, errors: int = 0):
|
||||
"""Update sync run with results."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
await db.execute("""
|
||||
UPDATE sync_runs SET
|
||||
finished_at = datetime('now'),
|
||||
status = ?,
|
||||
total_orders = ?,
|
||||
imported = ?,
|
||||
skipped = ?,
|
||||
errors = ?
|
||||
WHERE run_id = ?
|
||||
""", (status, total_orders, imported, skipped, errors, run_id))
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def add_import_order(sync_run_id: str, order_number: str, order_date: str,
|
||||
customer_name: str, status: str, id_comanda: int = None,
|
||||
id_partener: int = None, error_message: str = None,
|
||||
missing_skus: list = None, items_count: int = 0):
|
||||
"""Record an individual order import result."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
await db.execute("""
|
||||
INSERT INTO import_orders
|
||||
(sync_run_id, order_number, order_date, customer_name, status,
|
||||
id_comanda, id_partener, error_message, missing_skus, items_count)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""", (sync_run_id, order_number, order_date, customer_name, status,
|
||||
id_comanda, id_partener, error_message,
|
||||
json.dumps(missing_skus) if missing_skus else None, items_count))
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def track_missing_sku(sku: str, product_name: str = ""):
|
||||
"""Track a missing SKU."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
await db.execute("""
|
||||
INSERT OR IGNORE INTO missing_skus (sku, product_name)
|
||||
VALUES (?, ?)
|
||||
""", (sku, product_name))
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def resolve_missing_sku(sku: str):
|
||||
"""Mark a missing SKU as resolved."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
await db.execute("""
|
||||
UPDATE missing_skus SET resolved = 1, resolved_at = datetime('now')
|
||||
WHERE sku = ?
|
||||
""", (sku,))
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def get_sync_runs(page: int = 1, per_page: int = 20):
|
||||
"""Get paginated sync run history."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
offset = (page - 1) * per_page
|
||||
|
||||
cursor = await db.execute("SELECT COUNT(*) FROM sync_runs")
|
||||
total = (await cursor.fetchone())[0]
|
||||
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM sync_runs
|
||||
ORDER BY started_at DESC
|
||||
LIMIT ? OFFSET ?
|
||||
""", (per_page, offset))
|
||||
rows = await cursor.fetchall()
|
||||
|
||||
return {
|
||||
"runs": [dict(row) for row in rows],
|
||||
"total": total,
|
||||
"page": page,
|
||||
"pages": (total + per_page - 1) // per_page if total > 0 else 0
|
||||
}
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def get_sync_run_detail(run_id: str):
|
||||
"""Get details for a specific sync run including its orders."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
cursor = await db.execute(
|
||||
"SELECT * FROM sync_runs WHERE run_id = ?", (run_id,)
|
||||
)
|
||||
run = await cursor.fetchone()
|
||||
if not run:
|
||||
return None
|
||||
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM import_orders
|
||||
WHERE sync_run_id = ?
|
||||
ORDER BY created_at
|
||||
""", (run_id,))
|
||||
orders = await cursor.fetchall()
|
||||
|
||||
return {
|
||||
"run": dict(run),
|
||||
"orders": [dict(o) for o in orders]
|
||||
}
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def get_dashboard_stats():
|
||||
"""Get stats for the dashboard."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
# Total imported
|
||||
cursor = await db.execute(
|
||||
"SELECT COUNT(*) FROM import_orders WHERE status = 'IMPORTED'"
|
||||
)
|
||||
imported = (await cursor.fetchone())[0]
|
||||
|
||||
# Total skipped
|
||||
cursor = await db.execute(
|
||||
"SELECT COUNT(*) FROM import_orders WHERE status = 'SKIPPED'"
|
||||
)
|
||||
skipped = (await cursor.fetchone())[0]
|
||||
|
||||
# Total errors
|
||||
cursor = await db.execute(
|
||||
"SELECT COUNT(*) FROM import_orders WHERE status = 'ERROR'"
|
||||
)
|
||||
errors = (await cursor.fetchone())[0]
|
||||
|
||||
# Missing SKUs (unresolved)
|
||||
cursor = await db.execute(
|
||||
"SELECT COUNT(*) FROM missing_skus WHERE resolved = 0"
|
||||
)
|
||||
missing = (await cursor.fetchone())[0]
|
||||
|
||||
# Last sync run
|
||||
cursor = await db.execute("""
|
||||
SELECT * FROM sync_runs ORDER BY started_at DESC LIMIT 1
|
||||
""")
|
||||
last_run = await cursor.fetchone()
|
||||
|
||||
return {
|
||||
"imported": imported,
|
||||
"skipped": skipped,
|
||||
"errors": errors,
|
||||
"missing_skus": missing,
|
||||
"last_run": dict(last_run) if last_run else None
|
||||
}
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def get_scheduler_config():
|
||||
"""Get scheduler configuration from SQLite."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
cursor = await db.execute("SELECT key, value FROM scheduler_config")
|
||||
rows = await cursor.fetchall()
|
||||
return {row["key"]: row["value"] for row in rows}
|
||||
finally:
|
||||
await db.close()
|
||||
|
||||
|
||||
async def set_scheduler_config(key: str, value: str):
|
||||
"""Set a scheduler configuration value."""
|
||||
db = await get_sqlite()
|
||||
try:
|
||||
await db.execute("""
|
||||
INSERT OR REPLACE INTO scheduler_config (key, value)
|
||||
VALUES (?, ?)
|
||||
""", (key, value))
|
||||
await db.commit()
|
||||
finally:
|
||||
await db.close()
|
||||
Reference in New Issue
Block a user