wip: update Letzshop service and API for historical imports

- Add historical order import functionality
- Add order detail page route
- Update API endpoints for order confirmation flow

Note: These files need further updates to use the new unified order model.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2025-12-19 21:18:39 +01:00
parent 473a4fabfc
commit fceaba703e
3 changed files with 338 additions and 55 deletions

View File

@@ -11,7 +11,7 @@ Provides admin-level management of:
import logging
from fastapi import APIRouter, Depends, Path, Query
from fastapi import APIRouter, BackgroundTasks, Depends, Path, Query
from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api
@@ -25,6 +25,7 @@ from app.services.letzshop import (
OrderNotFoundError,
VendorNotFoundError,
)
from app.tasks.letzshop_tasks import process_historical_import
from models.database.user import User
from models.schema.letzshop import (
FulfillmentOperationResponse,
@@ -33,8 +34,11 @@ from models.schema.letzshop import (
LetzshopCredentialsCreate,
LetzshopCredentialsResponse,
LetzshopCredentialsUpdate,
LetzshopHistoricalImportJobResponse,
LetzshopHistoricalImportStartResponse,
LetzshopJobItem,
LetzshopJobsListResponse,
LetzshopOrderDetailResponse,
LetzshopOrderListResponse,
LetzshopOrderResponse,
LetzshopSuccessResponse,
@@ -345,6 +349,12 @@ def list_vendor_letzshop_orders(
skip: int = Query(0, ge=0),
limit: int = Query(50, ge=1, le=200),
sync_status: str | None = Query(None, description="Filter by sync status"),
has_declined_items: bool | None = Query(
None, description="Filter orders with declined/unavailable items"
),
search: str | None = Query(
None, description="Search by order number, customer name, or email"
),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
@@ -361,6 +371,8 @@ def list_vendor_letzshop_orders(
skip=skip,
limit=limit,
sync_status=sync_status,
has_declined_items=has_declined_items,
search=search,
)
# Get order stats for all statuses
@@ -377,6 +389,9 @@ def list_vendor_letzshop_orders(
letzshop_state=order.letzshop_state,
customer_email=order.customer_email,
customer_name=order.customer_name,
customer_locale=order.customer_locale,
shipping_country_iso=order.shipping_country_iso,
billing_country_iso=order.billing_country_iso,
total_amount=order.total_amount,
currency=order.currency,
local_order_id=order.local_order_id,
@@ -389,6 +404,7 @@ def list_vendor_letzshop_orders(
tracking_number=order.tracking_number,
tracking_carrier=order.tracking_carrier,
inventory_units=order.inventory_units,
order_date=order.order_date,
created_at=order.created_at,
updated_at=order.updated_at,
)
@@ -401,6 +417,53 @@ def list_vendor_letzshop_orders(
)
@router.get(
"/orders/{order_id}",
response_model=LetzshopOrderDetailResponse,
)
def get_letzshop_order_detail(
order_id: int = Path(..., description="Letzshop order ID"),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""Get detailed information for a single Letzshop order."""
order_service = get_order_service(db)
order = order_service.get_order_by_id(order_id)
if not order:
raise ResourceNotFoundException("Order", str(order_id))
return LetzshopOrderDetailResponse(
id=order.id,
vendor_id=order.vendor_id,
letzshop_order_id=order.letzshop_order_id,
letzshop_shipment_id=order.letzshop_shipment_id,
letzshop_order_number=order.letzshop_order_number,
letzshop_state=order.letzshop_state,
customer_email=order.customer_email,
customer_name=order.customer_name,
customer_locale=order.customer_locale,
shipping_country_iso=order.shipping_country_iso,
billing_country_iso=order.billing_country_iso,
total_amount=order.total_amount,
currency=order.currency,
local_order_id=order.local_order_id,
sync_status=order.sync_status,
last_synced_at=order.last_synced_at,
sync_error=order.sync_error,
confirmed_at=order.confirmed_at,
rejected_at=order.rejected_at,
tracking_set_at=order.tracking_set_at,
tracking_number=order.tracking_number,
tracking_carrier=order.tracking_carrier,
inventory_units=order.inventory_units,
order_date=order.order_date,
created_at=order.created_at,
updated_at=order.updated_at,
raw_order_data=order.raw_order_data,
)
@router.post(
"/vendors/{vendor_id}/sync",
response_model=LetzshopSyncTriggerResponse,
@@ -543,22 +606,21 @@ def list_vendor_letzshop_jobs(
@router.post(
"/vendors/{vendor_id}/import-history",
response_model=LetzshopHistoricalImportStartResponse,
)
def import_historical_orders(
def start_historical_import(
vendor_id: int = Path(..., description="Vendor ID"),
state: str = Query("confirmed", description="Shipment state to import"),
max_pages: int | None = Query(None, ge=1, le=100, description="Max pages to fetch"),
match_products: bool = Query(True, description="Match EANs to local products"),
background_tasks: BackgroundTasks = None,
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Import historical orders from Letzshop.
Start historical order import from Letzshop as a background job.
Fetches all shipments with the specified state (default: confirmed)
and imports them into the database. Supports pagination and EAN matching.
Creates a job that imports both confirmed and declined orders with
real-time progress tracking. Poll the status endpoint to track progress.
Returns statistics on imported/updated/skipped orders and product matching.
Returns a job_id for polling the status endpoint.
"""
order_service = get_order_service(db)
creds_service = get_credentials_service(db)
@@ -576,51 +638,55 @@ def import_historical_orders(
f"Letzshop credentials not configured for vendor {vendor.name}"
)
# Fetch all shipments with pagination
try:
with creds_service.create_client(vendor_id) as client:
logger.info(
f"Starting historical import for vendor {vendor_id}, state={state}, max_pages={max_pages}"
)
# Check if there's already a running import for this vendor
existing_job = order_service.get_running_historical_import_job(vendor_id)
if existing_job:
raise ValidationException(
f"Historical import already in progress (job_id={existing_job.id})"
)
shipments = client.get_all_shipments_paginated(
state=state,
page_size=50,
max_pages=max_pages,
)
# Create job record
job = order_service.create_historical_import_job(vendor_id, current_admin.id)
logger.info(f"Fetched {len(shipments)} {state} shipments from Letzshop")
logger.info(f"Created historical import job {job.id} for vendor {vendor_id}")
# Import shipments
stats = order_service.import_historical_shipments(
vendor_id=vendor_id,
shipments=shipments,
match_products=match_products,
)
# Queue background task
background_tasks.add_task(
process_historical_import,
job.id,
vendor_id,
)
db.commit()
return LetzshopHistoricalImportStartResponse(
job_id=job.id,
status="pending",
message="Historical import job started",
)
# Update sync status
creds_service.update_sync_status(
vendor_id,
"success",
None,
)
logger.info(
f"Historical import completed: {stats['imported']} imported, "
f"{stats['updated']} updated, {stats['skipped']} skipped"
)
@router.get(
"/vendors/{vendor_id}/import-history/{job_id}/status",
response_model=LetzshopHistoricalImportJobResponse,
)
def get_historical_import_status(
vendor_id: int = Path(..., description="Vendor ID"),
job_id: int = Path(..., description="Import job ID"),
db: Session = Depends(get_db),
current_admin: User = Depends(get_current_admin_api),
):
"""
Get status of a historical import job.
return {
"success": True,
"message": f"Historical import completed: {stats['imported']} imported, {stats['updated']} updated",
"statistics": stats,
}
Poll this endpoint to track import progress. Returns current phase,
page being fetched, and counts of processed/imported/updated orders.
"""
order_service = get_order_service(db)
job = order_service.get_historical_import_job_by_id(vendor_id, job_id)
except LetzshopClientError as e:
creds_service.update_sync_status(vendor_id, "failed", str(e))
raise ValidationException(f"Letzshop API error: {e}")
if not job:
raise ResourceNotFoundException("HistoricalImportJob", str(job_id))
return LetzshopHistoricalImportJobResponse.model_validate(job)
@router.get(