- Remove |safe from |tojson in HTML attributes (x-data) - quotes must become " for browsers to parse correctly - Update LANG-002 and LANG-003 architecture rules to document correct |tojson usage patterns: - HTML attributes: |tojson (no |safe) - Script blocks: |tojson|safe - Fix validator to warn when |tojson|safe is used in x-data (breaks HTML attribute parsing) - Improve code quality across services, APIs, and tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
747 lines
26 KiB
Python
747 lines
26 KiB
Python
# app/api/v1/vendor/letzshop.py
|
|
"""
|
|
Vendor API endpoints for Letzshop marketplace integration.
|
|
|
|
Provides vendor-level management of:
|
|
- Letzshop credentials
|
|
- Connection testing
|
|
- Order import and sync
|
|
- Fulfillment operations (confirm, reject, tracking)
|
|
|
|
Vendor Context: Uses token_vendor_id from JWT token.
|
|
"""
|
|
|
|
import logging
|
|
|
|
from fastapi import APIRouter, Depends, Path, Query
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.api.deps import get_current_vendor_api
|
|
from app.core.database import get_db
|
|
from app.exceptions import ResourceNotFoundException, ValidationException
|
|
from app.services.letzshop import (
|
|
CredentialsNotFoundError,
|
|
LetzshopClientError,
|
|
LetzshopCredentialsService,
|
|
LetzshopOrderService,
|
|
OrderNotFoundError,
|
|
)
|
|
from models.database.user import User
|
|
from models.schema.letzshop import (
|
|
FulfillmentConfirmRequest,
|
|
FulfillmentOperationResponse,
|
|
FulfillmentQueueItemResponse,
|
|
FulfillmentQueueListResponse,
|
|
FulfillmentRejectRequest,
|
|
FulfillmentTrackingRequest,
|
|
LetzshopConnectionTestRequest,
|
|
LetzshopConnectionTestResponse,
|
|
LetzshopCredentialsCreate,
|
|
LetzshopCredentialsResponse,
|
|
LetzshopCredentialsStatus,
|
|
LetzshopCredentialsUpdate,
|
|
LetzshopOrderDetailResponse,
|
|
LetzshopOrderListResponse,
|
|
LetzshopOrderResponse,
|
|
LetzshopSuccessResponse,
|
|
LetzshopSyncLogListResponse,
|
|
LetzshopSyncLogResponse,
|
|
LetzshopSyncTriggerRequest,
|
|
LetzshopSyncTriggerResponse,
|
|
)
|
|
|
|
router = APIRouter(prefix="/letzshop")
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
# ============================================================================
|
|
# Helper Functions
|
|
# ============================================================================
|
|
|
|
|
|
def get_order_service(db: Session) -> LetzshopOrderService:
|
|
"""Get order service instance."""
|
|
return LetzshopOrderService(db)
|
|
|
|
|
|
def get_credentials_service(db: Session) -> LetzshopCredentialsService:
|
|
"""Get credentials service instance."""
|
|
return LetzshopCredentialsService(db)
|
|
|
|
|
|
# ============================================================================
|
|
# Status & Configuration
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/status", response_model=LetzshopCredentialsStatus)
|
|
def get_letzshop_status(
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Get Letzshop integration status for the current vendor."""
|
|
creds_service = get_credentials_service(db)
|
|
status = creds_service.get_status(current_user.token_vendor_id)
|
|
return LetzshopCredentialsStatus(**status)
|
|
|
|
|
|
@router.get("/credentials", response_model=LetzshopCredentialsResponse)
|
|
def get_credentials(
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Get Letzshop credentials for the current vendor (API key is masked)."""
|
|
creds_service = get_credentials_service(db)
|
|
vendor_id = current_user.token_vendor_id
|
|
|
|
try:
|
|
credentials = creds_service.get_credentials_or_raise(vendor_id)
|
|
except CredentialsNotFoundError:
|
|
raise ResourceNotFoundException("LetzshopCredentials", str(vendor_id))
|
|
|
|
return LetzshopCredentialsResponse(
|
|
id=credentials.id,
|
|
vendor_id=credentials.vendor_id,
|
|
api_key_masked=creds_service.get_masked_api_key(vendor_id),
|
|
api_endpoint=credentials.api_endpoint,
|
|
auto_sync_enabled=credentials.auto_sync_enabled,
|
|
sync_interval_minutes=credentials.sync_interval_minutes,
|
|
last_sync_at=credentials.last_sync_at,
|
|
last_sync_status=credentials.last_sync_status,
|
|
last_sync_error=credentials.last_sync_error,
|
|
created_at=credentials.created_at,
|
|
updated_at=credentials.updated_at,
|
|
)
|
|
|
|
|
|
@router.post("/credentials", response_model=LetzshopCredentialsResponse)
|
|
def save_credentials(
|
|
credentials_data: LetzshopCredentialsCreate,
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Create or update Letzshop credentials for the current vendor."""
|
|
creds_service = get_credentials_service(db)
|
|
vendor_id = current_user.token_vendor_id
|
|
|
|
credentials = creds_service.upsert_credentials(
|
|
vendor_id=vendor_id,
|
|
api_key=credentials_data.api_key,
|
|
api_endpoint=credentials_data.api_endpoint,
|
|
auto_sync_enabled=credentials_data.auto_sync_enabled,
|
|
sync_interval_minutes=credentials_data.sync_interval_minutes,
|
|
)
|
|
db.commit()
|
|
|
|
logger.info(f"Vendor user {current_user.email} updated Letzshop credentials")
|
|
|
|
return LetzshopCredentialsResponse(
|
|
id=credentials.id,
|
|
vendor_id=credentials.vendor_id,
|
|
api_key_masked=creds_service.get_masked_api_key(vendor_id),
|
|
api_endpoint=credentials.api_endpoint,
|
|
auto_sync_enabled=credentials.auto_sync_enabled,
|
|
sync_interval_minutes=credentials.sync_interval_minutes,
|
|
last_sync_at=credentials.last_sync_at,
|
|
last_sync_status=credentials.last_sync_status,
|
|
last_sync_error=credentials.last_sync_error,
|
|
created_at=credentials.created_at,
|
|
updated_at=credentials.updated_at,
|
|
)
|
|
|
|
|
|
@router.patch("/credentials", response_model=LetzshopCredentialsResponse)
|
|
def update_credentials(
|
|
credentials_data: LetzshopCredentialsUpdate,
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Partially update Letzshop credentials for the current vendor."""
|
|
creds_service = get_credentials_service(db)
|
|
vendor_id = current_user.token_vendor_id
|
|
|
|
try:
|
|
credentials = creds_service.update_credentials(
|
|
vendor_id=vendor_id,
|
|
api_key=credentials_data.api_key,
|
|
api_endpoint=credentials_data.api_endpoint,
|
|
auto_sync_enabled=credentials_data.auto_sync_enabled,
|
|
sync_interval_minutes=credentials_data.sync_interval_minutes,
|
|
)
|
|
db.commit()
|
|
except CredentialsNotFoundError:
|
|
raise ResourceNotFoundException("LetzshopCredentials", str(vendor_id))
|
|
|
|
return LetzshopCredentialsResponse(
|
|
id=credentials.id,
|
|
vendor_id=credentials.vendor_id,
|
|
api_key_masked=creds_service.get_masked_api_key(vendor_id),
|
|
api_endpoint=credentials.api_endpoint,
|
|
auto_sync_enabled=credentials.auto_sync_enabled,
|
|
sync_interval_minutes=credentials.sync_interval_minutes,
|
|
last_sync_at=credentials.last_sync_at,
|
|
last_sync_status=credentials.last_sync_status,
|
|
last_sync_error=credentials.last_sync_error,
|
|
created_at=credentials.created_at,
|
|
updated_at=credentials.updated_at,
|
|
)
|
|
|
|
|
|
@router.delete("/credentials", response_model=LetzshopSuccessResponse)
|
|
def delete_credentials(
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Delete Letzshop credentials for the current vendor."""
|
|
creds_service = get_credentials_service(db)
|
|
|
|
deleted = creds_service.delete_credentials(current_user.token_vendor_id)
|
|
if not deleted:
|
|
raise ResourceNotFoundException(
|
|
"LetzshopCredentials", str(current_user.token_vendor_id)
|
|
)
|
|
db.commit()
|
|
|
|
logger.info(f"Vendor user {current_user.email} deleted Letzshop credentials")
|
|
return LetzshopSuccessResponse(success=True, message="Letzshop credentials deleted")
|
|
|
|
|
|
# ============================================================================
|
|
# Connection Testing
|
|
# ============================================================================
|
|
|
|
|
|
@router.post("/test", response_model=LetzshopConnectionTestResponse)
|
|
def test_connection(
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Test the Letzshop connection using stored credentials."""
|
|
creds_service = get_credentials_service(db)
|
|
|
|
success, response_time_ms, error = creds_service.test_connection(
|
|
current_user.token_vendor_id
|
|
)
|
|
|
|
return LetzshopConnectionTestResponse(
|
|
success=success,
|
|
message="Connection successful" if success else "Connection failed",
|
|
response_time_ms=response_time_ms,
|
|
error_details=error,
|
|
)
|
|
|
|
|
|
@router.post("/test-key", response_model=LetzshopConnectionTestResponse)
|
|
def test_api_key(
|
|
test_request: LetzshopConnectionTestRequest,
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Test a Letzshop API key without saving it."""
|
|
creds_service = get_credentials_service(db)
|
|
|
|
success, response_time_ms, error = creds_service.test_api_key(
|
|
api_key=test_request.api_key,
|
|
api_endpoint=test_request.api_endpoint,
|
|
)
|
|
|
|
return LetzshopConnectionTestResponse(
|
|
success=success,
|
|
message="Connection successful" if success else "Connection failed",
|
|
response_time_ms=response_time_ms,
|
|
error_details=error,
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Order Management
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/orders", response_model=LetzshopOrderListResponse)
|
|
def list_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"),
|
|
letzshop_state: str | None = Query(None, description="Filter by Letzshop state"),
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""List Letzshop orders for the current vendor."""
|
|
order_service = get_order_service(db)
|
|
vendor_id = current_user.token_vendor_id
|
|
|
|
orders, total = order_service.list_orders(
|
|
vendor_id=vendor_id,
|
|
skip=skip,
|
|
limit=limit,
|
|
sync_status=sync_status,
|
|
letzshop_state=letzshop_state,
|
|
)
|
|
|
|
return LetzshopOrderListResponse(
|
|
orders=[
|
|
LetzshopOrderResponse(
|
|
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,
|
|
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,
|
|
created_at=order.created_at,
|
|
updated_at=order.updated_at,
|
|
)
|
|
for order in orders
|
|
],
|
|
total=total,
|
|
skip=skip,
|
|
limit=limit,
|
|
)
|
|
|
|
|
|
@router.get("/orders/{order_id}", response_model=LetzshopOrderDetailResponse)
|
|
def get_order(
|
|
order_id: int = Path(..., description="Order ID"),
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Get a specific Letzshop order with full details."""
|
|
order_service = get_order_service(db)
|
|
|
|
try:
|
|
order = order_service.get_order_or_raise(current_user.token_vendor_id, order_id)
|
|
except OrderNotFoundError:
|
|
raise ResourceNotFoundException("LetzshopOrder", 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,
|
|
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,
|
|
raw_order_data=order.raw_order_data,
|
|
created_at=order.created_at,
|
|
updated_at=order.updated_at,
|
|
)
|
|
|
|
|
|
@router.post("/orders/import", response_model=LetzshopSyncTriggerResponse)
|
|
def import_orders(
|
|
sync_request: LetzshopSyncTriggerRequest = LetzshopSyncTriggerRequest(),
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Import new orders from Letzshop."""
|
|
vendor_id = current_user.token_vendor_id
|
|
order_service = get_order_service(db)
|
|
creds_service = get_credentials_service(db)
|
|
|
|
# Verify credentials exist
|
|
try:
|
|
creds_service.get_credentials_or_raise(vendor_id)
|
|
except CredentialsNotFoundError:
|
|
raise ValidationException("Letzshop credentials not configured")
|
|
|
|
# Import orders
|
|
try:
|
|
with creds_service.create_client(vendor_id) as client:
|
|
shipments = client.get_unconfirmed_shipments()
|
|
|
|
orders_imported = 0
|
|
orders_updated = 0
|
|
errors = []
|
|
|
|
for shipment in shipments:
|
|
try:
|
|
existing = order_service.get_order_by_shipment_id(
|
|
vendor_id, shipment["id"]
|
|
)
|
|
|
|
if existing:
|
|
order_service.update_order_from_shipment(existing, shipment)
|
|
orders_updated += 1
|
|
else:
|
|
order_service.create_order(vendor_id, shipment)
|
|
orders_imported += 1
|
|
|
|
except Exception as e:
|
|
errors.append(
|
|
f"Error processing shipment {shipment.get('id')}: {e}"
|
|
)
|
|
|
|
db.commit()
|
|
creds_service.update_sync_status(
|
|
vendor_id,
|
|
"success" if not errors else "partial",
|
|
"; ".join(errors) if errors else None,
|
|
)
|
|
|
|
return LetzshopSyncTriggerResponse(
|
|
success=True,
|
|
message=f"Import completed: {orders_imported} imported, {orders_updated} updated",
|
|
orders_imported=orders_imported,
|
|
orders_updated=orders_updated,
|
|
errors=errors,
|
|
)
|
|
|
|
except LetzshopClientError as e:
|
|
creds_service.update_sync_status(vendor_id, "failed", str(e))
|
|
return LetzshopSyncTriggerResponse(
|
|
success=False,
|
|
message=f"Import failed: {e}",
|
|
errors=[str(e)],
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Fulfillment Operations
|
|
# ============================================================================
|
|
|
|
|
|
@router.post("/orders/{order_id}/confirm", response_model=FulfillmentOperationResponse)
|
|
def confirm_order(
|
|
order_id: int = Path(..., description="Order ID"),
|
|
confirm_request: FulfillmentConfirmRequest | None = None,
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Confirm inventory units for a Letzshop order."""
|
|
vendor_id = current_user.token_vendor_id
|
|
order_service = get_order_service(db)
|
|
creds_service = get_credentials_service(db)
|
|
|
|
try:
|
|
order = order_service.get_order_or_raise(vendor_id, order_id)
|
|
except OrderNotFoundError:
|
|
raise ResourceNotFoundException("LetzshopOrder", str(order_id))
|
|
|
|
# Get inventory unit IDs from request or order
|
|
if confirm_request and confirm_request.inventory_unit_ids:
|
|
inventory_unit_ids = confirm_request.inventory_unit_ids
|
|
elif order.inventory_units:
|
|
inventory_unit_ids = [u["id"] for u in order.inventory_units]
|
|
else:
|
|
raise ValidationException("No inventory units to confirm")
|
|
|
|
try:
|
|
with creds_service.create_client(vendor_id) as client:
|
|
result = client.confirm_inventory_units(inventory_unit_ids)
|
|
|
|
# Check for errors
|
|
if result.get("errors"):
|
|
error_messages = [
|
|
f"{e.get('id', 'unknown')}: {e.get('message', 'Unknown error')}"
|
|
for e in result["errors"]
|
|
]
|
|
return FulfillmentOperationResponse(
|
|
success=False,
|
|
message="Some inventory units could not be confirmed",
|
|
errors=error_messages,
|
|
)
|
|
|
|
# Update order status
|
|
order_service.mark_order_confirmed(order)
|
|
db.commit()
|
|
|
|
return FulfillmentOperationResponse(
|
|
success=True,
|
|
message=f"Confirmed {len(inventory_unit_ids)} inventory units",
|
|
confirmed_units=[u.get("id") for u in result.get("inventoryUnits", [])],
|
|
)
|
|
|
|
except LetzshopClientError as e:
|
|
return FulfillmentOperationResponse(success=False, message=str(e))
|
|
|
|
|
|
@router.post("/orders/{order_id}/reject", response_model=FulfillmentOperationResponse)
|
|
def reject_order(
|
|
order_id: int = Path(..., description="Order ID"),
|
|
reject_request: FulfillmentRejectRequest | None = None,
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Reject inventory units for a Letzshop order."""
|
|
vendor_id = current_user.token_vendor_id
|
|
order_service = get_order_service(db)
|
|
creds_service = get_credentials_service(db)
|
|
|
|
try:
|
|
order = order_service.get_order_or_raise(vendor_id, order_id)
|
|
except OrderNotFoundError:
|
|
raise ResourceNotFoundException("LetzshopOrder", str(order_id))
|
|
|
|
# Get inventory unit IDs from request or order
|
|
if reject_request and reject_request.inventory_unit_ids:
|
|
inventory_unit_ids = reject_request.inventory_unit_ids
|
|
elif order.inventory_units:
|
|
inventory_unit_ids = [u["id"] for u in order.inventory_units]
|
|
else:
|
|
raise ValidationException("No inventory units to reject")
|
|
|
|
try:
|
|
with creds_service.create_client(vendor_id) as client:
|
|
result = client.reject_inventory_units(inventory_unit_ids)
|
|
|
|
if result.get("errors"):
|
|
error_messages = [
|
|
f"{e.get('id', 'unknown')}: {e.get('message', 'Unknown error')}"
|
|
for e in result["errors"]
|
|
]
|
|
return FulfillmentOperationResponse(
|
|
success=False,
|
|
message="Some inventory units could not be rejected",
|
|
errors=error_messages,
|
|
)
|
|
|
|
order_service.mark_order_rejected(order)
|
|
db.commit()
|
|
|
|
return FulfillmentOperationResponse(
|
|
success=True,
|
|
message=f"Rejected {len(inventory_unit_ids)} inventory units",
|
|
)
|
|
|
|
except LetzshopClientError as e:
|
|
return FulfillmentOperationResponse(success=False, message=str(e))
|
|
|
|
|
|
@router.post("/orders/{order_id}/tracking", response_model=FulfillmentOperationResponse)
|
|
def set_order_tracking(
|
|
order_id: int = Path(..., description="Order ID"),
|
|
tracking_request: FulfillmentTrackingRequest = ...,
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""Set tracking information for a Letzshop order."""
|
|
vendor_id = current_user.token_vendor_id
|
|
order_service = get_order_service(db)
|
|
creds_service = get_credentials_service(db)
|
|
|
|
try:
|
|
order = order_service.get_order_or_raise(vendor_id, order_id)
|
|
except OrderNotFoundError:
|
|
raise ResourceNotFoundException("LetzshopOrder", str(order_id))
|
|
|
|
if not order.letzshop_shipment_id:
|
|
raise ValidationException("Order does not have a shipment ID")
|
|
|
|
try:
|
|
with creds_service.create_client(vendor_id) as client:
|
|
result = client.set_shipment_tracking(
|
|
shipment_id=order.letzshop_shipment_id,
|
|
tracking_code=tracking_request.tracking_number,
|
|
tracking_provider=tracking_request.tracking_carrier,
|
|
)
|
|
|
|
if result.get("errors"):
|
|
error_messages = [
|
|
f"{e.get('code', 'unknown')}: {e.get('message', 'Unknown error')}"
|
|
for e in result["errors"]
|
|
]
|
|
return FulfillmentOperationResponse(
|
|
success=False,
|
|
message="Failed to set tracking",
|
|
errors=error_messages,
|
|
)
|
|
|
|
order_service.set_order_tracking(
|
|
order,
|
|
tracking_request.tracking_number,
|
|
tracking_request.tracking_carrier,
|
|
)
|
|
db.commit()
|
|
|
|
return FulfillmentOperationResponse(
|
|
success=True,
|
|
message="Tracking information set",
|
|
tracking_number=tracking_request.tracking_number,
|
|
tracking_carrier=tracking_request.tracking_carrier,
|
|
)
|
|
|
|
except LetzshopClientError as e:
|
|
return FulfillmentOperationResponse(success=False, message=str(e))
|
|
|
|
|
|
# ============================================================================
|
|
# Sync Logs
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/logs", response_model=LetzshopSyncLogListResponse)
|
|
def list_sync_logs(
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""List Letzshop sync logs for the current vendor."""
|
|
order_service = get_order_service(db)
|
|
vendor_id = current_user.token_vendor_id
|
|
|
|
logs, total = order_service.list_sync_logs(
|
|
vendor_id=vendor_id,
|
|
skip=skip,
|
|
limit=limit,
|
|
)
|
|
|
|
return LetzshopSyncLogListResponse(
|
|
logs=[
|
|
LetzshopSyncLogResponse(
|
|
id=log.id,
|
|
vendor_id=log.vendor_id,
|
|
operation_type=log.operation_type,
|
|
direction=log.direction,
|
|
status=log.status,
|
|
records_processed=log.records_processed,
|
|
records_succeeded=log.records_succeeded,
|
|
records_failed=log.records_failed,
|
|
error_details=log.error_details,
|
|
started_at=log.started_at,
|
|
completed_at=log.completed_at,
|
|
duration_seconds=log.duration_seconds,
|
|
triggered_by=log.triggered_by,
|
|
created_at=log.created_at,
|
|
)
|
|
for log in logs
|
|
],
|
|
total=total,
|
|
skip=skip,
|
|
limit=limit,
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Fulfillment Queue
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/queue", response_model=FulfillmentQueueListResponse)
|
|
def list_fulfillment_queue(
|
|
skip: int = Query(0, ge=0),
|
|
limit: int = Query(50, ge=1, le=200),
|
|
status: str | None = Query(None, description="Filter by status"),
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""List fulfillment queue items for the current vendor."""
|
|
order_service = get_order_service(db)
|
|
vendor_id = current_user.token_vendor_id
|
|
|
|
items, total = order_service.list_fulfillment_queue(
|
|
vendor_id=vendor_id,
|
|
skip=skip,
|
|
limit=limit,
|
|
status=status,
|
|
)
|
|
|
|
return FulfillmentQueueListResponse(
|
|
items=[
|
|
FulfillmentQueueItemResponse(
|
|
id=item.id,
|
|
vendor_id=item.vendor_id,
|
|
letzshop_order_id=item.letzshop_order_id,
|
|
operation=item.operation,
|
|
payload=item.payload,
|
|
status=item.status,
|
|
attempts=item.attempts,
|
|
max_attempts=item.max_attempts,
|
|
last_attempt_at=item.last_attempt_at,
|
|
next_retry_at=item.next_retry_at,
|
|
error_message=item.error_message,
|
|
completed_at=item.completed_at,
|
|
response_data=item.response_data,
|
|
created_at=item.created_at,
|
|
updated_at=item.updated_at,
|
|
)
|
|
for item in items
|
|
],
|
|
total=total,
|
|
skip=skip,
|
|
limit=limit,
|
|
)
|
|
|
|
|
|
# ============================================================================
|
|
# Product Export
|
|
# ============================================================================
|
|
|
|
|
|
@router.get("/export")
|
|
def export_products_letzshop(
|
|
language: str = Query(
|
|
"en", description="Language for title/description (en, fr, de)"
|
|
),
|
|
include_inactive: bool = Query(False, description="Include inactive products"),
|
|
current_user: User = Depends(get_current_vendor_api),
|
|
db: Session = Depends(get_db),
|
|
):
|
|
"""
|
|
Export vendor products in Letzshop CSV format.
|
|
|
|
Generates a Google Shopping compatible CSV file for Letzshop marketplace.
|
|
The file uses tab-separated values and includes all required Letzshop fields.
|
|
|
|
**Supported languages:** en, fr, de
|
|
|
|
**CSV Format:**
|
|
- Delimiter: Tab (\\t)
|
|
- Encoding: UTF-8
|
|
- Fields: id, title, description, price, availability, image_link, etc.
|
|
|
|
Returns:
|
|
CSV file as attachment (vendor_code_letzshop_export.csv)
|
|
"""
|
|
from fastapi.responses import Response
|
|
|
|
from app.services.letzshop_export_service import letzshop_export_service
|
|
from app.services.vendor_service import vendor_service
|
|
|
|
vendor_id = current_user.token_vendor_id
|
|
vendor = vendor_service.get_vendor_by_id(db, vendor_id)
|
|
|
|
csv_content = letzshop_export_service.export_vendor_products(
|
|
db=db,
|
|
vendor_id=vendor_id,
|
|
language=language,
|
|
include_inactive=include_inactive,
|
|
)
|
|
|
|
filename = f"{vendor.vendor_code.lower()}_letzshop_export.csv"
|
|
|
|
return Response(
|
|
content=csv_content,
|
|
media_type="text/csv; charset=utf-8",
|
|
headers={
|
|
"Content-Disposition": f'attachment; filename="{filename}"',
|
|
},
|
|
)
|