# app/api/v1/admin/letzshop.py """ Admin API endpoints for Letzshop marketplace integration. Provides admin-level management of: - Per-vendor Letzshop credentials - Connection testing - Sync triggers and status - Order overview """ import logging from fastapi import APIRouter, Depends, Path, Query from sqlalchemy.orm import Session from app.api.deps import get_current_admin_api from app.core.database import get_db from app.exceptions import ResourceNotFoundException, ValidationException from app.services.letzshop import ( CredentialsNotFoundError, LetzshopClientError, LetzshopCredentialsService, LetzshopOrderService, VendorNotFoundError, ) from models.database.user import User from models.schema.letzshop import ( LetzshopConnectionTestRequest, LetzshopConnectionTestResponse, LetzshopCredentialsCreate, LetzshopCredentialsResponse, LetzshopCredentialsUpdate, LetzshopOrderListResponse, LetzshopOrderResponse, LetzshopSuccessResponse, LetzshopSyncTriggerRequest, LetzshopSyncTriggerResponse, LetzshopVendorListResponse, LetzshopVendorOverview, ) 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) # ============================================================================ # Vendor Overview # ============================================================================ @router.get("/vendors", response_model=LetzshopVendorListResponse) def list_vendors_letzshop_status( skip: int = Query(0, ge=0), limit: int = Query(100, ge=1, le=1000), configured_only: bool = Query( False, description="Only show vendors with Letzshop configured" ), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """ List all vendors with their Letzshop integration status. Shows which vendors have Letzshop configured, sync status, and pending orders. """ order_service = get_order_service(db) vendor_overviews, total = order_service.list_vendors_with_letzshop_status( skip=skip, limit=limit, configured_only=configured_only, ) return LetzshopVendorListResponse( vendors=[LetzshopVendorOverview(**v) for v in vendor_overviews], total=total, skip=skip, limit=limit, ) # ============================================================================ # Credentials Management # ============================================================================ @router.get( "/vendors/{vendor_id}/credentials", response_model=LetzshopCredentialsResponse, ) def get_vendor_credentials( vendor_id: int = Path(..., description="Vendor ID"), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """Get Letzshop credentials for a vendor (API key is masked).""" order_service = get_order_service(db) creds_service = get_credentials_service(db) try: vendor = order_service.get_vendor_or_raise(vendor_id) except VendorNotFoundError: raise ResourceNotFoundException("Vendor", str(vendor_id)) try: credentials = creds_service.get_credentials_or_raise(vendor_id) except CredentialsNotFoundError: raise ResourceNotFoundException( "LetzshopCredentials", str(vendor_id), message=f"Letzshop credentials not configured for vendor {vendor.name}", ) 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( "/vendors/{vendor_id}/credentials", response_model=LetzshopCredentialsResponse, ) def create_or_update_vendor_credentials( vendor_id: int = Path(..., description="Vendor ID"), credentials_data: LetzshopCredentialsCreate = ..., db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """Create or update Letzshop credentials for a vendor.""" order_service = get_order_service(db) creds_service = get_credentials_service(db) try: vendor = order_service.get_vendor_or_raise(vendor_id) except VendorNotFoundError: raise ResourceNotFoundException("Vendor", str(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"Admin {current_admin.email} updated Letzshop credentials for vendor {vendor.name}" ) 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( "/vendors/{vendor_id}/credentials", response_model=LetzshopCredentialsResponse, ) def update_vendor_credentials( vendor_id: int = Path(..., description="Vendor ID"), credentials_data: LetzshopCredentialsUpdate = ..., db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """Partially update Letzshop credentials for a vendor.""" order_service = get_order_service(db) creds_service = get_credentials_service(db) try: vendor = order_service.get_vendor_or_raise(vendor_id) except VendorNotFoundError: raise ResourceNotFoundException("Vendor", str(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), message=f"Letzshop credentials not configured for vendor {vendor.name}", ) 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( "/vendors/{vendor_id}/credentials", response_model=LetzshopSuccessResponse, ) def delete_vendor_credentials( vendor_id: int = Path(..., description="Vendor ID"), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """Delete Letzshop credentials for a vendor.""" order_service = get_order_service(db) creds_service = get_credentials_service(db) try: vendor = order_service.get_vendor_or_raise(vendor_id) except VendorNotFoundError: raise ResourceNotFoundException("Vendor", str(vendor_id)) deleted = creds_service.delete_credentials(vendor_id) if not deleted: raise ResourceNotFoundException( "LetzshopCredentials", str(vendor_id), message=f"Letzshop credentials not configured for vendor {vendor.name}", ) db.commit() logger.info( f"Admin {current_admin.email} deleted Letzshop credentials for vendor {vendor.name}" ) return LetzshopSuccessResponse(success=True, message="Letzshop credentials deleted") # ============================================================================ # Connection Testing # ============================================================================ @router.post( "/vendors/{vendor_id}/test", response_model=LetzshopConnectionTestResponse, ) def test_vendor_connection( vendor_id: int = Path(..., description="Vendor ID"), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """Test the Letzshop connection for a vendor using stored credentials.""" order_service = get_order_service(db) creds_service = get_credentials_service(db) try: order_service.get_vendor_or_raise(vendor_id) except VendorNotFoundError: raise ResourceNotFoundException("Vendor", str(vendor_id)) success, response_time_ms, error = creds_service.test_connection(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", response_model=LetzshopConnectionTestResponse) def test_api_key( test_request: LetzshopConnectionTestRequest, db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """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( "/vendors/{vendor_id}/orders", response_model=LetzshopOrderListResponse, ) def list_vendor_letzshop_orders( vendor_id: int = Path(..., description="Vendor ID"), 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"), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """List Letzshop orders for a vendor.""" order_service = get_order_service(db) try: order_service.get_vendor_or_raise(vendor_id) except VendorNotFoundError: raise ResourceNotFoundException("Vendor", str(vendor_id)) orders, total = order_service.list_orders( vendor_id=vendor_id, skip=skip, limit=limit, sync_status=sync_status, ) 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.post( "/vendors/{vendor_id}/sync", response_model=LetzshopSyncTriggerResponse, ) def trigger_vendor_sync( vendor_id: int = Path(..., description="Vendor ID"), sync_request: LetzshopSyncTriggerRequest = LetzshopSyncTriggerRequest(), db: Session = Depends(get_db), current_admin: User = Depends(get_current_admin_api), ): """ Trigger a sync operation for a vendor. This imports new orders from Letzshop. """ order_service = get_order_service(db) creds_service = get_credentials_service(db) try: vendor = order_service.get_vendor_or_raise(vendor_id) except VendorNotFoundError: raise ResourceNotFoundException("Vendor", str(vendor_id)) # Verify credentials exist try: creds_service.get_credentials_or_raise(vendor_id) except CredentialsNotFoundError: raise ValidationException( f"Letzshop credentials not configured for vendor {vendor.name}" ) # Import orders using the client 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: # Check if order already exists existing = order_service.get_order_by_shipment_id( vendor_id, shipment["id"] ) if existing: # Update existing order order_service.update_order_from_shipment(existing, shipment) orders_updated += 1 else: # Create new order 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() # Update sync status 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"Sync 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"Sync failed: {e}", errors=[str(e)], )