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:
2025-12-27 21:55:03 +01:00
parent 409a2eaa05
commit 73f612a01a
5 changed files with 538 additions and 118 deletions

View File

@@ -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)