- Move Product/ProductTranslation to app/modules/catalog/models/ - Move VendorOnboarding to app/modules/marketplace/models/ - Delete legacy re-export files for marketplace models: - letzshop.py, marketplace.py, marketplace_product.py - marketplace_product_translation.py, marketplace_import_job.py - Delete legacy product.py, product_translation.py, onboarding.py - Update all imports across services, tasks, tests to use module locations Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
401 lines
12 KiB
Python
401 lines
12 KiB
Python
# app/services/letzshop/credentials_service.py
|
|
"""
|
|
Letzshop credentials management service.
|
|
|
|
Handles secure storage and retrieval of per-vendor Letzshop API credentials.
|
|
"""
|
|
|
|
import logging
|
|
from datetime import UTC, datetime
|
|
|
|
from sqlalchemy.orm import Session
|
|
|
|
from app.utils.encryption import decrypt_value, encrypt_value, mask_api_key
|
|
from app.modules.marketplace.models import VendorLetzshopCredentials
|
|
|
|
from .client_service import LetzshopClient
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Default Letzshop GraphQL endpoint
|
|
DEFAULT_ENDPOINT = "https://letzshop.lu/graphql"
|
|
|
|
|
|
class CredentialsError(Exception):
|
|
"""Base exception for credentials errors."""
|
|
|
|
|
|
class CredentialsNotFoundError(CredentialsError):
|
|
"""Raised when credentials are not found for a vendor."""
|
|
|
|
|
|
class LetzshopCredentialsService:
|
|
"""
|
|
Service for managing Letzshop API credentials.
|
|
|
|
Provides secure storage and retrieval of encrypted API keys,
|
|
connection testing, and sync status updates.
|
|
"""
|
|
|
|
def __init__(self, db: Session):
|
|
"""
|
|
Initialize the credentials service.
|
|
|
|
Args:
|
|
db: SQLAlchemy database session.
|
|
"""
|
|
self.db = db
|
|
|
|
# ========================================================================
|
|
# CRUD Operations
|
|
# ========================================================================
|
|
|
|
def get_credentials(self, vendor_id: int) -> VendorLetzshopCredentials | None:
|
|
"""
|
|
Get Letzshop credentials for a vendor.
|
|
|
|
Args:
|
|
vendor_id: The vendor ID.
|
|
|
|
Returns:
|
|
VendorLetzshopCredentials or None if not found.
|
|
"""
|
|
return (
|
|
self.db.query(VendorLetzshopCredentials)
|
|
.filter(VendorLetzshopCredentials.vendor_id == vendor_id)
|
|
.first()
|
|
)
|
|
|
|
def get_credentials_or_raise(self, vendor_id: int) -> VendorLetzshopCredentials:
|
|
"""
|
|
Get Letzshop credentials for a vendor or raise an exception.
|
|
|
|
Args:
|
|
vendor_id: The vendor ID.
|
|
|
|
Returns:
|
|
VendorLetzshopCredentials.
|
|
|
|
Raises:
|
|
CredentialsNotFoundError: If credentials are not found.
|
|
"""
|
|
credentials = self.get_credentials(vendor_id)
|
|
if credentials is None:
|
|
raise CredentialsNotFoundError(
|
|
f"Letzshop credentials not found for vendor {vendor_id}"
|
|
)
|
|
return credentials
|
|
|
|
def create_credentials(
|
|
self,
|
|
vendor_id: int,
|
|
api_key: str,
|
|
api_endpoint: str | None = None,
|
|
auto_sync_enabled: bool = False,
|
|
sync_interval_minutes: int = 15,
|
|
) -> VendorLetzshopCredentials:
|
|
"""
|
|
Create Letzshop credentials for a vendor.
|
|
|
|
Args:
|
|
vendor_id: The vendor ID.
|
|
api_key: The Letzshop API key (will be encrypted).
|
|
api_endpoint: Custom API endpoint (optional).
|
|
auto_sync_enabled: Whether to enable automatic sync.
|
|
sync_interval_minutes: Sync interval in minutes.
|
|
|
|
Returns:
|
|
Created VendorLetzshopCredentials.
|
|
"""
|
|
# Encrypt the API key
|
|
encrypted_key = encrypt_value(api_key)
|
|
|
|
credentials = VendorLetzshopCredentials(
|
|
vendor_id=vendor_id,
|
|
api_key_encrypted=encrypted_key,
|
|
api_endpoint=api_endpoint or DEFAULT_ENDPOINT,
|
|
auto_sync_enabled=auto_sync_enabled,
|
|
sync_interval_minutes=sync_interval_minutes,
|
|
)
|
|
|
|
self.db.add(credentials)
|
|
self.db.flush()
|
|
|
|
logger.info(f"Created Letzshop credentials for vendor {vendor_id}")
|
|
return credentials
|
|
|
|
def update_credentials(
|
|
self,
|
|
vendor_id: int,
|
|
api_key: str | None = None,
|
|
api_endpoint: str | None = None,
|
|
auto_sync_enabled: bool | None = None,
|
|
sync_interval_minutes: int | None = None,
|
|
) -> VendorLetzshopCredentials:
|
|
"""
|
|
Update Letzshop credentials for a vendor.
|
|
|
|
Args:
|
|
vendor_id: The vendor ID.
|
|
api_key: New API key (optional, will be encrypted if provided).
|
|
api_endpoint: New API endpoint (optional).
|
|
auto_sync_enabled: New auto-sync setting (optional).
|
|
sync_interval_minutes: New sync interval (optional).
|
|
|
|
Returns:
|
|
Updated VendorLetzshopCredentials.
|
|
|
|
Raises:
|
|
CredentialsNotFoundError: If credentials are not found.
|
|
"""
|
|
credentials = self.get_credentials_or_raise(vendor_id)
|
|
|
|
if api_key is not None:
|
|
credentials.api_key_encrypted = encrypt_value(api_key)
|
|
if api_endpoint is not None:
|
|
credentials.api_endpoint = api_endpoint
|
|
if auto_sync_enabled is not None:
|
|
credentials.auto_sync_enabled = auto_sync_enabled
|
|
if sync_interval_minutes is not None:
|
|
credentials.sync_interval_minutes = sync_interval_minutes
|
|
|
|
self.db.flush()
|
|
|
|
logger.info(f"Updated Letzshop credentials for vendor {vendor_id}")
|
|
return credentials
|
|
|
|
def delete_credentials(self, vendor_id: int) -> bool:
|
|
"""
|
|
Delete Letzshop credentials for a vendor.
|
|
|
|
Args:
|
|
vendor_id: The vendor ID.
|
|
|
|
Returns:
|
|
True if deleted, False if not found.
|
|
"""
|
|
credentials = self.get_credentials(vendor_id)
|
|
if credentials is None:
|
|
return False
|
|
|
|
self.db.delete(credentials)
|
|
self.db.flush()
|
|
|
|
logger.info(f"Deleted Letzshop credentials for vendor {vendor_id}")
|
|
return True
|
|
|
|
def upsert_credentials(
|
|
self,
|
|
vendor_id: int,
|
|
api_key: str,
|
|
api_endpoint: str | None = None,
|
|
auto_sync_enabled: bool = False,
|
|
sync_interval_minutes: int = 15,
|
|
) -> VendorLetzshopCredentials:
|
|
"""
|
|
Create or update Letzshop credentials for a vendor.
|
|
|
|
Args:
|
|
vendor_id: The vendor ID.
|
|
api_key: The Letzshop API key (will be encrypted).
|
|
api_endpoint: Custom API endpoint (optional).
|
|
auto_sync_enabled: Whether to enable automatic sync.
|
|
sync_interval_minutes: Sync interval in minutes.
|
|
|
|
Returns:
|
|
Created or updated VendorLetzshopCredentials.
|
|
"""
|
|
existing = self.get_credentials(vendor_id)
|
|
|
|
if existing:
|
|
return self.update_credentials(
|
|
vendor_id=vendor_id,
|
|
api_key=api_key,
|
|
api_endpoint=api_endpoint,
|
|
auto_sync_enabled=auto_sync_enabled,
|
|
sync_interval_minutes=sync_interval_minutes,
|
|
)
|
|
|
|
return self.create_credentials(
|
|
vendor_id=vendor_id,
|
|
api_key=api_key,
|
|
api_endpoint=api_endpoint,
|
|
auto_sync_enabled=auto_sync_enabled,
|
|
sync_interval_minutes=sync_interval_minutes,
|
|
)
|
|
|
|
# ========================================================================
|
|
# Key Decryption and Client Creation
|
|
# ========================================================================
|
|
|
|
def get_decrypted_api_key(self, vendor_id: int) -> str:
|
|
"""
|
|
Get the decrypted API key for a vendor.
|
|
|
|
Args:
|
|
vendor_id: The vendor ID.
|
|
|
|
Returns:
|
|
Decrypted API key.
|
|
|
|
Raises:
|
|
CredentialsNotFoundError: If credentials are not found.
|
|
"""
|
|
credentials = self.get_credentials_or_raise(vendor_id)
|
|
return decrypt_value(credentials.api_key_encrypted)
|
|
|
|
def get_masked_api_key(self, vendor_id: int) -> str:
|
|
"""
|
|
Get a masked version of the API key for display.
|
|
|
|
Args:
|
|
vendor_id: The vendor ID.
|
|
|
|
Returns:
|
|
Masked API key (e.g., "sk-a***************").
|
|
|
|
Raises:
|
|
CredentialsNotFoundError: If credentials are not found.
|
|
"""
|
|
api_key = self.get_decrypted_api_key(vendor_id)
|
|
return mask_api_key(api_key)
|
|
|
|
def create_client(self, vendor_id: int) -> LetzshopClient:
|
|
"""
|
|
Create a Letzshop client for a vendor.
|
|
|
|
Args:
|
|
vendor_id: The vendor ID.
|
|
|
|
Returns:
|
|
Configured LetzshopClient.
|
|
|
|
Raises:
|
|
CredentialsNotFoundError: If credentials are not found.
|
|
"""
|
|
credentials = self.get_credentials_or_raise(vendor_id)
|
|
api_key = decrypt_value(credentials.api_key_encrypted)
|
|
|
|
return LetzshopClient(
|
|
api_key=api_key,
|
|
endpoint=credentials.api_endpoint,
|
|
)
|
|
|
|
# ========================================================================
|
|
# Connection Testing
|
|
# ========================================================================
|
|
|
|
def test_connection(self, vendor_id: int) -> tuple[bool, float | None, str | None]:
|
|
"""
|
|
Test the connection for a vendor's credentials.
|
|
|
|
Args:
|
|
vendor_id: The vendor ID.
|
|
|
|
Returns:
|
|
Tuple of (success, response_time_ms, error_message).
|
|
"""
|
|
try:
|
|
with self.create_client(vendor_id) as client:
|
|
return client.test_connection()
|
|
except CredentialsNotFoundError:
|
|
return False, None, "Letzshop credentials not configured"
|
|
except Exception as e:
|
|
logger.error(f"Connection test failed for vendor {vendor_id}: {e}")
|
|
return False, None, str(e)
|
|
|
|
def test_api_key(
|
|
self,
|
|
api_key: str,
|
|
api_endpoint: str | None = None,
|
|
) -> tuple[bool, float | None, str | None]:
|
|
"""
|
|
Test an API key without saving it.
|
|
|
|
Args:
|
|
api_key: The API key to test.
|
|
api_endpoint: Optional custom endpoint.
|
|
|
|
Returns:
|
|
Tuple of (success, response_time_ms, error_message).
|
|
"""
|
|
try:
|
|
with LetzshopClient(
|
|
api_key=api_key,
|
|
endpoint=api_endpoint or DEFAULT_ENDPOINT,
|
|
) as client:
|
|
return client.test_connection()
|
|
except Exception as e:
|
|
logger.error(f"API key test failed: {e}")
|
|
return False, None, str(e)
|
|
|
|
# ========================================================================
|
|
# Sync Status Updates
|
|
# ========================================================================
|
|
|
|
def update_sync_status(
|
|
self,
|
|
vendor_id: int,
|
|
status: str,
|
|
error: str | None = None,
|
|
) -> VendorLetzshopCredentials | None:
|
|
"""
|
|
Update the last sync status for a vendor.
|
|
|
|
Args:
|
|
vendor_id: The vendor ID.
|
|
status: Sync status (success, failed, partial).
|
|
error: Error message if sync failed.
|
|
|
|
Returns:
|
|
Updated credentials or None if not found.
|
|
"""
|
|
credentials = self.get_credentials(vendor_id)
|
|
if credentials is None:
|
|
return None
|
|
|
|
credentials.last_sync_at = datetime.now(UTC)
|
|
credentials.last_sync_status = status
|
|
credentials.last_sync_error = error
|
|
|
|
self.db.flush()
|
|
|
|
return credentials
|
|
|
|
# ========================================================================
|
|
# Status Helpers
|
|
# ========================================================================
|
|
|
|
def is_configured(self, vendor_id: int) -> bool:
|
|
"""Check if Letzshop is configured for a vendor."""
|
|
return self.get_credentials(vendor_id) is not None
|
|
|
|
def get_status(self, vendor_id: int) -> dict:
|
|
"""
|
|
Get the Letzshop integration status for a vendor.
|
|
|
|
Args:
|
|
vendor_id: The vendor ID.
|
|
|
|
Returns:
|
|
Status dictionary with configuration and sync info.
|
|
"""
|
|
credentials = self.get_credentials(vendor_id)
|
|
|
|
if credentials is None:
|
|
return {
|
|
"is_configured": False,
|
|
"is_connected": False,
|
|
"last_sync_at": None,
|
|
"last_sync_status": None,
|
|
"auto_sync_enabled": False,
|
|
}
|
|
|
|
return {
|
|
"is_configured": True,
|
|
"is_connected": credentials.last_sync_status == "success",
|
|
"last_sync_at": credentials.last_sync_at,
|
|
"last_sync_status": credentials.last_sync_status,
|
|
"auto_sync_enabled": credentials.auto_sync_enabled,
|
|
}
|