fix: use custom exceptions in onboarding and add tests
- Create onboarding-specific exceptions (OnboardingNotFoundException, etc.) - Remove HTTPException usage from API endpoints per architecture rules - Let exceptions propagate to global handler - Add 12 integration tests for onboarding API endpoints 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -14,10 +14,16 @@ from datetime import UTC, datetime
|
||||
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.exceptions import (
|
||||
OnboardingCsvUrlRequiredException,
|
||||
OnboardingNotFoundException,
|
||||
OnboardingStepOrderException,
|
||||
OnboardingSyncJobNotFoundException,
|
||||
OnboardingSyncNotCompleteException,
|
||||
VendorNotFoundException,
|
||||
)
|
||||
from app.services.letzshop.credentials_service import LetzshopCredentialsService
|
||||
from app.services.letzshop.order_service import LetzshopOrderService
|
||||
from models.database.company import Company
|
||||
from models.database.letzshop import LetzshopHistoricalImportJob
|
||||
from models.database.onboarding import (
|
||||
OnboardingStatus,
|
||||
OnboardingStep,
|
||||
@@ -28,18 +34,6 @@ from models.database.vendor import Vendor
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class OnboardingError(Exception):
|
||||
"""Base exception for onboarding errors."""
|
||||
|
||||
|
||||
class OnboardingNotFoundError(OnboardingError):
|
||||
"""Raised when onboarding record is not found."""
|
||||
|
||||
|
||||
class OnboardingStepError(OnboardingError):
|
||||
"""Raised when a step operation fails."""
|
||||
|
||||
|
||||
class OnboardingService:
|
||||
"""
|
||||
Service for managing vendor onboarding workflow.
|
||||
@@ -69,12 +63,10 @@ class OnboardingService:
|
||||
)
|
||||
|
||||
def get_onboarding_or_raise(self, vendor_id: int) -> VendorOnboarding:
|
||||
"""Get onboarding record or raise OnboardingNotFoundError."""
|
||||
"""Get onboarding record or raise OnboardingNotFoundException."""
|
||||
onboarding = self.get_onboarding(vendor_id)
|
||||
if onboarding is None:
|
||||
raise OnboardingNotFoundError(
|
||||
f"Onboarding not found for vendor {vendor_id}"
|
||||
)
|
||||
raise OnboardingNotFoundException(vendor_id)
|
||||
return onboarding
|
||||
|
||||
def create_onboarding(self, vendor_id: int) -> VendorOnboarding:
|
||||
@@ -221,7 +213,7 @@ class OnboardingService:
|
||||
# Get vendor and company
|
||||
vendor = self.db.query(Vendor).filter(Vendor.id == vendor_id).first()
|
||||
if not vendor:
|
||||
raise OnboardingStepError(f"Vendor {vendor_id} not found")
|
||||
raise VendorNotFoundException(vendor_id)
|
||||
|
||||
company = vendor.company
|
||||
|
||||
@@ -326,8 +318,9 @@ class OnboardingService:
|
||||
|
||||
# Verify step order
|
||||
if not onboarding.can_proceed_to_step(OnboardingStep.LETZSHOP_API.value):
|
||||
raise OnboardingStepError(
|
||||
"Please complete the Company Profile step first"
|
||||
raise OnboardingStepOrderException(
|
||||
current_step=onboarding.current_step,
|
||||
required_step=OnboardingStep.COMPANY_PROFILE.value,
|
||||
)
|
||||
|
||||
# Test connection first
|
||||
@@ -412,8 +405,9 @@ class OnboardingService:
|
||||
|
||||
# Verify step order
|
||||
if not onboarding.can_proceed_to_step(OnboardingStep.PRODUCT_IMPORT.value):
|
||||
raise OnboardingStepError(
|
||||
"Please complete the Letzshop API step first"
|
||||
raise OnboardingStepOrderException(
|
||||
current_step=onboarding.current_step,
|
||||
required_step=OnboardingStep.LETZSHOP_API.value,
|
||||
)
|
||||
|
||||
# Validate at least one CSV URL
|
||||
@@ -424,18 +418,12 @@ class OnboardingService:
|
||||
])
|
||||
|
||||
if csv_urls_count == 0:
|
||||
return {
|
||||
"success": False,
|
||||
"step_completed": False,
|
||||
"next_step": None,
|
||||
"message": "At least one CSV URL must be provided",
|
||||
"csv_urls_configured": 0,
|
||||
}
|
||||
raise OnboardingCsvUrlRequiredException()
|
||||
|
||||
# Update vendor settings
|
||||
vendor = self.db.query(Vendor).filter(Vendor.id == vendor_id).first()
|
||||
if not vendor:
|
||||
raise OnboardingStepError(f"Vendor {vendor_id} not found")
|
||||
raise VendorNotFoundException(vendor_id)
|
||||
|
||||
vendor.letzshop_csv_url_fr = csv_url_fr
|
||||
vendor.letzshop_csv_url_en = csv_url_en
|
||||
@@ -480,8 +468,9 @@ class OnboardingService:
|
||||
|
||||
# Verify step order
|
||||
if not onboarding.can_proceed_to_step(OnboardingStep.ORDER_SYNC.value):
|
||||
raise OnboardingStepError(
|
||||
"Please complete the Product Import step first"
|
||||
raise OnboardingStepOrderException(
|
||||
current_step=onboarding.current_step,
|
||||
required_step=OnboardingStep.PRODUCT_IMPORT.value,
|
||||
)
|
||||
|
||||
# Create historical import job
|
||||
@@ -597,16 +586,10 @@ class OnboardingService:
|
||||
job = order_service.get_historical_import_job_by_id(vendor_id, job_id)
|
||||
|
||||
if not job:
|
||||
raise OnboardingStepError(f"Job {job_id} not found")
|
||||
raise OnboardingSyncJobNotFoundException(job_id)
|
||||
|
||||
if job.status not in ("completed", "failed"):
|
||||
return {
|
||||
"success": False,
|
||||
"step_completed": False,
|
||||
"onboarding_completed": False,
|
||||
"message": f"Job is still {job.status}, please wait",
|
||||
"redirect_url": None,
|
||||
}
|
||||
raise OnboardingSyncNotCompleteException(job.status)
|
||||
|
||||
# Mark step complete (even if job failed - they can retry later)
|
||||
onboarding.mark_step_complete(OnboardingStep.ORDER_SYNC.value)
|
||||
|
||||
Reference in New Issue
Block a user