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:
@@ -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(
|
||||
|
||||
Reference in New Issue
Block a user