docs: update Letzshop order import documentation
- Update implementation guide with unified order approach - Add mkdocs navigation entry - Add background task for order sync - Add debug script for historical imports 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
235
app/tasks/letzshop_tasks.py
Normal file
235
app/tasks/letzshop_tasks.py
Normal file
@@ -0,0 +1,235 @@
|
||||
# app/tasks/letzshop_tasks.py
|
||||
"""Background tasks for Letzshop historical order imports."""
|
||||
|
||||
import logging
|
||||
from datetime import UTC, datetime
|
||||
from typing import Callable
|
||||
|
||||
from app.core.database import SessionLocal
|
||||
from app.services.letzshop import LetzshopClientError
|
||||
from app.services.letzshop.credentials_service import LetzshopCredentialsService
|
||||
from app.services.letzshop.order_service import LetzshopOrderService
|
||||
from models.database.letzshop import LetzshopHistoricalImportJob
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _get_credentials_service(db) -> LetzshopCredentialsService:
|
||||
"""Create a credentials service instance."""
|
||||
return LetzshopCredentialsService(db)
|
||||
|
||||
|
||||
def _get_order_service(db) -> LetzshopOrderService:
|
||||
"""Create an order service instance."""
|
||||
return LetzshopOrderService(db)
|
||||
|
||||
|
||||
def process_historical_import(job_id: int, vendor_id: int):
|
||||
"""
|
||||
Background task for historical order import with progress tracking.
|
||||
|
||||
Imports both confirmed and declined orders from Letzshop API,
|
||||
updating job progress in the database for frontend polling.
|
||||
|
||||
Args:
|
||||
job_id: ID of the LetzshopHistoricalImportJob record
|
||||
vendor_id: ID of the vendor to import orders for
|
||||
"""
|
||||
db = SessionLocal()
|
||||
job = None
|
||||
|
||||
try:
|
||||
# Get the import job
|
||||
job = (
|
||||
db.query(LetzshopHistoricalImportJob)
|
||||
.filter(LetzshopHistoricalImportJob.id == job_id)
|
||||
.first()
|
||||
)
|
||||
if not job:
|
||||
logger.error(f"Historical import job {job_id} not found")
|
||||
return
|
||||
|
||||
# Mark as started
|
||||
job.status = "fetching"
|
||||
job.started_at = datetime.now(UTC)
|
||||
db.commit()
|
||||
|
||||
creds_service = _get_credentials_service(db)
|
||||
order_service = _get_order_service(db)
|
||||
|
||||
# Create progress callback for fetching
|
||||
def fetch_progress_callback(page: int, total_fetched: int):
|
||||
"""Update fetch progress in database."""
|
||||
job.current_page = page
|
||||
job.shipments_fetched = total_fetched
|
||||
db.commit()
|
||||
|
||||
# Create progress callback for processing
|
||||
def create_processing_callback(
|
||||
phase: str,
|
||||
) -> Callable[[int, int, int, int], None]:
|
||||
"""Create a processing progress callback for a phase."""
|
||||
|
||||
def callback(processed: int, imported: int, updated: int, skipped: int):
|
||||
job.orders_processed = processed
|
||||
job.orders_imported = imported
|
||||
job.orders_updated = updated
|
||||
job.orders_skipped = skipped
|
||||
db.commit()
|
||||
|
||||
return callback
|
||||
|
||||
with creds_service.create_client(vendor_id) as client:
|
||||
# ================================================================
|
||||
# Phase 1: Import confirmed orders
|
||||
# ================================================================
|
||||
job.current_phase = "confirmed"
|
||||
job.current_page = 0
|
||||
job.shipments_fetched = 0
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Job {job_id}: Fetching confirmed shipments for vendor {vendor_id}")
|
||||
|
||||
confirmed_shipments = client.get_all_shipments_paginated(
|
||||
state="confirmed",
|
||||
page_size=50,
|
||||
progress_callback=fetch_progress_callback,
|
||||
)
|
||||
|
||||
logger.info(f"Job {job_id}: Fetched {len(confirmed_shipments)} confirmed shipments")
|
||||
|
||||
# Process confirmed shipments
|
||||
job.status = "processing"
|
||||
job.orders_processed = 0
|
||||
job.orders_imported = 0
|
||||
job.orders_updated = 0
|
||||
job.orders_skipped = 0
|
||||
db.commit()
|
||||
|
||||
confirmed_stats = order_service.import_historical_shipments(
|
||||
vendor_id=vendor_id,
|
||||
shipments=confirmed_shipments,
|
||||
match_products=True,
|
||||
progress_callback=create_processing_callback("confirmed"),
|
||||
)
|
||||
|
||||
# Store confirmed stats
|
||||
job.confirmed_stats = {
|
||||
"total": confirmed_stats["total"],
|
||||
"imported": confirmed_stats["imported"],
|
||||
"updated": confirmed_stats["updated"],
|
||||
"skipped": confirmed_stats["skipped"],
|
||||
"products_matched": confirmed_stats["products_matched"],
|
||||
"products_not_found": confirmed_stats["products_not_found"],
|
||||
}
|
||||
job.products_matched = confirmed_stats["products_matched"]
|
||||
job.products_not_found = confirmed_stats["products_not_found"]
|
||||
db.commit()
|
||||
|
||||
logger.info(
|
||||
f"Job {job_id}: Confirmed phase complete - "
|
||||
f"imported={confirmed_stats['imported']}, "
|
||||
f"updated={confirmed_stats['updated']}, "
|
||||
f"skipped={confirmed_stats['skipped']}"
|
||||
)
|
||||
|
||||
# ================================================================
|
||||
# Phase 2: Import unconfirmed (pending) orders
|
||||
# Note: Letzshop API has no "declined" state. Declined items
|
||||
# are tracked at the inventory unit level, not shipment level.
|
||||
# Valid states: unconfirmed, confirmed, completed, accepted
|
||||
# ================================================================
|
||||
job.current_phase = "unconfirmed"
|
||||
job.status = "fetching"
|
||||
job.current_page = 0
|
||||
job.shipments_fetched = 0
|
||||
db.commit()
|
||||
|
||||
logger.info(f"Job {job_id}: Fetching unconfirmed shipments for vendor {vendor_id}")
|
||||
|
||||
unconfirmed_shipments = client.get_all_shipments_paginated(
|
||||
state="unconfirmed",
|
||||
page_size=50,
|
||||
progress_callback=fetch_progress_callback,
|
||||
)
|
||||
|
||||
logger.info(f"Job {job_id}: Fetched {len(unconfirmed_shipments)} unconfirmed shipments")
|
||||
|
||||
# Process unconfirmed shipments
|
||||
job.status = "processing"
|
||||
job.orders_processed = 0
|
||||
db.commit()
|
||||
|
||||
unconfirmed_stats = order_service.import_historical_shipments(
|
||||
vendor_id=vendor_id,
|
||||
shipments=unconfirmed_shipments,
|
||||
match_products=True,
|
||||
progress_callback=create_processing_callback("unconfirmed"),
|
||||
)
|
||||
|
||||
# Store unconfirmed stats (in declined_stats field for compatibility)
|
||||
job.declined_stats = {
|
||||
"total": unconfirmed_stats["total"],
|
||||
"imported": unconfirmed_stats["imported"],
|
||||
"updated": unconfirmed_stats["updated"],
|
||||
"skipped": unconfirmed_stats["skipped"],
|
||||
"products_matched": unconfirmed_stats["products_matched"],
|
||||
"products_not_found": unconfirmed_stats["products_not_found"],
|
||||
}
|
||||
|
||||
# Add to cumulative product matching stats
|
||||
job.products_matched += unconfirmed_stats["products_matched"]
|
||||
job.products_not_found += unconfirmed_stats["products_not_found"]
|
||||
|
||||
logger.info(
|
||||
f"Job {job_id}: Unconfirmed phase complete - "
|
||||
f"imported={unconfirmed_stats['imported']}, "
|
||||
f"updated={unconfirmed_stats['updated']}, "
|
||||
f"skipped={unconfirmed_stats['skipped']}"
|
||||
)
|
||||
|
||||
# ================================================================
|
||||
# Complete
|
||||
# ================================================================
|
||||
job.status = "completed"
|
||||
job.completed_at = datetime.now(UTC)
|
||||
db.commit()
|
||||
|
||||
# Update credentials sync status
|
||||
creds_service.update_sync_status(vendor_id, "success", None)
|
||||
|
||||
logger.info(f"Job {job_id}: Historical import completed successfully")
|
||||
|
||||
except LetzshopClientError as e:
|
||||
logger.error(f"Job {job_id}: Letzshop API error: {e}")
|
||||
if job is not None:
|
||||
try:
|
||||
job.status = "failed"
|
||||
job.error_message = f"Letzshop API error: {e}"
|
||||
job.completed_at = datetime.now(UTC)
|
||||
db.commit()
|
||||
|
||||
creds_service = _get_credentials_service(db)
|
||||
creds_service.update_sync_status(vendor_id, "failed", str(e))
|
||||
except Exception as commit_error:
|
||||
logger.error(f"Job {job_id}: Failed to update job status: {commit_error}")
|
||||
db.rollback()
|
||||
|
||||
except Exception as e:
|
||||
logger.error(f"Job {job_id}: Unexpected error: {e}", exc_info=True)
|
||||
if job is not None:
|
||||
try:
|
||||
job.status = "failed"
|
||||
job.error_message = str(e)
|
||||
job.completed_at = datetime.now(UTC)
|
||||
db.commit()
|
||||
except Exception as commit_error:
|
||||
logger.error(f"Job {job_id}: Failed to update job status: {commit_error}")
|
||||
db.rollback()
|
||||
|
||||
finally:
|
||||
if hasattr(db, "close") and callable(db.close):
|
||||
try:
|
||||
db.close()
|
||||
except Exception as close_error:
|
||||
logger.error(f"Job {job_id}: Error closing database session: {close_error}")
|
||||
Reference in New Issue
Block a user