refactor: migrate templates and static files to self-contained modules

Templates Migration:
- Migrate admin templates to modules (tenancy, billing, monitoring, marketplace, etc.)
- Migrate vendor templates to modules (tenancy, billing, orders, messaging, etc.)
- Migrate storefront templates to modules (catalog, customers, orders, cart, checkout, cms)
- Migrate public templates to modules (billing, marketplace, cms)
- Keep shared templates in app/templates/ (base.html, errors/, partials/, macros/)
- Migrate letzshop partials to marketplace module

Static Files Migration:
- Migrate admin JS to modules: tenancy (23 files), core (5 files), monitoring (1 file)
- Migrate vendor JS to modules: tenancy (4 files), core (2 files)
- Migrate shared JS: vendor-selector.js to core, media-picker.js to cms
- Migrate storefront JS: storefront-layout.js to core
- Keep framework JS in static/ (api-client, utils, money, icons, log-config, lib/)
- Update all template references to use module_static paths

Naming Consistency:
- Rename static/platform/ to static/public/
- Rename app/templates/platform/ to app/templates/public/
- Update all extends and static references

Documentation:
- Update module-system.md with shared templates documentation
- Update frontend-structure.md with new module JS organization

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-02-01 14:34:16 +01:00
parent 843703258f
commit 4e28d91a78
542 changed files with 11603 additions and 9037 deletions

View File

@@ -39,7 +39,7 @@ from fastapi.security import HTTPAuthorizationCredentials, HTTPBearer
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.exceptions import (
from app.modules.tenancy.exceptions import (
AdminRequiredException,
InsufficientPermissionsException,
InsufficientVendorPermissionsException,
@@ -48,7 +48,7 @@ from app.exceptions import (
VendorNotFoundException,
VendorOwnerOnlyException,
)
from app.services.vendor_service import vendor_service
from app.modules.tenancy.services.vendor_service import vendor_service
from middleware.auth import AuthManager
from middleware.rate_limiter import RateLimiter
from models.database.user import User as UserModel
@@ -552,7 +552,7 @@ def require_menu_access(menu_item_id: str, frontend_type: "FrontendType"):
"""
from app.modules.registry import get_menu_item_module
from app.modules.service import module_service
from app.services.menu_service import menu_service
from app.modules.core.services.menu_service import menu_service
from models.database.admin_menu_config import FrontendType as FT
def _check_menu_access(

View File

@@ -5,13 +5,12 @@ API router configuration for multi-tenant ecommerce platform.
This module provides:
- API version 1 route aggregation
- Route organization by user type (admin, vendor, storefront)
- Proper route prefixing and tagging
- Auto-discovery of module routes
"""
from fastapi import APIRouter
from app.api.v1 import admin, platform, storefront, vendor
from app.api.v1.shared import language, webhooks
from app.api.v1 import admin, public, storefront, vendor, webhooks
api_router = APIRouter()
@@ -38,16 +37,17 @@ api_router.include_router(vendor.router, prefix="/v1/vendor", tags=["vendor"])
api_router.include_router(storefront.router, prefix="/v1/storefront", tags=["storefront"])
# ============================================================================
# PLATFORM ROUTES (Public marketing and signup)
# Prefix: /api/v1/platform
# PUBLIC ROUTES (Unauthenticated endpoints)
# Prefix: /api/v1/public
# Includes: /signup, /pricing, /letzshop-vendors, /language
# ============================================================================
api_router.include_router(platform.router, prefix="/v1/platform", tags=["platform"])
api_router.include_router(public.router, prefix="/v1/public", tags=["public"])
# ============================================================================
# SHARED ROUTES (Cross-context utilities)
# Prefix: /api/v1
# WEBHOOK ROUTES (External service callbacks via auto-discovery)
# Prefix: /api/v1/webhooks
# Includes: /stripe
# ============================================================================
api_router.include_router(language.router, prefix="/v1", tags=["language"])
api_router.include_router(webhooks.router, prefix="/v1", tags=["webhooks"])
api_router.include_router(webhooks.router, prefix="/v1/webhooks", tags=["webhooks"])

View File

@@ -2,68 +2,45 @@
"""
Admin API router aggregation.
This module combines all admin-related JSON API endpoints:
- Authentication (login/logout)
- Vendor management (CRUD, bulk operations)
- Vendor domains management (custom domains, DNS verification)
- Vendor themes management (theme editor, presets)
- User management (status, roles)
- Dashboard and statistics
- Marketplace monitoring
- Audit logging
- Platform settings
- Notifications and alerts
- Code quality and architecture validation
This module combines legacy admin routes with auto-discovered module routes.
LEGACY ROUTES (defined in app/api/v1/admin/):
- /menu-config/* - Navigation configuration (super admin)
- /modules/* - Module management (super admin)
- /module-config/* - Module settings (super admin)
AUTO-DISCOVERED MODULE ROUTES:
- tenancy: auth, admin_users, users, companies, platforms, vendors, vendor_domains
- core: dashboard, settings
- messaging: messages, notifications, email-templates
- monitoring: logs, tasks, tests, code_quality, audit, platform-health
- billing: subscriptions, invoices, payments
- inventory: stock management
- orders: order management, fulfillment, exceptions
- marketplace: letzshop integration, product sync
- catalog: vendor product catalog
- cms: content-pages, images, media, vendor-themes
- customers: customer management
IMPORTANT:
- This router is for JSON API endpoints only
- HTML page routes are mounted separately in main.py at /vendor/*
- Do NOT include pages.router here - it causes route conflicts
MODULE SYSTEM:
Routes can be module-gated using require_module_access() dependency.
For multi-tenant apps, module enablement is checked at request time
based on platform context (not at route registration time).
Self-contained modules (auto-discovered from app/modules/{module}/routes/api/admin.py):
- billing: Subscription tiers, vendor billing, invoices
- inventory: Stock management, inventory tracking
- orders: Order management, fulfillment, exceptions
- marketplace: Letzshop integration, product sync, marketplace products
- catalog: Vendor product catalog management
- cms: Content pages management
- customers: Customer management
- HTML page routes are mounted separately in main.py
- Module routes are auto-discovered from app/modules/{module}/routes/api/admin.py
"""
from fastapi import APIRouter
# Import all admin routers (legacy routes that haven't been migrated to modules)
# NOTE: Migrated to modules (auto-discovered):
# - tenancy: auth, admin_users, users, companies, platforms, vendors, vendor_domains
# - core: dashboard, settings
# - messaging: messages, notifications, email_templates
# - monitoring: logs, tasks, tests, code_quality, audit, platform_health
# - cms: content_pages, images, media, vendor_themes
from . import (
admin_users,
audit,
auth,
background_tasks,
code_quality,
companies,
dashboard,
email_templates,
images,
logs,
media,
menu_config,
messages,
module_config,
modules,
monitoring,
notifications,
platform_health,
platforms,
settings,
tests,
users,
vendor_domains,
vendor_themes,
vendors,
)
# Create admin router
@@ -71,32 +48,9 @@ router = APIRouter()
# ============================================================================
# Authentication & Authorization
# Framework Config (remain in legacy - super admin only)
# ============================================================================
# Include authentication endpoints
router.include_router(auth.router, tags=["admin-auth"])
# ============================================================================
# Company & Vendor Management
# ============================================================================
# Include company management endpoints
router.include_router(companies.router, tags=["admin-companies"])
# Include vendor management endpoints
router.include_router(vendors.router, tags=["admin-vendors"])
# Include vendor domains management endpoints
router.include_router(vendor_domains.router, tags=["admin-vendor-domains"])
# Include vendor themes management endpoints
router.include_router(vendor_themes.router, tags=["admin-vendor-themes"])
# Include platforms management endpoints (multi-platform CMS)
router.include_router(platforms.router, tags=["admin-platforms"])
# Include menu configuration endpoints (super admin only)
router.include_router(menu_config.router, tags=["admin-menu-config"])
@@ -107,82 +61,13 @@ router.include_router(modules.router, tags=["admin-modules"])
router.include_router(module_config.router, tags=["admin-module-config"])
# ============================================================================
# User Management
# ============================================================================
# Include user management endpoints
router.include_router(users.router, tags=["admin-users"])
# Include admin user management endpoints (super admin only)
router.include_router(admin_users.router, tags=["admin-admin-users"])
# ============================================================================
# Dashboard & Statistics
# ============================================================================
# Include dashboard and statistics endpoints
router.include_router(dashboard.router, tags=["admin-dashboard"])
# ============================================================================
# Platform Administration
# ============================================================================
# Include background tasks monitoring endpoints
router.include_router(
background_tasks.router, prefix="/background-tasks", tags=["admin-background-tasks"]
)
# Include audit logging endpoints
router.include_router(audit.router, tags=["admin-audit"])
# Include platform settings endpoints
router.include_router(settings.router, tags=["admin-settings"])
# Include notifications and alerts endpoints
router.include_router(notifications.router, tags=["admin-notifications"])
# Include messaging endpoints
router.include_router(messages.router, tags=["admin-messages"])
# Include email templates management endpoints
router.include_router(email_templates.router, tags=["admin-email-templates"])
# Include log management endpoints
router.include_router(logs.router, tags=["admin-logs"])
# Include image management endpoints
router.include_router(images.router, tags=["admin-images"])
# Include media library management endpoints
router.include_router(media.router, tags=["admin-media"])
# Include platform health endpoints
router.include_router(
platform_health.router, prefix="/platform", tags=["admin-platform-health"]
)
# ============================================================================
# Code Quality & Architecture
# ============================================================================
# Include code quality and architecture validation endpoints
router.include_router(
code_quality.router, prefix="/code-quality", tags=["admin-code-quality"]
)
# Include test runner endpoints
router.include_router(tests.router, prefix="/tests", tags=["admin-tests"])
# ============================================================================
# Auto-discovered Module Routes
# ============================================================================
# Routes from self-contained modules are auto-discovered and registered.
# Modules include: billing, inventory, orders, marketplace, cms, customers
# Modules include: billing, inventory, orders, marketplace, cms, customers,
# monitoring (logs, tasks, tests, code_quality, audit, platform_health),
# messaging (messages, notifications, email_templates)
from app.modules.routes import get_admin_api_routes

View File

@@ -27,8 +27,8 @@ from app.api.deps import (
get_current_super_admin,
get_db,
)
from app.services.menu_service import MenuItemConfig, menu_service
from app.services.platform_service import platform_service
from app.modules.core.services.menu_service import MenuItemConfig, menu_service
from app.modules.tenancy.services.platform_service import platform_service
from models.database.admin_menu_config import FrontendType # noqa: API-007 - Enum for type safety
from models.schema.auth import UserContext

View File

@@ -21,7 +21,7 @@ from app.api.deps import get_current_super_admin, get_db
from app.exceptions import ValidationException
from app.modules.registry import MODULES
from app.modules.service import module_service
from app.services.platform_service import platform_service
from app.modules.tenancy.services.platform_service import platform_service
from models.schema.auth import UserContext
logger = logging.getLogger(__name__)

View File

@@ -21,7 +21,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_super_admin, get_db
from app.modules.registry import MODULES, get_core_module_codes
from app.modules.service import module_service
from app.services.platform_service import platform_service
from app.modules.tenancy.services.platform_service import platform_service
from models.schema.auth import UserContext
logger = logging.getLogger(__name__)
@@ -263,7 +263,7 @@ async def enable_module(
# Validate module code
if request.module_code not in MODULES:
from app.exceptions import BadRequestException
from app.modules.tenancy.exceptions import BadRequestException
raise BadRequestException(f"Unknown module: {request.module_code}")
@@ -307,13 +307,13 @@ async def disable_module(
# Validate module code
if request.module_code not in MODULES:
from app.exceptions import BadRequestException
from app.modules.tenancy.exceptions import BadRequestException
raise BadRequestException(f"Unknown module: {request.module_code}")
# Check if core module
if request.module_code in get_core_module_codes():
from app.exceptions import BadRequestException
from app.modules.tenancy.exceptions import BadRequestException
raise BadRequestException(f"Cannot disable core module: {request.module_code}")

View File

@@ -1 +0,0 @@
# Platform monitoring and alerts

View File

@@ -1,22 +0,0 @@
# app/api/v1/platform/__init__.py
"""
Platform public API endpoints.
These endpoints are publicly accessible (no authentication required)
and serve the marketing homepage, pricing pages, and signup flows.
"""
from fastapi import APIRouter
from app.api.v1.platform import pricing, letzshop_vendors, signup
router = APIRouter()
# Public pricing and tier info
router.include_router(pricing.router, tags=["platform-pricing"])
# Letzshop vendor lookup
router.include_router(letzshop_vendors.router, tags=["platform-vendors"])
# Signup flow
router.include_router(signup.router, tags=["platform-signup"])

View File

@@ -0,0 +1,38 @@
# app/api/v1/public/__init__.py
"""
Public API endpoints (no authentication required).
Includes:
- signup: /signup/* (multi-step signup flow - cross-cutting)
Auto-discovers and aggregates public routes from self-contained modules:
- billing: /pricing/* (subscription tiers and add-ons)
- marketplace: /letzshop-vendors/* (vendor lookup for signup)
- core: /language/* (language preferences)
These endpoints serve the marketing homepage, pricing pages, and signup flows.
"""
from fastapi import APIRouter
from app.api.v1.public import signup
from app.modules.routes import get_public_api_routes
router = APIRouter()
# Cross-cutting signup flow (spans auth, vendors, billing, payments)
router.include_router(signup.router, tags=["public-signup"])
# Auto-discover public routes from modules
for route_info in get_public_api_routes():
if route_info.custom_prefix:
router.include_router(
route_info.router,
prefix=route_info.custom_prefix,
tags=route_info.tags,
)
else:
router.include_router(
route_info.router,
tags=route_info.tags,
)

View File

@@ -1,4 +1,4 @@
# app/api/v1/platform/signup.py
# app/api/v1/public/signup.py
"""
Platform signup API endpoints.
@@ -20,7 +20,7 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from app.core.environment import should_use_secure_cookies
from app.services.platform_signup_service import platform_signup_service
from app.modules.marketplace.services.platform_signup_service import platform_signup_service
router = APIRouter()
logger = logging.getLogger(__name__)

View File

@@ -1 +0,0 @@
# Health checks

View File

@@ -1 +0,0 @@
# File upload handling

View File

@@ -5,63 +5,48 @@ Storefront API router aggregation.
This module aggregates all storefront-related JSON API endpoints (public facing).
Uses vendor context from middleware - no vendor_id in URLs.
Endpoints:
- Products: Browse catalog, search products (catalog module)
- Cart: Shopping cart operations (cart module)
- Orders: Order history viewing (orders module)
- Checkout: Order placement (checkout module)
- Auth: Customer login, registration, password reset (customers module)
- Profile/Addresses: Customer profile management (customers module)
- Messages: Customer messaging (messaging module)
- Content Pages: CMS pages (cms module)
AUTO-DISCOVERED MODULE ROUTES:
- cart: Shopping cart operations
- catalog: Browse catalog, search products
- checkout: Order placement
- cms: Content pages
- customers: Auth, profile, addresses
- messaging: Customer messaging
- orders: Order history viewing
Authentication:
- Products, Cart, Content Pages: No auth required
- Orders, Profile, Messages: Requires customer authentication
- Auth: Public (login, register)
Note: Routes are now served from their respective modules.
Note: Routes are auto-discovered from app/modules/{module}/routes/api/storefront.py
"""
from fastapi import APIRouter
# Import module routers
from app.modules.cart.routes.api import storefront_router as cart_router
from app.modules.catalog.routes.api import storefront_router as catalog_router
from app.modules.checkout.routes.api import storefront_router as checkout_router
from app.modules.cms.routes.api.storefront import router as cms_storefront_router
from app.modules.customers.routes.api import storefront_router as customers_router
from app.modules.messaging.routes.api import storefront_router as messaging_router
from app.modules.orders.routes.api import storefront_router as orders_router
# Create storefront router
router = APIRouter()
# ============================================================================
# STOREFRONT API ROUTES (All vendor-context aware via middleware)
# Auto-discovered Module Routes
# ============================================================================
# Routes from self-contained modules are auto-discovered and registered.
# Modules include: cart, catalog, checkout, cms, customers, messaging, orders
# Customer authentication and account management (customers module)
router.include_router(customers_router, tags=["storefront-auth", "storefront-profile", "storefront-addresses"])
from app.modules.routes import get_storefront_api_routes
# Product catalog browsing (catalog module)
router.include_router(catalog_router, tags=["storefront-products"])
# Shopping cart (cart module)
router.include_router(cart_router, tags=["storefront-cart"])
# Order placement (checkout module)
router.include_router(checkout_router, tags=["storefront-checkout"])
# Order history viewing (orders module)
router.include_router(orders_router, tags=["storefront-orders"])
# Customer messaging (messaging module)
router.include_router(messaging_router, tags=["storefront-messages"])
# CMS content pages (cms module)
for route_info in get_storefront_api_routes():
# Only pass prefix if custom_prefix is set (router already has internal prefix)
if route_info.custom_prefix:
router.include_router(
cms_storefront_router, prefix="/content-pages", tags=["storefront-content-pages"]
route_info.router,
prefix=route_info.custom_prefix,
tags=route_info.tags,
)
else:
router.include_router(
route_info.router,
tags=route_info.tags,
)
__all__ = ["router"]

View File

@@ -0,0 +1,29 @@
# app/api/v1/webhooks/__init__.py
"""
Webhook API endpoints for external service callbacks.
Auto-discovers and aggregates webhook routes from self-contained modules:
- payments: /stripe (Stripe payment webhooks)
Webhooks use signature verification for security, not user authentication.
"""
from fastapi import APIRouter
from app.modules.routes import get_webhooks_api_routes
router = APIRouter()
# Auto-discover webhook routes from modules
for route_info in get_webhooks_api_routes():
if route_info.custom_prefix:
router.include_router(
route_info.router,
prefix=route_info.custom_prefix,
tags=route_info.tags,
)
else:
router.include_router(
route_info.router,
tags=route_info.tags,
)

View File

@@ -37,7 +37,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api
from app.core.database import get_db
from app.services.feature_service import feature_service
from app.modules.billing.services.feature_service import feature_service
from app.modules.billing.models import FeatureCode
from models.database.user import User

View File

@@ -102,7 +102,7 @@ def get_log_level_from_db():
"""
try:
from app.core.database import SessionLocal
from app.services.admin_settings_service import admin_settings_service
from app.modules.core.services.admin_settings_service import admin_settings_service
db = SessionLocal()
if not db:
@@ -127,7 +127,7 @@ def get_rotation_settings_from_db():
"""
try:
from app.core.database import SessionLocal
from app.services.admin_settings_service import admin_settings_service
from app.modules.core.services.admin_settings_service import admin_settings_service
db = SessionLocal()
if not db:

View File

@@ -1,45 +1,32 @@
# app/exceptions/__init__.py
"""
Custom exception classes for the API.
Base exception classes for the application.
This module provides frontend-friendly exceptions with consistent error codes,
messages, and HTTP status mappings.
This module provides only framework-level exceptions. Domain-specific exceptions
have been moved to their respective modules:
- tenancy: VendorNotFoundException, CompanyNotFoundException, etc.
- orders: OrderNotFoundException, InvoiceNotFoundException, etc.
- inventory: InventoryNotFoundException, InsufficientInventoryException, etc.
- billing: TierNotFoundException, SubscriptionNotFoundException, etc.
- marketplace: ImportJobNotFoundException, MarketplaceProductNotFoundException, etc.
- messaging: ConversationNotFoundException, MessageNotFoundException, etc.
- customers: CustomerNotFoundException, AddressNotFoundException, etc.
- cart: CartItemNotFoundException, EmptyCartException, etc.
- catalog: ProductNotFoundException, ProductValidationException, etc.
- cms: ContentPageNotFoundException, MediaNotFoundException, etc.
- monitoring: ScanNotFoundException, ViolationNotFoundException, etc.
Import pattern:
# Base exceptions (framework-level)
from app.exceptions import ValidationException, ResourceNotFoundException
# Domain exceptions (module-level)
from app.modules.orders.exceptions import OrderNotFoundException
from app.modules.tenancy.exceptions import VendorNotFoundException
"""
# Address exceptions
from .address import (
AddressLimitExceededException,
AddressNotFoundException,
InvalidAddressTypeException,
)
# Admin exceptions
from .admin import (
AdminOperationException,
BulkOperationException,
CannotModifyAdminException,
CannotModifySelfException,
ConfirmationRequiredException,
InvalidAdminActionException,
UserCannotBeDeletedException,
UserNotFoundException,
UserRoleChangeException,
UserStatusChangeException,
VendorVerificationException,
)
# Authentication exceptions
from .auth import (
AdminRequiredException,
InsufficientPermissionsException,
InvalidCredentialsException,
InvalidTokenException,
TokenExpiredException,
UserAlreadyExistsException,
UserNotActiveException,
)
# Base exceptions
# Base exceptions - these are the only exports from root
from .base import (
AuthenticationException,
AuthorizationException,
@@ -53,425 +40,21 @@ from .base import (
WizamartException,
)
# Billing exceptions
from .billing import (
InvalidWebhookSignatureException,
NoActiveSubscriptionException,
PaymentSystemNotConfiguredException,
StripePriceNotConfiguredException,
SubscriptionAlreadyCancelledException,
SubscriptionNotCancelledException,
TierNotFoundException,
WebhookMissingSignatureException,
)
# Cart exceptions
from .cart import (
CartItemNotFoundException,
CartValidationException,
EmptyCartException,
InsufficientInventoryForCartException,
InvalidCartQuantityException,
ProductNotAvailableForCartException,
)
# Code quality exceptions
from .code_quality import (
InvalidViolationStatusException,
ScanExecutionException,
ScanNotFoundException,
ScanParseException,
ScanTimeoutException,
ViolationNotFoundException,
ViolationOperationException,
)
# Company exceptions
from .company import (
CompanyAlreadyExistsException,
CompanyHasVendorsException,
CompanyNotActiveException,
CompanyNotFoundException,
CompanyNotVerifiedException,
CompanyValidationException,
InvalidCompanyDataException,
UnauthorizedCompanyAccessException,
)
# Customer exceptions
from .customer import (
CustomerAlreadyExistsException,
CustomerAuthorizationException,
CustomerNotActiveException,
CustomerNotFoundException,
CustomerValidationException,
DuplicateCustomerEmailException,
InvalidCustomerCredentialsException,
)
# Invoice exceptions
from .invoice import (
InvoiceNotFoundException,
InvoicePDFGenerationException,
InvoicePDFNotFoundException,
InvoiceSettingsAlreadyExistException,
InvoiceSettingsNotFoundException,
InvoiceValidationException,
InvalidInvoiceStatusTransitionException,
OrderNotFoundException as OrderNotFoundForInvoiceException,
)
# Inventory exceptions
from .inventory import (
InsufficientInventoryException,
InvalidInventoryOperationException,
InvalidQuantityException,
InventoryNotFoundException,
InventoryValidationException,
LocationNotFoundException,
NegativeInventoryException,
)
# Message exceptions
from .message import (
AttachmentNotFoundException,
ConversationClosedException,
ConversationNotFoundException,
InvalidConversationTypeException,
InvalidRecipientTypeException,
MessageAttachmentException,
MessageNotFoundException,
UnauthorizedConversationAccessException,
)
# Marketplace import job exceptions
from .marketplace_import_job import (
ImportJobAlreadyProcessingException,
ImportJobCannotBeCancelledException,
ImportJobCannotBeDeletedException,
ImportJobNotFoundException,
ImportJobNotOwnedException,
ImportRateLimitException,
InvalidImportDataException,
InvalidMarketplaceException,
MarketplaceConnectionException,
MarketplaceDataParsingException,
MarketplaceImportException,
)
# Marketplace product exceptions
from .marketplace_product import (
InvalidGTINException,
InvalidMarketplaceProductDataException,
MarketplaceProductAlreadyExistsException,
MarketplaceProductCSVImportException,
MarketplaceProductNotFoundException,
MarketplaceProductValidationException,
)
# Order exceptions
from .order import (
InvalidOrderStatusException,
OrderAlreadyExistsException,
OrderCannotBeCancelledException,
OrderNotFoundException,
OrderValidationException,
)
# Order item exception exceptions
from .order_item_exception import (
ExceptionAlreadyResolvedException,
InvalidProductForExceptionException,
OrderHasUnresolvedExceptionsException,
OrderItemExceptionNotFoundException,
)
# Product exceptions
from .product import (
CannotDeleteProductWithInventoryException,
CannotDeleteProductWithOrdersException,
InvalidProductDataException,
ProductAlreadyExistsException,
ProductNotActiveException,
ProductNotFoundException,
ProductNotInCatalogException,
ProductValidationException,
)
# Team exceptions
from .team import (
CannotModifyOwnRoleException,
CannotRemoveOwnerException,
InsufficientTeamPermissionsException,
InvalidInvitationDataException,
InvalidInvitationTokenException,
InvalidRoleException,
MaxTeamMembersReachedException,
RoleNotFoundException,
TeamInvitationAlreadyAcceptedException,
TeamInvitationExpiredException,
TeamInvitationNotFoundException,
TeamMemberAlreadyExistsException,
TeamMemberNotFoundException,
TeamValidationException,
UnauthorizedTeamActionException,
)
# Vendor exceptions
from .vendor import (
InsufficientVendorPermissionsException,
InvalidVendorDataException,
MaxVendorsReachedException,
UnauthorizedVendorAccessException,
VendorAccessDeniedException,
VendorAlreadyExistsException,
VendorNotActiveException,
VendorNotFoundException,
VendorNotVerifiedException,
VendorOwnerOnlyException,
VendorValidationException,
)
# Vendor domain exceptions
from .vendor_domain import (
DNSVerificationException,
DomainAlreadyVerifiedException,
DomainNotVerifiedException,
DomainVerificationFailedException,
InvalidDomainFormatException,
MaxDomainsReachedException,
MultiplePrimaryDomainsException,
ReservedDomainException,
UnauthorizedDomainAccessException,
VendorDomainAlreadyExistsException,
VendorDomainNotFoundException,
)
# Vendor theme exceptions
from .vendor_theme import (
InvalidColorFormatException,
InvalidFontFamilyException,
InvalidThemeDataException,
ThemeOperationException,
ThemePresetAlreadyAppliedException,
ThemePresetNotFoundException,
ThemeValidationException,
VendorThemeNotFoundException,
)
# Onboarding exceptions
from .onboarding import (
LetzshopConnectionFailedException,
OnboardingAlreadyCompletedException,
OnboardingCsvUrlRequiredException,
OnboardingNotFoundException,
OnboardingStepOrderException,
OnboardingSyncJobNotFoundException,
OnboardingSyncNotCompleteException,
)
# Feature management exceptions
from .feature import (
FeatureNotFoundError,
InvalidFeatureCodesError,
TierNotFoundError,
)
__all__ = [
# Base exceptions
# Base exception class
"WizamartException",
# Validation and business logic
"ValidationException",
"BusinessLogicException",
# Authentication and authorization
"AuthenticationException",
"AuthorizationException",
# Resource operations
"ResourceNotFoundException",
"ConflictException",
"BusinessLogicException",
# External services
"ExternalServiceException",
"RateLimitException",
"ServiceUnavailableException",
# Auth exceptions
"InvalidCredentialsException",
"TokenExpiredException",
"InvalidTokenException",
"InsufficientPermissionsException",
"UserNotActiveException",
"AdminRequiredException",
"UserAlreadyExistsException",
# Customer exceptions
"CustomerNotFoundException",
"CustomerAlreadyExistsException",
"DuplicateCustomerEmailException",
"CustomerNotActiveException",
"InvalidCustomerCredentialsException",
"CustomerValidationException",
"CustomerAuthorizationException",
# Team exceptions
"TeamMemberNotFoundException",
"TeamMemberAlreadyExistsException",
"TeamInvitationNotFoundException",
"TeamInvitationExpiredException",
"TeamInvitationAlreadyAcceptedException",
"UnauthorizedTeamActionException",
"CannotRemoveOwnerException",
"CannotModifyOwnRoleException",
"RoleNotFoundException",
"InvalidRoleException",
"InsufficientTeamPermissionsException",
"MaxTeamMembersReachedException",
"TeamValidationException",
"InvalidInvitationDataException",
"InvalidInvitationTokenException",
# Invoice exceptions
"InvoiceNotFoundException",
"InvoiceSettingsNotFoundException",
"InvoiceSettingsAlreadyExistException",
"InvoiceValidationException",
"InvoicePDFGenerationException",
"InvoicePDFNotFoundException",
"InvalidInvoiceStatusTransitionException",
"OrderNotFoundForInvoiceException",
# Inventory exceptions
"InventoryNotFoundException",
"InsufficientInventoryException",
"InvalidInventoryOperationException",
"InventoryValidationException",
"NegativeInventoryException",
"InvalidQuantityException",
"LocationNotFoundException",
# Vendor exceptions
"InsufficientVendorPermissionsException",
"InvalidVendorDataException",
"MaxVendorsReachedException",
"UnauthorizedVendorAccessException",
"VendorAccessDeniedException",
"VendorAlreadyExistsException",
"VendorNotActiveException",
"VendorNotFoundException",
"VendorNotVerifiedException",
"VendorOwnerOnlyException",
"VendorValidationException",
# Vendor Domain
"VendorDomainNotFoundException",
"VendorDomainAlreadyExistsException",
"InvalidDomainFormatException",
"ReservedDomainException",
"DomainNotVerifiedException",
"DomainVerificationFailedException",
"DomainAlreadyVerifiedException",
"MultiplePrimaryDomainsException",
"DNSVerificationException",
"MaxDomainsReachedException",
"UnauthorizedDomainAccessException",
# Vendor Theme
"VendorThemeNotFoundException",
"InvalidThemeDataException",
"ThemePresetNotFoundException",
"ThemeValidationException",
"ThemePresetAlreadyAppliedException",
"InvalidColorFormatException",
"InvalidFontFamilyException",
"ThemeOperationException",
# Product exceptions
"ProductNotFoundException",
"ProductAlreadyExistsException",
"ProductNotInCatalogException",
"ProductNotActiveException",
"InvalidProductDataException",
"ProductValidationException",
"CannotDeleteProductWithInventoryException",
"CannotDeleteProductWithOrdersException",
# Order exceptions
"OrderNotFoundException",
"OrderAlreadyExistsException",
"OrderValidationException",
"InvalidOrderStatusException",
"OrderCannotBeCancelledException",
# Order item exception exceptions
"OrderItemExceptionNotFoundException",
"OrderHasUnresolvedExceptionsException",
"ExceptionAlreadyResolvedException",
"InvalidProductForExceptionException",
# Cart exceptions
"CartItemNotFoundException",
"EmptyCartException",
"CartValidationException",
"InsufficientInventoryForCartException",
"InvalidCartQuantityException",
"ProductNotAvailableForCartException",
# Company exceptions
"CompanyNotFoundException",
"CompanyAlreadyExistsException",
"CompanyNotActiveException",
"CompanyNotVerifiedException",
"UnauthorizedCompanyAccessException",
"InvalidCompanyDataException",
"CompanyValidationException",
"CompanyHasVendorsException",
# MarketplaceProduct exceptions
"MarketplaceProductNotFoundException",
"MarketplaceProductAlreadyExistsException",
"InvalidMarketplaceProductDataException",
"MarketplaceProductValidationException",
"InvalidGTINException",
"MarketplaceProductCSVImportException",
# Marketplace import exceptions
"MarketplaceImportException",
"ImportJobNotFoundException",
"ImportJobNotOwnedException",
"InvalidImportDataException",
"ImportJobCannotBeCancelledException",
"ImportJobCannotBeDeletedException",
"MarketplaceConnectionException",
"MarketplaceDataParsingException",
"ImportRateLimitException",
"InvalidMarketplaceException",
"ImportJobAlreadyProcessingException",
# Admin exceptions
"UserNotFoundException",
"UserStatusChangeException",
"VendorVerificationException",
"AdminOperationException",
"CannotModifyAdminException",
"CannotModifySelfException",
"InvalidAdminActionException",
"BulkOperationException",
"ConfirmationRequiredException",
# Code quality exceptions
"ViolationNotFoundException",
"ScanNotFoundException",
"ScanExecutionException",
"ScanTimeoutException",
"ScanParseException",
"ViolationOperationException",
"InvalidViolationStatusException",
# Message exceptions
"ConversationNotFoundException",
"MessageNotFoundException",
"ConversationClosedException",
"MessageAttachmentException",
"UnauthorizedConversationAccessException",
"InvalidConversationTypeException",
"InvalidRecipientTypeException",
"AttachmentNotFoundException",
# Billing exceptions
"PaymentSystemNotConfiguredException",
"TierNotFoundException",
"StripePriceNotConfiguredException",
"NoActiveSubscriptionException",
"SubscriptionNotCancelledException",
"SubscriptionAlreadyCancelledException",
"InvalidWebhookSignatureException",
"WebhookMissingSignatureException",
# Onboarding exceptions
"OnboardingNotFoundException",
"OnboardingStepOrderException",
"OnboardingAlreadyCompletedException",
"LetzshopConnectionFailedException",
"OnboardingCsvUrlRequiredException",
"OnboardingSyncJobNotFoundException",
"OnboardingSyncNotCompleteException",
# Feature exceptions
"FeatureNotFoundError",
"TierNotFoundError",
"InvalidFeatureCodesError",
# Rate limiting
"RateLimitException",
]

View File

@@ -1,38 +0,0 @@
# app/exceptions/address.py
"""
Address-related custom exceptions.
Used for customer address management operations.
"""
from .base import BusinessLogicException, ResourceNotFoundException
class AddressNotFoundException(ResourceNotFoundException):
"""Raised when a customer address is not found."""
def __init__(self, address_id: str | int):
super().__init__(
resource_type="Address",
identifier=str(address_id),
)
class AddressLimitExceededException(BusinessLogicException):
"""Raised when customer exceeds maximum number of addresses."""
def __init__(self, max_addresses: int = 10):
super().__init__(
message=f"Maximum number of addresses ({max_addresses}) reached",
error_code="ADDRESS_LIMIT_EXCEEDED",
)
class InvalidAddressTypeException(BusinessLogicException):
"""Raised when an invalid address type is provided."""
def __init__(self, address_type: str):
super().__init__(
message=f"Invalid address type '{address_type}'. Must be 'shipping' or 'billing'",
error_code="INVALID_ADDRESS_TYPE",
)

View File

@@ -1,248 +0,0 @@
# app/exceptions/admin.py
"""
Admin operations specific exceptions.
"""
from typing import Any
from .base import (
AuthorizationException,
BusinessLogicException,
ResourceNotFoundException,
ValidationException,
)
class UserNotFoundException(ResourceNotFoundException):
"""Raised when user is not found in admin operations."""
def __init__(self, user_identifier: str, identifier_type: str = "ID"):
if identifier_type.lower() == "username":
message = f"User with username '{user_identifier}' not found"
elif identifier_type.lower() == "email":
message = f"User with email '{user_identifier}' not found"
else:
message = f"User with ID '{user_identifier}' not found"
super().__init__(
resource_type="User",
identifier=user_identifier,
message=message,
error_code="USER_NOT_FOUND",
)
class UserStatusChangeException(BusinessLogicException):
"""Raised when user status cannot be changed."""
def __init__(
self,
user_id: int,
current_status: str,
attempted_action: str,
reason: str | None = None,
):
message = f"Cannot {attempted_action} user {user_id} (current status: {current_status})"
if reason:
message += f": {reason}"
super().__init__(
message=message,
error_code="USER_STATUS_CHANGE_FAILED",
details={
"user_id": user_id,
"current_status": current_status,
"attempted_action": attempted_action,
"reason": reason,
},
)
class AdminOperationException(BusinessLogicException):
"""Raised when admin operation fails."""
def __init__(
self,
operation: str,
reason: str,
target_type: str | None = None,
target_id: str | None = None,
):
message = f"Admin operation '{operation}' failed: {reason}"
details = {
"operation": operation,
"reason": reason,
}
if target_type:
details["target_type"] = target_type
if target_id:
details["target_id"] = target_id
super().__init__(
message=message,
error_code="ADMIN_OPERATION_FAILED",
details=details,
)
class CannotModifyAdminException(AuthorizationException):
"""Raised when trying to modify another admin user."""
def __init__(self, target_user_id: int, admin_user_id: int):
super().__init__(
message=f"Cannot modify admin user {target_user_id}",
error_code="CANNOT_MODIFY_ADMIN",
details={
"target_user_id": target_user_id,
"admin_user_id": admin_user_id,
},
)
class CannotModifySelfException(BusinessLogicException):
"""Raised when admin tries to modify their own status."""
def __init__(self, user_id: int, operation: str):
super().__init__(
message=f"Cannot perform '{operation}' on your own account",
error_code="CANNOT_MODIFY_SELF",
details={
"user_id": user_id,
"operation": operation,
},
)
class InvalidAdminActionException(ValidationException):
"""Raised when admin action is invalid."""
def __init__(
self,
action: str,
reason: str,
valid_actions: list | None = None,
):
details = {
"action": action,
"reason": reason,
}
if valid_actions:
details["valid_actions"] = valid_actions
super().__init__(
message=f"Invalid admin action '{action}': {reason}",
details=details,
)
self.error_code = "INVALID_ADMIN_ACTION"
class BulkOperationException(BusinessLogicException):
"""Raised when bulk admin operation fails."""
def __init__(
self,
operation: str,
total_items: int,
failed_items: int,
errors: dict[str, Any] | None = None,
):
message = f"Bulk {operation} completed with errors: {failed_items}/{total_items} failed"
details = {
"operation": operation,
"total_items": total_items,
"failed_items": failed_items,
"success_items": total_items - failed_items,
}
if errors:
details["errors"] = errors
super().__init__(
message=message,
error_code="BULK_OPERATION_PARTIAL_FAILURE",
details=details,
)
class ConfirmationRequiredException(BusinessLogicException):
"""Raised when a destructive operation requires explicit confirmation."""
def __init__(
self,
operation: str,
message: str | None = None,
confirmation_param: str = "confirm",
):
if not message:
message = f"Operation '{operation}' requires confirmation parameter: {confirmation_param}=true"
super().__init__(
message=message,
error_code="CONFIRMATION_REQUIRED",
details={
"operation": operation,
"confirmation_param": confirmation_param,
},
)
class VendorVerificationException(BusinessLogicException):
"""Raised when vendor verification fails."""
def __init__(
self,
vendor_id: int,
reason: str,
current_verification_status: bool | None = None,
):
details = {
"vendor_id": vendor_id,
"reason": reason,
}
if current_verification_status is not None:
details["current_verification_status"] = current_verification_status
super().__init__(
message=f"Vendor verification failed for vendor {vendor_id}: {reason}",
error_code="VENDOR_VERIFICATION_FAILED",
details=details,
)
class UserCannotBeDeletedException(BusinessLogicException):
"""Raised when a user cannot be deleted due to ownership constraints."""
def __init__(self, user_id: int, reason: str, owned_count: int = 0):
details = {
"user_id": user_id,
"reason": reason,
}
if owned_count > 0:
details["owned_companies_count"] = owned_count
super().__init__(
message=f"Cannot delete user {user_id}: {reason}",
error_code="USER_CANNOT_BE_DELETED",
details=details,
)
class UserRoleChangeException(BusinessLogicException):
"""Raised when user role cannot be changed."""
def __init__(self, user_id: int, current_role: str, target_role: str, reason: str):
super().__init__(
message=f"Cannot change user {user_id} role from {current_role} to {target_role}: {reason}",
error_code="USER_ROLE_CHANGE_FAILED",
details={
"user_id": user_id,
"current_role": current_role,
"target_role": target_role,
"reason": reason,
},
)

View File

@@ -1,94 +0,0 @@
# app/exceptions/auth.py
"""
Authentication and authorization specific exceptions.
"""
from .base import AuthenticationException, AuthorizationException, ConflictException
class InvalidCredentialsException(AuthenticationException):
"""Raised when login credentials are invalid."""
def __init__(self, message: str = "Invalid username or password"):
super().__init__(
message=message,
error_code="INVALID_CREDENTIALS",
)
class TokenExpiredException(AuthenticationException):
"""Raised when JWT token has expired."""
def __init__(self, message: str = "Token has expired"):
super().__init__(
message=message,
error_code="TOKEN_EXPIRED",
)
class InvalidTokenException(AuthenticationException):
"""Raised when JWT token is invalid or malformed."""
def __init__(self, message: str = "Invalid token"):
super().__init__(
message=message,
error_code="INVALID_TOKEN",
)
class InsufficientPermissionsException(AuthorizationException):
"""Raised when user lacks required permissions for an action."""
def __init__(
self,
message: str = "Insufficient permissions for this action",
required_permission: str | None = None,
):
details = {}
if required_permission:
details["required_permission"] = required_permission
super().__init__(
message=message,
error_code="INSUFFICIENT_PERMISSIONS",
details=details,
)
class UserNotActiveException(AuthorizationException):
"""Raised when user account is not active."""
def __init__(self, message: str = "User account is not active"):
super().__init__(
message=message,
error_code="USER_NOT_ACTIVE",
)
class AdminRequiredException(AuthorizationException):
"""Raised when admin privileges are required."""
def __init__(self, message: str = "Admin privileges required"):
super().__init__(
message=message,
error_code="ADMIN_REQUIRED",
)
class UserAlreadyExistsException(ConflictException):
"""Raised when trying to register with existing username/email."""
def __init__(
self,
message: str = "User already exists",
field: str | None = None,
):
details = {}
if field:
details["field"] = field
super().__init__(
message=message,
error_code="USER_ALREADY_EXISTS",
details=details,
)

View File

@@ -1 +0,0 @@
# Backup/recovery exceptions

View File

@@ -1,95 +0,0 @@
# app/exceptions/billing.py
"""
Billing and subscription related exceptions.
This module provides exceptions for:
- Payment system configuration issues
- Subscription management errors
- Tier-related errors
"""
from typing import Any
from .base import BusinessLogicException, ResourceNotFoundException, ServiceUnavailableException
class PaymentSystemNotConfiguredException(ServiceUnavailableException):
"""Raised when the payment system (Stripe) is not configured."""
def __init__(self):
super().__init__(message="Payment system not configured")
class TierNotFoundException(ResourceNotFoundException):
"""Raised when a subscription tier is not found."""
def __init__(self, tier_code: str):
super().__init__(
resource_type="SubscriptionTier",
identifier=tier_code,
message=f"Subscription tier '{tier_code}' not found",
error_code="TIER_NOT_FOUND",
)
self.tier_code = tier_code
class StripePriceNotConfiguredException(BusinessLogicException):
"""Raised when Stripe price is not configured for a tier."""
def __init__(self, tier_code: str):
super().__init__(
message=f"Stripe price not configured for tier '{tier_code}'",
error_code="STRIPE_PRICE_NOT_CONFIGURED",
details={"tier_code": tier_code},
)
self.tier_code = tier_code
class NoActiveSubscriptionException(BusinessLogicException):
"""Raised when no active subscription exists for an operation that requires one."""
def __init__(self, message: str = "No active subscription found"):
super().__init__(
message=message,
error_code="NO_ACTIVE_SUBSCRIPTION",
)
class SubscriptionNotCancelledException(BusinessLogicException):
"""Raised when trying to reactivate a subscription that is not cancelled."""
def __init__(self):
super().__init__(
message="Subscription is not cancelled",
error_code="SUBSCRIPTION_NOT_CANCELLED",
)
class SubscriptionAlreadyCancelledException(BusinessLogicException):
"""Raised when trying to cancel an already cancelled subscription."""
def __init__(self):
super().__init__(
message="Subscription is already cancelled",
error_code="SUBSCRIPTION_ALREADY_CANCELLED",
)
class InvalidWebhookSignatureException(BusinessLogicException):
"""Raised when Stripe webhook signature verification fails."""
def __init__(self, message: str = "Invalid webhook signature"):
super().__init__(
message=message,
error_code="INVALID_WEBHOOK_SIGNATURE",
)
class WebhookMissingSignatureException(BusinessLogicException):
"""Raised when Stripe webhook is missing the signature header."""
def __init__(self):
super().__init__(
message="Missing Stripe-Signature header",
error_code="WEBHOOK_MISSING_SIGNATURE",
)

View File

@@ -1,107 +0,0 @@
# app/exceptions/cart.py
"""
Shopping cart specific exceptions.
"""
from .base import BusinessLogicException, ResourceNotFoundException, ValidationException
class CartItemNotFoundException(ResourceNotFoundException):
"""Raised when a cart item is not found."""
def __init__(self, product_id: int, session_id: str):
super().__init__(
resource_type="CartItem",
identifier=str(product_id),
message=f"Product {product_id} not found in cart",
error_code="CART_ITEM_NOT_FOUND",
)
self.details.update({"product_id": product_id, "session_id": session_id})
class EmptyCartException(ValidationException):
"""Raised when trying to perform operations on an empty cart."""
def __init__(self, session_id: str):
super().__init__(message="Cart is empty", details={"session_id": session_id})
self.error_code = "CART_EMPTY"
class CartValidationException(ValidationException):
"""Raised when cart data validation fails."""
def __init__(
self,
message: str = "Cart validation failed",
field: str | None = None,
details: dict | None = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "CART_VALIDATION_FAILED"
class InsufficientInventoryForCartException(BusinessLogicException):
"""Raised when product doesn't have enough inventory for cart operation."""
def __init__(
self,
product_id: int,
product_name: str,
requested: int,
available: int,
):
message = f"Insufficient inventory for product '{product_name}'. Requested: {requested}, Available: {available}"
super().__init__(
message=message,
error_code="INSUFFICIENT_INVENTORY_FOR_CART",
details={
"product_id": product_id,
"product_name": product_name,
"requested_quantity": requested,
"available_quantity": available,
},
)
class InvalidCartQuantityException(ValidationException):
"""Raised when cart quantity is invalid."""
def __init__(
self, quantity: int, min_quantity: int = 1, max_quantity: int | None = None
):
if quantity < min_quantity:
message = f"Quantity must be at least {min_quantity}"
elif max_quantity and quantity > max_quantity:
message = f"Quantity cannot exceed {max_quantity}"
else:
message = f"Invalid quantity: {quantity}"
super().__init__(
message=message,
field="quantity",
details={
"quantity": quantity,
"min_quantity": min_quantity,
"max_quantity": max_quantity,
},
)
self.error_code = "INVALID_CART_QUANTITY"
class ProductNotAvailableForCartException(BusinessLogicException):
"""Raised when product is not available for adding to cart."""
def __init__(self, product_id: int, reason: str):
super().__init__(
message=f"Product {product_id} cannot be added to cart: {reason}",
error_code="PRODUCT_NOT_AVAILABLE_FOR_CART",
details={
"product_id": product_id,
"reason": reason,
},
)

View File

@@ -1,98 +0,0 @@
# app/exceptions/code_quality.py
"""
Code Quality Domain Exceptions
These exceptions are raised by the code quality service layer
and converted to HTTP responses by the global exception handler.
Note: These exceptions are also re-exported from app.modules.dev_tools.exceptions
for module-based access.
"""
from app.exceptions.base import (
BusinessLogicException,
ExternalServiceException,
ResourceNotFoundException,
ValidationException,
)
class ViolationNotFoundException(ResourceNotFoundException):
"""Raised when a violation is not found."""
def __init__(self, violation_id: int):
super().__init__(
resource_type="Violation",
identifier=str(violation_id),
error_code="VIOLATION_NOT_FOUND",
)
class ScanNotFoundException(ResourceNotFoundException):
"""Raised when a scan is not found."""
def __init__(self, scan_id: int):
super().__init__(
resource_type="Scan",
identifier=str(scan_id),
error_code="SCAN_NOT_FOUND",
)
class ScanExecutionException(ExternalServiceException):
"""Raised when architecture scan execution fails."""
def __init__(self, reason: str):
super().__init__(
service_name="ArchitectureValidator",
message=f"Scan execution failed: {reason}",
error_code="SCAN_EXECUTION_FAILED",
)
class ScanTimeoutException(ExternalServiceException):
"""Raised when architecture scan times out."""
def __init__(self, timeout_seconds: int = 300):
super().__init__(
service_name="ArchitectureValidator",
message=f"Scan timed out after {timeout_seconds} seconds",
error_code="SCAN_TIMEOUT",
)
class ScanParseException(BusinessLogicException):
"""Raised when scan results cannot be parsed."""
def __init__(self, reason: str):
super().__init__(
message=f"Failed to parse scan results: {reason}",
error_code="SCAN_PARSE_FAILED",
)
class ViolationOperationException(BusinessLogicException):
"""Raised when a violation operation fails."""
def __init__(self, operation: str, violation_id: int, reason: str):
super().__init__(
message=f"Failed to {operation} violation {violation_id}: {reason}",
error_code="VIOLATION_OPERATION_FAILED",
details={
"operation": operation,
"violation_id": violation_id,
"reason": reason,
},
)
class InvalidViolationStatusException(ValidationException):
"""Raised when a violation status transition is invalid."""
def __init__(self, violation_id: int, current_status: str, target_status: str):
super().__init__(
message=f"Cannot change violation {violation_id} from '{current_status}' to '{target_status}'",
field="status",
value=target_status,
)
self.error_code = "INVALID_VIOLATION_STATUS"

View File

@@ -1,128 +0,0 @@
# app/exceptions/company.py
"""
Company management specific exceptions.
"""
from typing import Any
from .base import (
AuthorizationException,
BusinessLogicException,
ConflictException,
ResourceNotFoundException,
ValidationException,
)
class CompanyNotFoundException(ResourceNotFoundException):
"""Raised when a company is not found."""
def __init__(self, company_identifier: str | int, identifier_type: str = "id"):
if identifier_type.lower() == "id":
message = f"Company with ID '{company_identifier}' not found"
else:
message = f"Company with name '{company_identifier}' not found"
super().__init__(
resource_type="Company",
identifier=str(company_identifier),
message=message,
error_code="COMPANY_NOT_FOUND",
)
class CompanyAlreadyExistsException(ConflictException):
"""Raised when trying to create a company that already exists."""
def __init__(self, company_name: str):
super().__init__(
message=f"Company with name '{company_name}' already exists",
error_code="COMPANY_ALREADY_EXISTS",
details={"company_name": company_name},
)
class CompanyNotActiveException(BusinessLogicException):
"""Raised when trying to perform operations on inactive company."""
def __init__(self, company_id: int):
super().__init__(
message=f"Company with ID '{company_id}' is not active",
error_code="COMPANY_NOT_ACTIVE",
details={"company_id": company_id},
)
class CompanyNotVerifiedException(BusinessLogicException):
"""Raised when trying to perform operations requiring verified company."""
def __init__(self, company_id: int):
super().__init__(
message=f"Company with ID '{company_id}' is not verified",
error_code="COMPANY_NOT_VERIFIED",
details={"company_id": company_id},
)
class UnauthorizedCompanyAccessException(AuthorizationException):
"""Raised when user tries to access company they don't own."""
def __init__(self, company_id: int, user_id: int | None = None):
details = {"company_id": company_id}
if user_id:
details["user_id"] = user_id
super().__init__(
message=f"Unauthorized access to company with ID '{company_id}'",
error_code="UNAUTHORIZED_COMPANY_ACCESS",
details=details,
)
class InvalidCompanyDataException(ValidationException):
"""Raised when company data is invalid or incomplete."""
def __init__(
self,
message: str = "Invalid company data",
field: str | None = None,
details: dict[str, Any] | None = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_COMPANY_DATA"
class CompanyValidationException(ValidationException):
"""Raised when company validation fails."""
def __init__(
self,
message: str = "Company validation failed",
field: str | None = None,
validation_errors: dict[str, str] | None = None,
):
details = {}
if validation_errors:
details["validation_errors"] = validation_errors
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "COMPANY_VALIDATION_FAILED"
class CompanyHasVendorsException(BusinessLogicException):
"""Raised when trying to delete a company that still has active vendors."""
def __init__(self, company_id: int, vendor_count: int):
super().__init__(
message=f"Cannot delete company with ID '{company_id}' because it has {vendor_count} associated vendor(s)",
error_code="COMPANY_HAS_VENDORS",
details={"company_id": company_id, "vendor_count": vendor_count},
)

View File

@@ -1,116 +0,0 @@
# app/exceptions/customer.py
"""
Customer management specific exceptions.
"""
from typing import Any
from .base import (
AuthenticationException,
BusinessLogicException,
ConflictException,
ResourceNotFoundException,
ValidationException,
)
class CustomerNotFoundException(ResourceNotFoundException):
"""Raised when a customer is not found."""
def __init__(self, customer_identifier: str):
super().__init__(
resource_type="Customer",
identifier=customer_identifier,
message=f"Customer '{customer_identifier}' not found",
error_code="CUSTOMER_NOT_FOUND",
)
class CustomerAlreadyExistsException(ConflictException):
"""Raised when trying to create a customer that already exists."""
def __init__(self, email: str):
super().__init__(
message=f"Customer with email '{email}' already exists",
error_code="CUSTOMER_ALREADY_EXISTS",
details={"email": email},
)
class DuplicateCustomerEmailException(ConflictException):
"""Raised when email already exists for vendor."""
def __init__(self, email: str, vendor_code: str):
super().__init__(
message=f"Email '{email}' is already registered for this vendor",
error_code="DUPLICATE_CUSTOMER_EMAIL",
details={"email": email, "vendor_code": vendor_code},
)
class CustomerNotActiveException(BusinessLogicException):
"""Raised when trying to perform operations on inactive customer."""
def __init__(self, email: str):
super().__init__(
message=f"Customer account '{email}' is not active",
error_code="CUSTOMER_NOT_ACTIVE",
details={"email": email},
)
class InvalidCustomerCredentialsException(AuthenticationException):
"""Raised when customer credentials are invalid."""
def __init__(self):
super().__init__(
message="Invalid email or password",
error_code="INVALID_CUSTOMER_CREDENTIALS",
)
class CustomerValidationException(ValidationException):
"""Raised when customer data validation fails."""
def __init__(
self,
message: str = "Customer validation failed",
field: str | None = None,
details: dict[str, Any] | None = None,
):
super().__init__(message=message, field=field, details=details)
self.error_code = "CUSTOMER_VALIDATION_FAILED"
class CustomerAuthorizationException(BusinessLogicException):
"""Raised when customer is not authorized for operation."""
def __init__(self, customer_email: str, operation: str):
super().__init__(
message=f"Customer '{customer_email}' not authorized for: {operation}",
error_code="CUSTOMER_NOT_AUTHORIZED",
details={"customer_email": customer_email, "operation": operation},
)
class InvalidPasswordResetTokenException(ValidationException):
"""Raised when password reset token is invalid or expired."""
def __init__(self):
super().__init__(
message="Invalid or expired password reset link. Please request a new one.",
field="reset_token",
)
self.error_code = "INVALID_RESET_TOKEN"
class PasswordTooShortException(ValidationException):
"""Raised when password doesn't meet minimum length requirement."""
def __init__(self, min_length: int = 8):
super().__init__(
message=f"Password must be at least {min_length} characters long",
field="password",
details={"min_length": min_length},
)
self.error_code = "PASSWORD_TOO_SHORT"

View File

@@ -1,42 +0,0 @@
# app/exceptions/feature.py
"""
Feature management exceptions.
"""
from app.exceptions.base import ResourceNotFoundException, ValidationException
class FeatureNotFoundError(ResourceNotFoundException):
"""Feature not found."""
def __init__(self, feature_code: str):
super().__init__(
resource_type="Feature",
identifier=feature_code,
message=f"Feature '{feature_code}' not found",
)
self.feature_code = feature_code
class TierNotFoundError(ResourceNotFoundException):
"""Subscription tier not found."""
def __init__(self, tier_code: str):
super().__init__(
resource_type="SubscriptionTier",
identifier=tier_code,
message=f"Tier '{tier_code}' not found",
)
self.tier_code = tier_code
class InvalidFeatureCodesError(ValidationException):
"""Invalid feature codes provided."""
def __init__(self, invalid_codes: set[str]):
codes_str = ", ".join(sorted(invalid_codes))
super().__init__(
message=f"Invalid feature codes: {codes_str}",
details={"invalid_codes": list(invalid_codes)},
)
self.invalid_codes = invalid_codes

View File

@@ -1,135 +0,0 @@
# app/exceptions/inventory.py
"""
Inventory management specific exceptions.
"""
from typing import Any
from .base import BusinessLogicException, ResourceNotFoundException, ValidationException
class InventoryNotFoundException(ResourceNotFoundException):
"""Raised when inventory record is not found."""
def __init__(self, identifier: str, identifier_type: str = "ID"):
if identifier_type.lower() == "gtin":
message = f"No inventory found for GTIN '{identifier}'"
else:
message = (
f"Inventory record with {identifier_type} '{identifier}' not found"
)
super().__init__(
resource_type="Inventory",
identifier=identifier,
message=message,
error_code="INVENTORY_NOT_FOUND",
)
class InsufficientInventoryException(BusinessLogicException):
"""Raised when trying to remove more inventory than available."""
def __init__(
self,
gtin: str,
location: str,
requested: int,
available: int,
):
message = f"Insufficient inventory for GTIN '{gtin}' at '{location}'. Requested: {requested}, Available: {available}"
super().__init__(
message=message,
error_code="INSUFFICIENT_INVENTORY",
details={
"gtin": gtin,
"location": location,
"requested_quantity": requested,
"available_quantity": available,
},
)
class InvalidInventoryOperationException(ValidationException):
"""Raised when inventory operation is invalid."""
def __init__(
self,
message: str,
operation: str | None = None,
details: dict[str, Any] | None = None,
):
if not details:
details = {}
if operation:
details["operation"] = operation
super().__init__(
message=message,
details=details,
)
self.error_code = "INVALID_INVENTORY_OPERATION"
class InventoryValidationException(ValidationException):
"""Raised when inventory data validation fails."""
def __init__(
self,
message: str = "Inventory validation failed",
field: str | None = None,
validation_errors: dict[str, str] | None = None,
):
details = {}
if validation_errors:
details["validation_errors"] = validation_errors
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVENTORY_VALIDATION_FAILED"
class NegativeInventoryException(BusinessLogicException):
"""Raised when inventory quantity would become negative."""
def __init__(self, gtin: str, location: str, resulting_quantity: int):
message = f"Inventory operation would result in negative quantity ({resulting_quantity}) for GTIN '{gtin}' at '{location}'"
super().__init__(
message=message,
error_code="NEGATIVE_INVENTORY_NOT_ALLOWED",
details={
"gtin": gtin,
"location": location,
"resulting_quantity": resulting_quantity,
},
)
class InvalidQuantityException(ValidationException):
"""Raised when quantity value is invalid."""
def __init__(self, quantity: Any, message: str = "Invalid quantity"):
super().__init__(
message=f"{message}: {quantity}",
field="quantity",
details={"quantity": quantity},
)
self.error_code = "INVALID_QUANTITY"
class LocationNotFoundException(ResourceNotFoundException):
"""Raised when inventory location is not found."""
def __init__(self, location: str):
super().__init__(
resource_type="Location",
identifier=location,
message=f"Inventory location '{location}' not found",
error_code="LOCATION_NOT_FOUND",
)

View File

@@ -1,118 +0,0 @@
# app/exceptions/invoice.py
"""
Invoice-related exceptions.
This module provides exception classes for invoice operations including:
- Invoice not found errors
- Invoice settings validation
- PDF generation errors
- Invoice status transitions
"""
from typing import Any
from .base import BusinessLogicException, ResourceNotFoundException, WizamartException
class InvoiceNotFoundException(ResourceNotFoundException):
"""Raised when an invoice is not found."""
def __init__(self, invoice_id: int | str):
super().__init__(
resource_type="Invoice",
identifier=str(invoice_id),
error_code="INVOICE_NOT_FOUND",
)
class InvoiceSettingsNotFoundException(ResourceNotFoundException):
"""Raised when invoice settings are not found for a vendor."""
def __init__(self, vendor_id: int):
super().__init__(
resource_type="InvoiceSettings",
identifier=str(vendor_id),
message="Invoice settings not found. Create settings first.",
error_code="INVOICE_SETTINGS_NOT_FOUND",
)
class InvoiceSettingsAlreadyExistException(WizamartException):
"""Raised when trying to create invoice settings that already exist."""
def __init__(self, vendor_id: int):
super().__init__(
message=f"Invoice settings already exist for vendor {vendor_id}",
error_code="INVOICE_SETTINGS_ALREADY_EXIST",
status_code=409,
details={"vendor_id": vendor_id},
)
class InvoiceValidationException(BusinessLogicException):
"""Raised when invoice data validation fails."""
def __init__(self, message: str, details: dict[str, Any] | None = None):
super().__init__(
message=message,
error_code="INVOICE_VALIDATION_ERROR",
details=details,
)
class InvoicePDFGenerationException(WizamartException):
"""Raised when PDF generation fails."""
def __init__(self, invoice_id: int, reason: str):
super().__init__(
message=f"Failed to generate PDF for invoice {invoice_id}: {reason}",
error_code="INVOICE_PDF_GENERATION_FAILED",
status_code=500,
details={"invoice_id": invoice_id, "reason": reason},
)
class InvoicePDFNotFoundException(ResourceNotFoundException):
"""Raised when invoice PDF file is not found."""
def __init__(self, invoice_id: int):
super().__init__(
resource_type="InvoicePDF",
identifier=str(invoice_id),
message="PDF file not found. Generate the PDF first.",
error_code="INVOICE_PDF_NOT_FOUND",
)
class InvalidInvoiceStatusTransitionException(BusinessLogicException):
"""Raised when an invalid invoice status transition is attempted."""
def __init__(
self,
current_status: str,
new_status: str,
reason: str | None = None,
):
message = f"Cannot change invoice status from '{current_status}' to '{new_status}'"
if reason:
message += f": {reason}"
super().__init__(
message=message,
error_code="INVALID_INVOICE_STATUS_TRANSITION",
details={
"current_status": current_status,
"new_status": new_status,
},
)
class OrderNotFoundException(ResourceNotFoundException):
"""Raised when an order for invoice creation is not found."""
def __init__(self, order_id: int):
super().__init__(
resource_type="Order",
identifier=str(order_id),
error_code="ORDER_NOT_FOUND_FOR_INVOICE",
)

View File

@@ -1 +0,0 @@
# Import/marketplace exceptions

View File

@@ -1,203 +0,0 @@
# app/exceptions/marketplace_import_job.py
"""
Marketplace import specific exceptions.
"""
from typing import Any
from .base import (
AuthorizationException,
BusinessLogicException,
ExternalServiceException,
ResourceNotFoundException,
ValidationException,
)
class MarketplaceImportException(BusinessLogicException):
"""Base exception for marketplace import operations."""
def __init__(
self,
message: str,
error_code: str = "MARKETPLACE_IMPORT_ERROR",
marketplace: str | None = None,
details: dict[str, Any] | None = None,
):
if not details:
details = {}
if marketplace:
details["marketplace"] = marketplace
super().__init__(
message=message,
error_code=error_code,
details=details,
)
class ImportJobNotFoundException(ResourceNotFoundException):
"""Raised when import job is not found."""
def __init__(self, job_id: int):
super().__init__(
resource_type="ImportJob",
identifier=str(job_id),
message=f"Import job with ID '{job_id}' not found",
error_code="IMPORT_JOB_NOT_FOUND",
)
class ImportJobNotOwnedException(AuthorizationException):
"""Raised when user tries to access import job they don't own."""
def __init__(self, job_id: int, user_id: int | None = None):
details = {"job_id": job_id}
if user_id:
details["user_id"] = user_id
super().__init__(
message=f"Unauthorized access to import job '{job_id}'",
error_code="IMPORT_JOB_NOT_OWNED",
details=details,
)
class InvalidImportDataException(ValidationException):
"""Raised when import data is invalid."""
def __init__(
self,
message: str = "Invalid import data",
field: str | None = None,
row_number: int | None = None,
details: dict[str, Any] | None = None,
):
if not details:
details = {}
if row_number:
details["row_number"] = row_number
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_IMPORT_DATA"
class ImportJobCannotBeCancelledException(BusinessLogicException):
"""Raised when trying to cancel job that cannot be cancelled."""
def __init__(self, job_id: int, current_status: str):
super().__init__(
message=f"Import job '{job_id}' cannot be cancelled (current status: {current_status})",
error_code="IMPORT_JOB_CANNOT_BE_CANCELLED",
details={
"job_id": job_id,
"current_status": current_status,
},
)
class ImportJobCannotBeDeletedException(BusinessLogicException):
"""Raised when trying to delete job that cannot be deleted."""
def __init__(self, job_id: int, current_status: str):
super().__init__(
message=f"Import job '{job_id}' cannot be deleted (current status: {current_status})",
error_code="IMPORT_JOB_CANNOT_BE_DELETED",
details={
"job_id": job_id,
"current_status": current_status,
},
)
class MarketplaceConnectionException(ExternalServiceException):
"""Raised when marketplace connection fails."""
def __init__(
self, marketplace: str, message: str = "Failed to connect to marketplace"
):
super().__init__(
service=marketplace,
message=f"{message}: {marketplace}",
error_code="MARKETPLACE_CONNECTION_FAILED",
)
class MarketplaceDataParsingException(ValidationException):
"""Raised when marketplace data cannot be parsed."""
def __init__(
self,
marketplace: str,
message: str = "Failed to parse marketplace data",
details: dict[str, Any] | None = None,
):
if not details:
details = {}
details["marketplace"] = marketplace
super().__init__(
message=f"{message} from {marketplace}",
details=details,
)
self.error_code = "MARKETPLACE_DATA_PARSING_FAILED"
class ImportRateLimitException(BusinessLogicException):
"""Raised when import rate limit is exceeded."""
def __init__(
self,
max_imports: int,
time_window: str,
retry_after: int | None = None,
):
details = {
"max_imports": max_imports,
"time_window": time_window,
}
if retry_after:
details["retry_after"] = retry_after
super().__init__(
message=f"Import rate limit exceeded: {max_imports} imports per {time_window}",
error_code="IMPORT_RATE_LIMIT_EXCEEDED",
details=details,
)
class InvalidMarketplaceException(ValidationException):
"""Raised when marketplace is not supported."""
def __init__(self, marketplace: str, supported_marketplaces: list | None = None):
details = {"marketplace": marketplace}
if supported_marketplaces:
details["supported_marketplaces"] = supported_marketplaces
super().__init__(
message=f"Unsupported marketplace: {marketplace}",
field="marketplace",
details=details,
)
self.error_code = "INVALID_MARKETPLACE"
class ImportJobAlreadyProcessingException(BusinessLogicException):
"""Raised when trying to start import while another is already processing."""
def __init__(self, vendor_code: str, existing_job_id: int):
super().__init__(
message=f"Import already in progress for vendor '{vendor_code}'",
error_code="IMPORT_JOB_ALREADY_PROCESSING",
details={
"vendor_code": vendor_code,
"existing_job_id": existing_job_id,
},
)

View File

@@ -1,108 +0,0 @@
# app/exceptions/marketplace_products.py
"""
MarketplaceProduct management specific exceptions.
"""
from typing import Any
from .base import (
BusinessLogicException,
ConflictException,
ResourceNotFoundException,
ValidationException,
)
class MarketplaceProductNotFoundException(ResourceNotFoundException):
"""Raised when a product is not found."""
def __init__(self, marketplace_product_id: str):
super().__init__(
resource_type="MarketplaceProduct",
identifier=marketplace_product_id,
message=f"MarketplaceProduct with ID '{marketplace_product_id}' not found",
error_code="PRODUCT_NOT_FOUND",
)
class MarketplaceProductAlreadyExistsException(ConflictException):
"""Raised when trying to create a product that already exists."""
def __init__(self, marketplace_product_id: str):
super().__init__(
message=f"MarketplaceProduct with ID '{marketplace_product_id}' already exists",
error_code="PRODUCT_ALREADY_EXISTS",
details={"marketplace_product_id": marketplace_product_id},
)
class InvalidMarketplaceProductDataException(ValidationException):
"""Raised when product data is invalid."""
def __init__(
self,
message: str = "Invalid product data",
field: str | None = None,
details: dict[str, Any] | None = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_PRODUCT_DATA"
class MarketplaceProductValidationException(ValidationException):
"""Raised when product validation fails."""
def __init__(
self,
message: str,
field: str | None = None,
validation_errors: dict[str, str] | None = None,
):
details = {}
if validation_errors:
details["validation_errors"] = validation_errors
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "PRODUCT_VALIDATION_FAILED"
class InvalidGTINException(ValidationException):
"""Raised when GTIN format is invalid."""
def __init__(self, gtin: str, message: str = "Invalid GTIN format"):
super().__init__(
message=f"{message}: {gtin}",
field="gtin",
details={"gtin": gtin},
)
self.error_code = "INVALID_GTIN"
class MarketplaceProductCSVImportException(BusinessLogicException):
"""Raised when product CSV import fails."""
def __init__(
self,
message: str = "MarketplaceProduct CSV import failed",
row_number: int | None = None,
errors: dict[str, Any] | None = None,
):
details = {}
if row_number:
details["row_number"] = row_number
if errors:
details["errors"] = errors
super().__init__(
message=message,
error_code="PRODUCT_CSV_IMPORT_FAILED",
details=details,
)

View File

@@ -1,101 +0,0 @@
# app/exceptions/media.py
"""
Media/file management exceptions.
"""
from typing import Any
from .base import (
BusinessLogicException,
ResourceNotFoundException,
ValidationException,
)
class MediaNotFoundException(ResourceNotFoundException):
"""Raised when a media file is not found."""
def __init__(self, media_id: int | str):
super().__init__(
resource_type="MediaFile",
identifier=str(media_id),
message=f"Media file '{media_id}' not found",
error_code="MEDIA_NOT_FOUND",
)
class MediaUploadException(BusinessLogicException):
"""Raised when media upload fails."""
def __init__(self, message: str, details: dict[str, Any] | None = None):
super().__init__(
message=message,
error_code="MEDIA_UPLOAD_FAILED",
details=details,
)
class MediaValidationException(ValidationException):
"""Raised when media validation fails (file type, size, etc.)."""
def __init__(
self,
message: str = "Media validation failed",
field: str | None = None,
details: dict[str, Any] | None = None,
):
super().__init__(message=message, field=field, details=details)
self.error_code = "MEDIA_VALIDATION_FAILED"
class UnsupportedMediaTypeException(ValidationException):
"""Raised when media file type is not supported."""
def __init__(self, file_type: str, allowed_types: list[str] | None = None):
details = {"file_type": file_type}
if allowed_types:
details["allowed_types"] = allowed_types
super().__init__(
message=f"Unsupported media type: {file_type}",
field="file",
details=details,
)
self.error_code = "UNSUPPORTED_MEDIA_TYPE"
class MediaFileTooLargeException(ValidationException):
"""Raised when media file exceeds size limit."""
def __init__(self, file_size: int, max_size: int, media_type: str = "file"):
super().__init__(
message=f"File size ({file_size} bytes) exceeds maximum allowed ({max_size} bytes) for {media_type}",
field="file",
details={
"file_size": file_size,
"max_size": max_size,
"media_type": media_type,
},
)
self.error_code = "MEDIA_FILE_TOO_LARGE"
class MediaOptimizationException(BusinessLogicException):
"""Raised when media optimization fails."""
def __init__(self, message: str = "Only images can be optimized"):
super().__init__(
message=message,
error_code="MEDIA_OPTIMIZATION_FAILED",
)
class MediaDeleteException(BusinessLogicException):
"""Raised when media deletion fails."""
def __init__(self, message: str, details: dict[str, Any] | None = None):
super().__init__(
message=message,
error_code="MEDIA_DELETE_FAILED",
details=details,
)

View File

@@ -1,100 +0,0 @@
# app/exceptions/message.py
"""
Messaging specific exceptions.
"""
from .base import BusinessLogicException, ResourceNotFoundException
class ConversationNotFoundException(ResourceNotFoundException):
"""Raised when a conversation is not found."""
def __init__(self, conversation_identifier: str):
super().__init__(
resource_type="Conversation",
identifier=conversation_identifier,
message=f"Conversation '{conversation_identifier}' not found",
error_code="CONVERSATION_NOT_FOUND",
)
class MessageNotFoundException(ResourceNotFoundException):
"""Raised when a message is not found."""
def __init__(self, message_identifier: str):
super().__init__(
resource_type="Message",
identifier=message_identifier,
message=f"Message '{message_identifier}' not found",
error_code="MESSAGE_NOT_FOUND",
)
class ConversationClosedException(BusinessLogicException):
"""Raised when trying to send message to a closed conversation."""
def __init__(self, conversation_id: int):
super().__init__(
message=f"Cannot send message to closed conversation {conversation_id}",
error_code="CONVERSATION_CLOSED",
details={"conversation_id": conversation_id},
)
class MessageAttachmentException(BusinessLogicException):
"""Raised when attachment validation fails."""
def __init__(self, message: str, details: dict | None = None):
super().__init__(
message=message,
error_code="MESSAGE_ATTACHMENT_INVALID",
details=details,
)
class UnauthorizedConversationAccessException(BusinessLogicException):
"""Raised when user tries to access a conversation they don't have access to."""
def __init__(self, conversation_id: int):
super().__init__(
message=f"You do not have access to conversation {conversation_id}",
error_code="CONVERSATION_ACCESS_DENIED",
details={"conversation_id": conversation_id},
)
class InvalidConversationTypeException(BusinessLogicException):
"""Raised when conversation type is not valid for the operation."""
def __init__(self, message: str, allowed_types: list[str] | None = None):
super().__init__(
message=message,
error_code="INVALID_CONVERSATION_TYPE",
details={"allowed_types": allowed_types} if allowed_types else None,
)
class InvalidRecipientTypeException(BusinessLogicException):
"""Raised when recipient type doesn't match conversation type."""
def __init__(self, conversation_type: str, expected_recipient_type: str):
super().__init__(
message=f"{conversation_type} conversations require a {expected_recipient_type} recipient",
error_code="INVALID_RECIPIENT_TYPE",
details={
"conversation_type": conversation_type,
"expected_recipient_type": expected_recipient_type,
},
)
class AttachmentNotFoundException(ResourceNotFoundException):
"""Raised when an attachment is not found."""
def __init__(self, attachment_id: int | str):
super().__init__(
resource_type="Attachment",
identifier=str(attachment_id),
message=f"Attachment '{attachment_id}' not found",
error_code="ATTACHMENT_NOT_FOUND",
)

View File

@@ -1 +0,0 @@
# Monitoring exceptions

View File

@@ -1 +0,0 @@
# Notification exceptions

View File

@@ -1,87 +0,0 @@
# app/exceptions/onboarding.py
"""
Onboarding-specific exceptions.
Exceptions for the vendor onboarding wizard flow.
"""
from typing import Any
from .base import BusinessLogicException, ResourceNotFoundException, ValidationException
class OnboardingNotFoundException(ResourceNotFoundException):
"""Raised when onboarding record is not found for a vendor."""
def __init__(self, vendor_id: int):
super().__init__(
resource_type="VendorOnboarding",
identifier=str(vendor_id),
)
class OnboardingStepOrderException(ValidationException):
"""Raised when trying to access a step out of order."""
def __init__(self, current_step: str, required_step: str):
super().__init__(
message=f"Please complete the {required_step} step first",
field="step",
details={
"current_step": current_step,
"required_step": required_step,
},
)
class OnboardingAlreadyCompletedException(BusinessLogicException):
"""Raised when trying to modify a completed onboarding."""
def __init__(self, vendor_id: int):
super().__init__(
message="Onboarding has already been completed",
error_code="ONBOARDING_ALREADY_COMPLETED",
details={"vendor_id": vendor_id},
)
class LetzshopConnectionFailedException(BusinessLogicException):
"""Raised when Letzshop API connection test fails."""
def __init__(self, error_message: str):
super().__init__(
message=f"Letzshop connection failed: {error_message}",
error_code="LETZSHOP_CONNECTION_FAILED",
details={"error": error_message},
)
class OnboardingCsvUrlRequiredException(ValidationException):
"""Raised when no CSV URL is provided in product import step."""
def __init__(self):
super().__init__(
message="At least one CSV URL must be provided",
field="csv_url",
)
class OnboardingSyncJobNotFoundException(ResourceNotFoundException):
"""Raised when sync job is not found."""
def __init__(self, job_id: int):
super().__init__(
resource_type="LetzshopHistoricalImportJob",
identifier=str(job_id),
)
class OnboardingSyncNotCompleteException(BusinessLogicException):
"""Raised when trying to complete onboarding before sync is done."""
def __init__(self, job_status: str):
super().__init__(
message=f"Import job is still {job_status}, please wait",
error_code="SYNC_NOT_COMPLETE",
details={"job_status": job_status},
)

View File

@@ -1,60 +0,0 @@
# app/exceptions/order.py
"""
Order management specific exceptions.
"""
from .base import BusinessLogicException, ResourceNotFoundException, ValidationException
class OrderNotFoundException(ResourceNotFoundException):
"""Raised when an order is not found."""
def __init__(self, order_identifier: str):
super().__init__(
resource_type="Order",
identifier=order_identifier,
message=f"Order '{order_identifier}' not found",
error_code="ORDER_NOT_FOUND",
)
class OrderAlreadyExistsException(ValidationException):
"""Raised when trying to create a duplicate order."""
def __init__(self, order_number: str):
super().__init__(
message=f"Order with number '{order_number}' already exists",
error_code="ORDER_ALREADY_EXISTS",
details={"order_number": order_number},
)
class OrderValidationException(ValidationException):
"""Raised when order data validation fails."""
def __init__(self, message: str, details: dict | None = None):
super().__init__(
message=message, error_code="ORDER_VALIDATION_FAILED", details=details
)
class InvalidOrderStatusException(BusinessLogicException):
"""Raised when trying to set an invalid order status."""
def __init__(self, current_status: str, new_status: str):
super().__init__(
message=f"Cannot change order status from '{current_status}' to '{new_status}'",
error_code="INVALID_ORDER_STATUS_CHANGE",
details={"current_status": current_status, "new_status": new_status},
)
class OrderCannotBeCancelledException(BusinessLogicException):
"""Raised when order cannot be cancelled."""
def __init__(self, order_number: str, reason: str):
super().__init__(
message=f"Order '{order_number}' cannot be cancelled: {reason}",
error_code="ORDER_CANNOT_BE_CANCELLED",
details={"order_number": order_number, "reason": reason},
)

View File

@@ -1,54 +0,0 @@
# app/exceptions/order_item_exception.py
"""Order item exception specific exceptions."""
from .base import BusinessLogicException, ResourceNotFoundException
class OrderItemExceptionNotFoundException(ResourceNotFoundException):
"""Raised when an order item exception is not found."""
def __init__(self, exception_id: int | str):
super().__init__(
resource_type="OrderItemException",
identifier=str(exception_id),
error_code="ORDER_ITEM_EXCEPTION_NOT_FOUND",
)
class OrderHasUnresolvedExceptionsException(BusinessLogicException):
"""Raised when trying to confirm an order with unresolved exceptions."""
def __init__(self, order_id: int, unresolved_count: int):
super().__init__(
message=(
f"Order has {unresolved_count} unresolved product exception(s). "
f"Please resolve all exceptions before confirming the order."
),
error_code="ORDER_HAS_UNRESOLVED_EXCEPTIONS",
details={
"order_id": order_id,
"unresolved_count": unresolved_count,
},
)
class ExceptionAlreadyResolvedException(BusinessLogicException):
"""Raised when trying to resolve an already resolved exception."""
def __init__(self, exception_id: int):
super().__init__(
message=f"Exception {exception_id} has already been resolved",
error_code="EXCEPTION_ALREADY_RESOLVED",
details={"exception_id": exception_id},
)
class InvalidProductForExceptionException(BusinessLogicException):
"""Raised when the product provided for resolution is invalid."""
def __init__(self, product_id: int, reason: str):
super().__init__(
message=f"Cannot use product {product_id} for resolution: {reason}",
error_code="INVALID_PRODUCT_FOR_EXCEPTION",
details={"product_id": product_id, "reason": reason},
)

View File

@@ -1 +0,0 @@
# Payment processing exceptions

View File

@@ -1,44 +0,0 @@
# app/exceptions/platform.py
"""
Platform-related exceptions.
Custom exceptions for platform management operations.
"""
from app.exceptions.base import WizamartException
class PlatformNotFoundException(WizamartException):
"""Raised when a platform is not found."""
def __init__(self, code: str):
super().__init__(
message=f"Platform not found: {code}",
error_code="PLATFORM_NOT_FOUND",
status_code=404,
details={"platform_code": code},
)
class PlatformInactiveException(WizamartException):
"""Raised when trying to access an inactive platform."""
def __init__(self, code: str):
super().__init__(
message=f"Platform is inactive: {code}",
error_code="PLATFORM_INACTIVE",
status_code=403,
details={"platform_code": code},
)
class PlatformUpdateException(WizamartException):
"""Raised when platform update fails."""
def __init__(self, code: str, reason: str):
super().__init__(
message=f"Failed to update platform {code}: {reason}",
error_code="PLATFORM_UPDATE_FAILED",
status_code=400,
details={"platform_code": code, "reason": reason},
)

View File

@@ -1,142 +0,0 @@
# app/exceptions/product.py
"""
Product (vendor catalog) specific exceptions.
"""
from .base import (
BusinessLogicException,
ConflictException,
ResourceNotFoundException,
ValidationException,
)
class ProductNotFoundException(ResourceNotFoundException):
"""Raised when a product is not found in vendor catalog."""
def __init__(self, product_id: int, vendor_id: int | None = None):
if vendor_id:
message = f"Product with ID '{product_id}' not found in vendor {vendor_id} catalog"
else:
message = f"Product with ID '{product_id}' not found"
super().__init__(
resource_type="Product",
identifier=str(product_id),
message=message,
error_code="PRODUCT_NOT_FOUND",
)
# Add extra details after init
self.details["product_id"] = product_id
if vendor_id:
self.details["vendor_id"] = vendor_id
class ProductAlreadyExistsException(ConflictException):
"""Raised when trying to add a marketplace product that's already in vendor catalog."""
def __init__(self, vendor_id: int, marketplace_product_id: int):
super().__init__(
message=f"Marketplace product {marketplace_product_id} already exists in vendor {vendor_id} catalog",
error_code="PRODUCT_ALREADY_EXISTS",
details={
"vendor_id": vendor_id,
"marketplace_product_id": marketplace_product_id,
},
)
class ProductNotInCatalogException(ResourceNotFoundException):
"""Raised when trying to access a product that's not in vendor's catalog."""
def __init__(self, product_id: int, vendor_id: int):
super().__init__(
resource_type="Product",
identifier=str(product_id),
message=f"Product {product_id} is not in vendor {vendor_id} catalog",
error_code="PRODUCT_NOT_IN_CATALOG",
details={
"product_id": product_id,
"vendor_id": vendor_id,
},
)
class ProductNotActiveException(BusinessLogicException):
"""Raised when trying to perform operations on inactive product."""
def __init__(self, product_id: int, vendor_id: int):
super().__init__(
message=f"Product {product_id} in vendor {vendor_id} catalog is not active",
error_code="PRODUCT_NOT_ACTIVE",
details={
"product_id": product_id,
"vendor_id": vendor_id,
},
)
class InvalidProductDataException(ValidationException):
"""Raised when product data is invalid."""
def __init__(
self,
message: str = "Invalid product data",
field: str | None = None,
details: dict | None = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_PRODUCT_DATA"
class ProductValidationException(ValidationException):
"""Raised when product validation fails."""
def __init__(
self,
message: str = "Product validation failed",
field: str | None = None,
validation_errors: dict | None = None,
):
details = {}
if validation_errors:
details["validation_errors"] = validation_errors
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "PRODUCT_VALIDATION_FAILED"
class CannotDeleteProductWithInventoryException(BusinessLogicException):
"""Raised when trying to delete a product that has inventory."""
def __init__(self, product_id: int, inventory_count: int):
super().__init__(
message=f"Cannot delete product {product_id} - it has {inventory_count} inventory entries",
error_code="CANNOT_DELETE_PRODUCT_WITH_INVENTORY",
details={
"product_id": product_id,
"inventory_count": inventory_count,
},
)
class CannotDeleteProductWithOrdersException(BusinessLogicException):
"""Raised when trying to delete a product that has been ordered."""
def __init__(self, product_id: int, order_count: int):
super().__init__(
message=f"Cannot delete product {product_id} - it has {order_count} associated orders",
error_code="CANNOT_DELETE_PRODUCT_WITH_ORDERS",
details={
"product_id": product_id,
"order_count": order_count,
},
)

View File

@@ -1 +0,0 @@
# Search exceptions

View File

@@ -1,273 +0,0 @@
# app/exceptions/team.py
"""
Team management specific exceptions.
"""
from typing import Any
from .base import (
AuthorizationException,
BusinessLogicException,
ConflictException,
ResourceNotFoundException,
ValidationException,
)
class TeamMemberNotFoundException(ResourceNotFoundException):
"""Raised when a team member is not found."""
def __init__(self, user_id: int, vendor_id: int | None = None):
details = {"user_id": user_id}
if vendor_id:
details["vendor_id"] = vendor_id
message = (
f"Team member with user ID '{user_id}' not found in vendor {vendor_id}"
)
else:
message = f"Team member with user ID '{user_id}' not found"
super().__init__(
resource_type="TeamMember",
identifier=str(user_id),
message=message,
error_code="TEAM_MEMBER_NOT_FOUND",
details=details,
)
class TeamMemberAlreadyExistsException(ConflictException):
"""Raised when trying to add a user who is already a team member."""
def __init__(self, user_id: int, vendor_id: int):
super().__init__(
message=f"User {user_id} is already a team member of vendor {vendor_id}",
error_code="TEAM_MEMBER_ALREADY_EXISTS",
details={
"user_id": user_id,
"vendor_id": vendor_id,
},
)
class TeamInvitationNotFoundException(ResourceNotFoundException):
"""Raised when a team invitation is not found."""
def __init__(self, invitation_token: str):
super().__init__(
resource_type="TeamInvitation",
identifier=invitation_token,
message=f"Team invitation with token '{invitation_token}' not found or expired",
error_code="TEAM_INVITATION_NOT_FOUND",
)
class TeamInvitationExpiredException(BusinessLogicException):
"""Raised when trying to accept an expired invitation."""
def __init__(self, invitation_token: str):
super().__init__(
message="Team invitation has expired",
error_code="TEAM_INVITATION_EXPIRED",
details={"invitation_token": invitation_token},
)
class TeamInvitationAlreadyAcceptedException(ConflictException):
"""Raised when trying to accept an already accepted invitation."""
def __init__(self, invitation_token: str):
super().__init__(
message="Team invitation has already been accepted",
error_code="TEAM_INVITATION_ALREADY_ACCEPTED",
details={"invitation_token": invitation_token},
)
class UnauthorizedTeamActionException(AuthorizationException):
"""Raised when user tries to perform team action without permission."""
def __init__(
self,
action: str,
user_id: int | None = None,
required_permission: str | None = None,
):
details = {"action": action}
if user_id:
details["user_id"] = user_id
if required_permission:
details["required_permission"] = required_permission
super().__init__(
message=f"Unauthorized to perform action: {action}",
error_code="UNAUTHORIZED_TEAM_ACTION",
details=details,
)
class CannotRemoveOwnerException(BusinessLogicException):
"""Raised when trying to remove the vendor owner from team."""
def __init__(self, user_id: int, vendor_id: int):
super().__init__(
message="Cannot remove vendor owner from team",
error_code="CANNOT_REMOVE_OWNER",
details={
"user_id": user_id,
"vendor_id": vendor_id,
},
)
class CannotModifyOwnRoleException(BusinessLogicException):
"""Raised when user tries to modify their own role."""
def __init__(self, user_id: int):
super().__init__(
message="Cannot modify your own role",
error_code="CANNOT_MODIFY_OWN_ROLE",
details={"user_id": user_id},
)
class RoleNotFoundException(ResourceNotFoundException):
"""Raised when a role is not found."""
def __init__(self, role_id: int, vendor_id: int | None = None):
details = {"role_id": role_id}
if vendor_id:
details["vendor_id"] = vendor_id
message = f"Role with ID '{role_id}' not found in vendor {vendor_id}"
else:
message = f"Role with ID '{role_id}' not found"
super().__init__(
resource_type="Role",
identifier=str(role_id),
message=message,
error_code="ROLE_NOT_FOUND",
details=details,
)
class InvalidRoleException(ValidationException):
"""Raised when role data is invalid."""
def __init__(
self,
message: str = "Invalid role data",
field: str | None = None,
details: dict[str, Any] | None = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_ROLE_DATA"
class InsufficientTeamPermissionsException(AuthorizationException):
"""Raised when user lacks required team permissions for an action."""
def __init__(
self,
required_permission: str,
user_id: int | None = None,
action: str | None = None,
):
details = {"required_permission": required_permission}
if user_id:
details["user_id"] = user_id
if action:
details["action"] = action
message = f"Insufficient team permissions. Required: {required_permission}"
super().__init__(
message=message,
error_code="INSUFFICIENT_TEAM_PERMISSIONS",
details=details,
)
class MaxTeamMembersReachedException(BusinessLogicException):
"""Raised when vendor has reached maximum team members limit."""
def __init__(self, max_members: int, vendor_id: int):
super().__init__(
message=f"Maximum number of team members reached ({max_members})",
error_code="MAX_TEAM_MEMBERS_REACHED",
details={
"max_members": max_members,
"vendor_id": vendor_id,
},
)
class TeamValidationException(ValidationException):
"""Raised when team operation validation fails."""
def __init__(
self,
message: str = "Team operation validation failed",
field: str | None = None,
validation_errors: dict[str, str] | None = None,
):
details = {}
if validation_errors:
details["validation_errors"] = validation_errors
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "TEAM_VALIDATION_FAILED"
class InvalidInvitationDataException(ValidationException):
"""Raised when team invitation data is invalid."""
def __init__(
self,
message: str = "Invalid invitation data",
field: str | None = None,
details: dict[str, Any] | None = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_INVITATION_DATA"
# ============================================================================
# NEW: Add InvalidInvitationTokenException
# ============================================================================
class InvalidInvitationTokenException(ValidationException):
"""Raised when invitation token is invalid, expired, or already used.
This is a general exception for any invitation token validation failure.
Use this when checking invitation tokens during the acceptance flow.
"""
def __init__(
self,
message: str = "Invalid or expired invitation token",
invitation_token: str | None = None,
):
details = {}
if invitation_token:
details["invitation_token"] = invitation_token
super().__init__(
message=message,
field="invitation_token",
details=details,
)
self.error_code = "INVALID_INVITATION_TOKEN"

View File

@@ -1,190 +0,0 @@
# app/exceptions/vendor.py
"""
Vendor management specific exceptions.
"""
from typing import Any
from .base import (
AuthorizationException,
BusinessLogicException,
ConflictException,
ResourceNotFoundException,
ValidationException,
)
class VendorNotFoundException(ResourceNotFoundException):
"""Raised when a vendor is not found."""
def __init__(self, vendor_identifier: str, identifier_type: str = "code"):
if identifier_type.lower() == "id":
message = f"Vendor with ID '{vendor_identifier}' not found"
else:
message = f"Vendor with code '{vendor_identifier}' not found"
super().__init__(
resource_type="Vendor",
identifier=vendor_identifier,
message=message,
error_code="VENDOR_NOT_FOUND",
)
class VendorAlreadyExistsException(ConflictException):
"""Raised when trying to create a vendor that already exists."""
def __init__(self, vendor_code: str):
super().__init__(
message=f"Vendor with code '{vendor_code}' already exists",
error_code="VENDOR_ALREADY_EXISTS",
details={"vendor_code": vendor_code},
)
class VendorNotActiveException(BusinessLogicException):
"""Raised when trying to perform operations on inactive vendor."""
def __init__(self, vendor_code: str):
super().__init__(
message=f"Vendor '{vendor_code}' is not active",
error_code="VENDOR_NOT_ACTIVE",
details={"vendor_code": vendor_code},
)
class VendorNotVerifiedException(BusinessLogicException):
"""Raised when trying to perform operations requiring verified vendor."""
def __init__(self, vendor_code: str):
super().__init__(
message=f"Vendor '{vendor_code}' is not verified",
error_code="VENDOR_NOT_VERIFIED",
details={"vendor_code": vendor_code},
)
class UnauthorizedVendorAccessException(AuthorizationException):
"""Raised when user tries to access vendor they don't own."""
def __init__(self, vendor_code: str, user_id: int | None = None):
details = {"vendor_code": vendor_code}
if user_id:
details["user_id"] = user_id
super().__init__(
message=f"Unauthorized access to vendor '{vendor_code}'",
error_code="UNAUTHORIZED_VENDOR_ACCESS",
details=details,
)
class InvalidVendorDataException(ValidationException):
"""Raised when vendor data is invalid or incomplete."""
def __init__(
self,
message: str = "Invalid vendor data",
field: str | None = None,
details: dict[str, Any] | None = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_VENDOR_DATA"
class VendorValidationException(ValidationException):
"""Raised when vendor validation fails."""
def __init__(
self,
message: str = "Vendor validation failed",
field: str | None = None,
validation_errors: dict[str, str] | None = None,
):
details = {}
if validation_errors:
details["validation_errors"] = validation_errors
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "VENDOR_VALIDATION_FAILED"
class IncompleteVendorDataException(ValidationException):
"""Raised when vendor data is missing required fields."""
def __init__(
self,
vendor_code: str,
missing_fields: list,
):
super().__init__(
message=f"Vendor '{vendor_code}' is missing required fields: {', '.join(missing_fields)}",
details={
"vendor_code": vendor_code,
"missing_fields": missing_fields,
},
)
self.error_code = "INCOMPLETE_VENDOR_DATA"
class MaxVendorsReachedException(BusinessLogicException):
"""Raised when user tries to create more vendors than allowed."""
def __init__(self, max_vendors: int, user_id: int | None = None):
details = {"max_vendors": max_vendors}
if user_id:
details["user_id"] = user_id
super().__init__(
message=f"Maximum number of vendors reached ({max_vendors})",
error_code="MAX_VENDORS_REACHED",
details=details,
)
class VendorAccessDeniedException(AuthorizationException):
"""Raised when no vendor context is available for an authenticated endpoint."""
def __init__(self, message: str = "No vendor context available"):
super().__init__(
message=message,
error_code="VENDOR_ACCESS_DENIED",
)
class VendorOwnerOnlyException(AuthorizationException):
"""Raised when operation requires vendor owner role."""
def __init__(self, operation: str, vendor_code: str | None = None):
details = {"operation": operation}
if vendor_code:
details["vendor_code"] = vendor_code
super().__init__(
message=f"Operation '{operation}' requires vendor owner role",
error_code="VENDOR_OWNER_ONLY",
details=details,
)
class InsufficientVendorPermissionsException(AuthorizationException):
"""Raised when user lacks required vendor permission."""
def __init__(self, required_permission: str, vendor_code: str | None = None):
details = {"required_permission": required_permission}
if vendor_code:
details["vendor_code"] = vendor_code
super().__init__(
message=f"Permission required: {required_permission}",
error_code="INSUFFICIENT_VENDOR_PERMISSIONS",
details=details,
)

View File

@@ -1,146 +0,0 @@
# app/exceptions/vendor_domain.py
"""
Vendor domain management specific exceptions.
"""
from .base import (
BusinessLogicException,
ConflictException,
ExternalServiceException,
ResourceNotFoundException,
ValidationException,
)
class VendorDomainNotFoundException(ResourceNotFoundException):
"""Raised when a vendor domain is not found."""
def __init__(self, domain_identifier: str, identifier_type: str = "ID"):
if identifier_type.lower() == "domain":
message = f"Domain '{domain_identifier}' not found"
else:
message = f"Domain with ID '{domain_identifier}' not found"
super().__init__(
resource_type="VendorDomain",
identifier=domain_identifier,
message=message,
error_code="VENDOR_DOMAIN_NOT_FOUND",
)
class VendorDomainAlreadyExistsException(ConflictException):
"""Raised when trying to add a domain that already exists."""
def __init__(self, domain: str, existing_vendor_id: int | None = None):
details = {"domain": domain}
if existing_vendor_id:
details["existing_vendor_id"] = existing_vendor_id
super().__init__(
message=f"Domain '{domain}' is already registered",
error_code="VENDOR_DOMAIN_ALREADY_EXISTS",
details=details,
)
class InvalidDomainFormatException(ValidationException):
"""Raised when domain format is invalid."""
def __init__(self, domain: str, reason: str = "Invalid domain format"):
super().__init__(
message=f"{reason}: {domain}",
field="domain",
details={"domain": domain, "reason": reason},
)
self.error_code = "INVALID_DOMAIN_FORMAT"
class ReservedDomainException(ValidationException):
"""Raised when trying to use a reserved domain."""
def __init__(self, domain: str, reserved_part: str):
super().__init__(
message=f"Domain cannot use reserved subdomain: {reserved_part}",
field="domain",
details={"domain": domain, "reserved_part": reserved_part},
)
self.error_code = "RESERVED_DOMAIN"
class DomainNotVerifiedException(BusinessLogicException):
"""Raised when trying to activate an unverified domain."""
def __init__(self, domain_id: int, domain: str):
super().__init__(
message=f"Domain '{domain}' must be verified before activation",
error_code="DOMAIN_NOT_VERIFIED",
details={"domain_id": domain_id, "domain": domain},
)
class DomainVerificationFailedException(BusinessLogicException):
"""Raised when domain verification fails."""
def __init__(self, domain: str, reason: str):
super().__init__(
message=f"Domain verification failed for '{domain}': {reason}",
error_code="DOMAIN_VERIFICATION_FAILED",
details={"domain": domain, "reason": reason},
)
class DomainAlreadyVerifiedException(BusinessLogicException):
"""Raised when trying to verify an already verified domain."""
def __init__(self, domain_id: int, domain: str):
super().__init__(
message=f"Domain '{domain}' is already verified",
error_code="DOMAIN_ALREADY_VERIFIED",
details={"domain_id": domain_id, "domain": domain},
)
class MultiplePrimaryDomainsException(BusinessLogicException):
"""Raised when trying to set multiple primary domains."""
def __init__(self, vendor_id: int):
super().__init__(
message="Vendor can only have one primary domain",
error_code="MULTIPLE_PRIMARY_DOMAINS",
details={"vendor_id": vendor_id},
)
class DNSVerificationException(ExternalServiceException):
"""Raised when DNS verification service fails."""
def __init__(self, domain: str, reason: str):
super().__init__(
service_name="DNS",
message=f"DNS verification failed for '{domain}': {reason}",
error_code="DNS_VERIFICATION_ERROR",
details={"domain": domain, "reason": reason},
)
class MaxDomainsReachedException(BusinessLogicException):
"""Raised when vendor tries to add more domains than allowed."""
def __init__(self, vendor_id: int, max_domains: int):
super().__init__(
message=f"Maximum number of domains reached ({max_domains})",
error_code="MAX_DOMAINS_REACHED",
details={"vendor_id": vendor_id, "max_domains": max_domains},
)
class UnauthorizedDomainAccessException(BusinessLogicException):
"""Raised when trying to access domain that doesn't belong to vendor."""
def __init__(self, domain_id: int, vendor_id: int):
super().__init__(
message=f"Unauthorized access to domain {domain_id}",
error_code="UNAUTHORIZED_DOMAIN_ACCESS",
details={"domain_id": domain_id, "vendor_id": vendor_id},
)

View File

@@ -1,129 +0,0 @@
# app/exceptions/vendor_theme.py
"""
Vendor theme management specific exceptions.
"""
from typing import Any
from .base import (
BusinessLogicException,
ResourceNotFoundException,
ValidationException,
)
class VendorThemeNotFoundException(ResourceNotFoundException):
"""Raised when a vendor theme is not found."""
def __init__(self, vendor_identifier: str):
super().__init__(
resource_type="VendorTheme",
identifier=vendor_identifier,
message=f"Theme for vendor '{vendor_identifier}' not found",
error_code="VENDOR_THEME_NOT_FOUND",
)
class InvalidThemeDataException(ValidationException):
"""Raised when theme data is invalid."""
def __init__(
self,
message: str = "Invalid theme data",
field: str | None = None,
details: dict[str, Any] | None = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_THEME_DATA"
class ThemePresetNotFoundException(ResourceNotFoundException):
"""Raised when a theme preset is not found."""
def __init__(self, preset_name: str, available_presets: list | None = None):
details = {"preset_name": preset_name}
if available_presets:
details["available_presets"] = available_presets
super().__init__(
resource_type="ThemePreset",
identifier=preset_name,
message=f"Theme preset '{preset_name}' not found",
error_code="THEME_PRESET_NOT_FOUND",
)
self.details = details
class ThemeValidationException(ValidationException):
"""Raised when theme validation fails."""
def __init__(
self,
message: str = "Theme validation failed",
field: str | None = None,
validation_errors: dict[str, str] | None = None,
):
details = {}
if validation_errors:
details["validation_errors"] = validation_errors
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "THEME_VALIDATION_FAILED"
class ThemePresetAlreadyAppliedException(BusinessLogicException):
"""Raised when trying to apply the same preset that's already active."""
def __init__(self, preset_name: str, vendor_code: str):
super().__init__(
message=f"Preset '{preset_name}' is already applied to vendor '{vendor_code}'",
error_code="THEME_PRESET_ALREADY_APPLIED",
details={"preset_name": preset_name, "vendor_code": vendor_code},
)
class InvalidColorFormatException(ValidationException):
"""Raised when color format is invalid."""
def __init__(self, color_value: str, field: str):
super().__init__(
message=f"Invalid color format: {color_value}",
field=field,
details={"color_value": color_value},
)
self.error_code = "INVALID_COLOR_FORMAT"
class InvalidFontFamilyException(ValidationException):
"""Raised when font family is invalid."""
def __init__(self, font_value: str, field: str):
super().__init__(
message=f"Invalid font family: {font_value}",
field=field,
details={"font_value": font_value},
)
self.error_code = "INVALID_FONT_FAMILY"
class ThemeOperationException(BusinessLogicException):
"""Raised when theme operation fails."""
def __init__(self, operation: str, vendor_code: str, reason: str):
super().__init__(
message=f"Theme operation '{operation}' failed for vendor '{vendor_code}': {reason}",
error_code="THEME_OPERATION_FAILED",
details={
"operation": operation,
"vendor_code": vendor_code,
"reason": reason,
},
)

View File

@@ -1,13 +1,2 @@
# app/modules/analytics/routes/pages/__init__.py
"""
Analytics module page routes.
Provides HTML page endpoints for analytics views:
- Vendor pages: Analytics dashboard for vendors
"""
from app.modules.analytics.routes.pages.vendor import router as vendor_router
# Note: Analytics has no admin pages - admin uses the main dashboard
__all__ = ["vendor_router"]
"""Analytics module page routes."""

View File

@@ -0,0 +1,87 @@
# app/modules/analytics/routes/pages/admin.py
"""
Analytics Admin Page Routes (HTML rendering).
Admin pages for code quality and analytics:
- Code quality dashboard
- Violations list
- Violation detail
"""
from fastapi import APIRouter, Depends, Path, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.api.deps import get_db, require_menu_access
from app.modules.core.utils.page_context import get_admin_context
from app.templates_config import templates
from models.database.admin_menu_config import FrontendType
from models.database.user import User
router = APIRouter()
# ============================================================================
# CODE QUALITY & ARCHITECTURE ROUTES
# ============================================================================
@router.get("/code-quality", response_class=HTMLResponse, include_in_schema=False)
async def admin_code_quality_dashboard(
request: Request,
current_user: User = Depends(
require_menu_access("code-quality", FrontendType.ADMIN)
),
db: Session = Depends(get_db),
):
"""
Render code quality dashboard.
Shows architecture violations, trends, and technical debt score.
"""
return templates.TemplateResponse(
"dev_tools/admin/code-quality-dashboard.html",
get_admin_context(request, current_user),
)
@router.get(
"/code-quality/violations", response_class=HTMLResponse, include_in_schema=False
)
async def admin_code_quality_violations(
request: Request,
current_user: User = Depends(
require_menu_access("code-quality", FrontendType.ADMIN)
),
db: Session = Depends(get_db),
):
"""
Render violations list page.
Shows all violations with filtering and sorting options.
"""
return templates.TemplateResponse(
"dev_tools/admin/code-quality-violations.html",
get_admin_context(request, current_user),
)
@router.get(
"/code-quality/violations/{violation_id}",
response_class=HTMLResponse,
include_in_schema=False,
)
async def admin_code_quality_violation_detail(
request: Request,
violation_id: int = Path(..., description="Violation ID"),
current_user: User = Depends(
require_menu_access("code-quality", FrontendType.ADMIN)
),
db: Session = Depends(get_db),
):
"""
Render violation detail page.
Shows violation details, code context, assignments, and comments.
"""
return templates.TemplateResponse(
"dev_tools/admin/code-quality-violation-detail.html",
get_admin_context(request, current_user, violation_id=violation_id),
)

View File

@@ -12,7 +12,7 @@ from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_from_cookie_or_header, get_db
from app.services.platform_settings_service import platform_settings_service # noqa: MOD-004 - shared platform service
from app.modules.core.services.platform_settings_service import platform_settings_service # noqa: MOD-004 - shared platform service
from app.templates_config import templates
from models.database.user import User
from models.database.vendor import Vendor

View File

@@ -18,7 +18,7 @@ from typing import Any
from sqlalchemy import func
from sqlalchemy.orm import Session
from app.exceptions import AdminOperationException, VendorNotFoundException
from app.modules.tenancy.exceptions import AdminOperationException, VendorNotFoundException
from app.modules.customers.models.customer import Customer
from app.modules.inventory.models import Inventory
from app.modules.marketplace.models import MarketplaceImportJob, MarketplaceProduct

View File

@@ -93,7 +93,7 @@ class UsageService:
Returns current usage, limits, and upgrade recommendations.
"""
from app.services.subscription_service import subscription_service
from app.modules.billing.services.subscription_service import subscription_service
# Get subscription
subscription = subscription_service.get_or_create_subscription(db, vendor_id)
@@ -151,7 +151,7 @@ class UsageService:
Returns:
LimitCheckData with proceed status and upgrade info
"""
from app.services.subscription_service import subscription_service
from app.modules.billing.services.subscription_service import subscription_service
if limit_type == "orders":
can_proceed, message = subscription_service.can_create_order(db, vendor_id)

View File

@@ -2,82 +2,294 @@
"""
Billing module exceptions.
Custom exceptions for subscription, billing, and payment operations.
This module provides exception classes for billing operations including:
- Subscription management
- Payment processing (Stripe)
- Feature management
- Tier management
"""
from app.exceptions import BusinessLogicException, ResourceNotFoundException
from app.exceptions.base import (
BusinessLogicException,
ResourceNotFoundException,
ServiceUnavailableException,
ValidationException,
)
__all__ = [
# Base billing exception
"BillingException",
# Subscription exceptions
"SubscriptionNotFoundException",
"NoActiveSubscriptionException",
"SubscriptionNotCancelledException",
"SubscriptionAlreadyCancelledException",
# Tier exceptions
"TierNotFoundException",
"TierNotFoundError",
"TierLimitExceededException",
# Payment exceptions
"PaymentSystemNotConfiguredException",
"StripeNotConfiguredException",
"StripePriceNotConfiguredException",
"PaymentFailedException",
# Webhook exceptions
"InvalidWebhookSignatureException",
"WebhookMissingSignatureException",
"WebhookVerificationException",
# Feature exceptions
"FeatureNotFoundException",
"FeatureNotFoundError",
"FeatureNotAvailableException",
"InvalidFeatureCodesError",
]
# =============================================================================
# Base Billing Exception
# =============================================================================
class BillingException(BusinessLogicException):
"""Base exception for billing module errors."""
pass
def __init__(self, message: str, error_code: str = "BILLING_ERROR", details: dict | None = None):
super().__init__(message=message, error_code=error_code, details=details)
# =============================================================================
# Subscription Exceptions
# =============================================================================
class SubscriptionNotFoundException(ResourceNotFoundException):
"""Raised when a subscription is not found."""
def __init__(self, vendor_id: int):
super().__init__("Subscription", str(vendor_id))
super().__init__(
resource_type="Subscription",
identifier=str(vendor_id),
error_code="SUBSCRIPTION_NOT_FOUND",
)
class NoActiveSubscriptionException(BusinessLogicException):
"""Raised when no active subscription exists for an operation that requires one."""
def __init__(self, message: str = "No active subscription found"):
super().__init__(
message=message,
error_code="NO_ACTIVE_SUBSCRIPTION",
)
class SubscriptionNotCancelledException(BusinessLogicException):
"""Raised when trying to reactivate a subscription that is not cancelled."""
def __init__(self):
super().__init__(
message="Subscription is not cancelled",
error_code="SUBSCRIPTION_NOT_CANCELLED",
)
class SubscriptionAlreadyCancelledException(BusinessLogicException):
"""Raised when trying to cancel an already cancelled subscription."""
def __init__(self):
super().__init__(
message="Subscription is already cancelled",
error_code="SUBSCRIPTION_ALREADY_CANCELLED",
)
# =============================================================================
# Tier Exceptions
# =============================================================================
class TierNotFoundException(ResourceNotFoundException):
"""Raised when a subscription tier is not found."""
def __init__(self, tier_code: str):
super().__init__("SubscriptionTier", tier_code)
super().__init__(
resource_type="SubscriptionTier",
identifier=tier_code,
message=f"Subscription tier '{tier_code}' not found",
error_code="TIER_NOT_FOUND",
)
self.tier_code = tier_code
class TierNotFoundError(ResourceNotFoundException):
"""Subscription tier not found (alternate naming)."""
def __init__(self, tier_code: str):
super().__init__(
resource_type="SubscriptionTier",
identifier=tier_code,
message=f"Tier '{tier_code}' not found",
)
self.tier_code = tier_code
class TierLimitExceededException(BillingException):
"""Raised when a tier limit is exceeded."""
def __init__(self, message: str, limit_type: str, current: int, limit: int):
super().__init__(message)
super().__init__(
message=message,
error_code="TIER_LIMIT_EXCEEDED",
details={
"limit_type": limit_type,
"current": current,
"limit": limit,
},
)
self.limit_type = limit_type
self.current = current
self.limit = limit
# =============================================================================
# Payment Exceptions
# =============================================================================
class PaymentSystemNotConfiguredException(ServiceUnavailableException):
"""Raised when the payment system (Stripe) is not configured."""
def __init__(self):
super().__init__(message="Payment system not configured")
class StripeNotConfiguredException(BillingException):
"""Raised when Stripe is not configured."""
def __init__(self):
super().__init__(
message="Stripe is not configured",
error_code="STRIPE_NOT_CONFIGURED",
)
class StripePriceNotConfiguredException(BusinessLogicException):
"""Raised when Stripe price is not configured for a tier."""
def __init__(self, tier_code: str):
super().__init__(
message=f"Stripe price not configured for tier '{tier_code}'",
error_code="STRIPE_PRICE_NOT_CONFIGURED",
details={"tier_code": tier_code},
)
self.tier_code = tier_code
class PaymentFailedException(BillingException):
"""Raised when a payment fails."""
def __init__(self, message: str, stripe_error: str | None = None):
details = {}
if stripe_error:
details["stripe_error"] = stripe_error
super().__init__(
message=message,
error_code="PAYMENT_FAILED",
details=details if details else None,
)
self.stripe_error = stripe_error
# =============================================================================
# Webhook Exceptions
# =============================================================================
class InvalidWebhookSignatureException(BusinessLogicException):
"""Raised when Stripe webhook signature verification fails."""
def __init__(self, message: str = "Invalid webhook signature"):
super().__init__(
message=message,
error_code="INVALID_WEBHOOK_SIGNATURE",
)
class WebhookMissingSignatureException(BusinessLogicException):
"""Raised when Stripe webhook is missing the signature header."""
def __init__(self):
super().__init__(
message="Missing Stripe-Signature header",
error_code="WEBHOOK_MISSING_SIGNATURE",
)
class WebhookVerificationException(BillingException):
"""Raised when webhook signature verification fails."""
def __init__(self, message: str = "Invalid webhook signature"):
super().__init__(
message=message,
error_code="WEBHOOK_VERIFICATION_FAILED",
)
# =============================================================================
# Feature Exceptions
# =============================================================================
class FeatureNotFoundException(ResourceNotFoundException):
"""Raised when a feature is not found."""
def __init__(self, feature_code: str):
super().__init__(
resource_type="Feature",
identifier=feature_code,
message=f"Feature '{feature_code}' not found",
)
self.feature_code = feature_code
class FeatureNotFoundError(ResourceNotFoundException):
"""Feature not found (alternate naming)."""
def __init__(self, feature_code: str):
super().__init__(
resource_type="Feature",
identifier=feature_code,
message=f"Feature '{feature_code}' not found",
)
self.feature_code = feature_code
class FeatureNotAvailableException(BillingException):
"""Raised when a feature is not available in current tier."""
def __init__(self, feature: str, current_tier: str, required_tier: str):
message = f"Feature '{feature}' requires {required_tier} tier (current: {current_tier})"
super().__init__(message)
super().__init__(
message=message,
error_code="FEATURE_NOT_AVAILABLE",
details={
"feature": feature,
"current_tier": current_tier,
"required_tier": required_tier,
},
)
self.feature = feature
self.current_tier = current_tier
self.required_tier = required_tier
class StripeNotConfiguredException(BillingException):
"""Raised when Stripe is not configured."""
class InvalidFeatureCodesError(ValidationException):
"""Invalid feature codes provided."""
def __init__(self):
super().__init__("Stripe is not configured")
class PaymentFailedException(BillingException):
"""Raised when a payment fails."""
def __init__(self, message: str, stripe_error: str | None = None):
super().__init__(message)
self.stripe_error = stripe_error
class WebhookVerificationException(BillingException):
"""Raised when webhook signature verification fails."""
def __init__(self, message: str = "Invalid webhook signature"):
super().__init__(message)
__all__ = [
"BillingException",
"SubscriptionNotFoundException",
"TierNotFoundException",
"TierLimitExceededException",
"FeatureNotAvailableException",
"StripeNotConfiguredException",
"PaymentFailedException",
"WebhookVerificationException",
]
def __init__(self, invalid_codes: set[str]):
codes_str = ", ".join(sorted(invalid_codes))
super().__init__(
message=f"Invalid feature codes: {codes_str}",
details={"invalid_codes": list(invalid_codes)},
)
self.invalid_codes = invalid_codes

View File

@@ -19,7 +19,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.services.feature_service import feature_service
from app.modules.billing.services.feature_service import feature_service
from models.schema.auth import UserContext
admin_features_router = APIRouter(
@@ -211,7 +211,7 @@ def get_feature(
feature = feature_service.get_feature_by_code(db, feature_code)
if not feature:
from app.exceptions import FeatureNotFoundError
from app.modules.billing.exceptions import FeatureNotFoundError
raise FeatureNotFoundError(feature_code)

View File

@@ -1,4 +1,4 @@
# app/api/v1/platform/pricing.py
# app/modules/billing/routes/api/public.py
"""
Public pricing API endpoints.
@@ -14,10 +14,10 @@ from sqlalchemy.orm import Session
from app.core.database import get_db
from app.exceptions import ResourceNotFoundException
from app.services.platform_pricing_service import platform_pricing_service
from app.modules.billing.services.platform_pricing_service import platform_pricing_service
from app.modules.billing.models import TierCode
router = APIRouter()
router = APIRouter(prefix="/pricing")
# =============================================================================
@@ -230,7 +230,7 @@ def get_addons(db: Session = Depends(get_db)) -> list[AddOnResponse]:
return [_addon_to_response(addon) for addon in addons]
@router.get("/pricing", response_model=PricingResponse) # public
@router.get("", response_model=PricingResponse) # public
def get_pricing(db: Session = Depends(get_db)) -> PricingResponse:
"""
Get complete pricing information (tiers + add-ons).

View File

@@ -24,8 +24,8 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, require_module_access
from app.core.database import get_db
from app.exceptions import FeatureNotFoundError
from app.services.feature_service import feature_service
from app.modules.billing.exceptions import FeatureNotFoundError
from app.modules.billing.services.feature_service import feature_service
from models.schema.auth import UserContext
vendor_features_router = APIRouter(
@@ -134,7 +134,7 @@ def get_available_features(
vendor_id = current_user.token_vendor_id
# Get subscription for tier info
from app.services.subscription_service import subscription_service
from app.modules.billing.services.subscription_service import subscription_service
subscription = subscription_service.get_or_create_subscription(db, vendor_id)
tier = subscription.tier_obj
@@ -175,7 +175,7 @@ def get_features(
vendor_id = current_user.token_vendor_id
# Get subscription for tier info
from app.services.subscription_service import subscription_service
from app.modules.billing.services.subscription_service import subscription_service
subscription = subscription_service.get_or_create_subscription(db, vendor_id)
tier = subscription.tier_obj

View File

@@ -18,7 +18,7 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, require_module_access
from app.core.database import get_db
from app.services.usage_service import usage_service
from app.modules.analytics.services.usage_service import usage_service
from models.schema.auth import UserContext
vendor_usage_router = APIRouter(

View File

@@ -1,18 +1,2 @@
# app/modules/billing/routes/pages/__init__.py
"""
Billing module page routes (HTML rendering).
Provides Jinja2 template rendering for billing management:
- Admin pages: Subscription tiers, subscriptions list, billing history
- Vendor pages: Billing dashboard, invoices
Note: These routes are placeholders. The actual page rendering
is currently handled by routes in app/api/v1/ and can be migrated here.
"""
__all__ = []
def __getattr__(name: str):
"""Lazy import routers to avoid circular dependencies."""
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
"""Billing module page routes."""

View File

@@ -0,0 +1,80 @@
# app/modules/billing/routes/pages/admin.py
"""
Billing Admin Page Routes (HTML rendering).
Admin pages for billing and subscription management:
- Subscription tiers
- Subscriptions list
- Billing history
"""
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.api.deps import get_db, require_menu_access
from app.modules.core.utils.page_context import get_admin_context
from app.templates_config import templates
from models.database.admin_menu_config import FrontendType
from models.database.user import User
router = APIRouter()
# ============================================================================
# BILLING & SUBSCRIPTIONS ROUTES
# ============================================================================
@router.get("/subscription-tiers", response_class=HTMLResponse, include_in_schema=False)
async def admin_subscription_tiers_page(
request: Request,
current_user: User = Depends(
require_menu_access("subscription-tiers", FrontendType.ADMIN)
),
db: Session = Depends(get_db),
):
"""
Render subscription tiers management page.
Shows all subscription tiers with their limits and pricing.
"""
return templates.TemplateResponse(
"billing/admin/subscription-tiers.html",
get_admin_context(request, current_user),
)
@router.get("/subscriptions", response_class=HTMLResponse, include_in_schema=False)
async def admin_subscriptions_page(
request: Request,
current_user: User = Depends(
require_menu_access("subscriptions", FrontendType.ADMIN)
),
db: Session = Depends(get_db),
):
"""
Render vendor subscriptions management page.
Shows all vendor subscriptions with status and usage.
"""
return templates.TemplateResponse(
"billing/admin/subscriptions.html",
get_admin_context(request, current_user),
)
@router.get("/billing-history", response_class=HTMLResponse, include_in_schema=False)
async def admin_billing_history_page(
request: Request,
current_user: User = Depends(
require_menu_access("billing-history", FrontendType.ADMIN)
),
db: Session = Depends(get_db),
):
"""
Render billing history page.
Shows invoices and payments across all vendors.
"""
return templates.TemplateResponse(
"billing/admin/billing-history.html",
get_admin_context(request, current_user),
)

View File

@@ -0,0 +1,121 @@
# app/modules/billing/routes/pages/public.py
"""
Billing Public Page Routes (HTML rendering).
Public (unauthenticated) pages for pricing and signup:
- Pricing page
- Signup wizard
- Signup success
"""
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.core.database import get_db
from app.modules.billing.models import TIER_LIMITS, TierCode
from app.modules.core.utils.page_context import get_public_context
from app.templates_config import templates
router = APIRouter()
def _get_tiers_data() -> list[dict]:
"""Build tier data for display in templates."""
tiers = []
for tier_code, limits in TIER_LIMITS.items():
tiers.append(
{
"code": tier_code.value,
"name": limits["name"],
"price_monthly": limits["price_monthly_cents"] / 100,
"price_annual": (limits["price_annual_cents"] / 100)
if limits.get("price_annual_cents")
else None,
"orders_per_month": limits.get("orders_per_month"),
"products_limit": limits.get("products_limit"),
"team_members": limits.get("team_members"),
"order_history_months": limits.get("order_history_months"),
"features": limits.get("features", []),
"is_popular": tier_code == TierCode.PROFESSIONAL,
"is_enterprise": tier_code == TierCode.ENTERPRISE,
}
)
return tiers
# ============================================================================
# PRICING PAGE
# ============================================================================
@router.get("/pricing", response_class=HTMLResponse, name="platform_pricing")
async def pricing_page(
request: Request,
db: Session = Depends(get_db),
):
"""
Standalone pricing page with detailed tier comparison.
"""
context = get_public_context(request, db)
context["tiers"] = _get_tiers_data()
context["page_title"] = "Pricing"
return templates.TemplateResponse(
"billing/public/pricing.html",
context,
)
# ============================================================================
# SIGNUP WIZARD
# ============================================================================
@router.get("/signup", response_class=HTMLResponse, name="platform_signup")
async def signup_page(
request: Request,
tier: str | None = None,
annual: bool = False,
db: Session = Depends(get_db),
):
"""
Multi-step signup wizard.
Query params:
- tier: Pre-selected tier code
- annual: Pre-select annual billing
"""
context = get_public_context(request, db)
context["page_title"] = "Start Your Free Trial"
context["selected_tier"] = tier
context["is_annual"] = annual
context["tiers"] = _get_tiers_data()
return templates.TemplateResponse(
"billing/public/signup.html",
context,
)
@router.get(
"/signup/success", response_class=HTMLResponse, name="platform_signup_success"
)
async def signup_success_page(
request: Request,
vendor_code: str | None = None,
db: Session = Depends(get_db),
):
"""
Signup success page.
Shown after successful account creation.
"""
context = get_public_context(request, db)
context["page_title"] = "Welcome to Wizamart!"
context["vendor_code"] = vendor_code
return templates.TemplateResponse(
"billing/public/signup-success.html",
context,
)

View File

@@ -0,0 +1,62 @@
# app/modules/billing/routes/pages/vendor.py
"""
Billing Vendor Page Routes (HTML rendering).
Vendor pages for billing management:
- Billing dashboard
- Invoices
"""
from fastapi import APIRouter, Depends, Path, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_from_cookie_or_header, get_db
from app.modules.core.utils.page_context import get_vendor_context
from app.templates_config import templates
from models.database.user import User
router = APIRouter()
# ============================================================================
# BILLING ROUTES
# ============================================================================
@router.get(
"/{vendor_code}/billing", response_class=HTMLResponse, include_in_schema=False
)
async def vendor_billing_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
db: Session = Depends(get_db),
):
"""
Render billing and subscription management page.
JavaScript loads subscription status, tiers, and invoices via API.
"""
return templates.TemplateResponse(
"billing/vendor/billing.html",
get_vendor_context(request, db, current_user, vendor_code),
)
@router.get(
"/{vendor_code}/invoices", response_class=HTMLResponse, include_in_schema=False
)
async def vendor_invoices_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
db: Session = Depends(get_db),
):
"""
Render invoices management page.
JavaScript loads invoices via API.
"""
return templates.TemplateResponse(
"orders/vendor/invoices.html",
get_vendor_context(request, db, current_user, vendor_code),
)

View File

@@ -27,6 +27,21 @@ from app.modules.billing.services.billing_service import (
NoActiveSubscriptionError,
SubscriptionNotCancelledError,
)
from app.modules.billing.services.feature_service import (
FeatureService,
feature_service,
FeatureInfo,
FeatureUpgradeInfo,
FeatureCode,
)
from app.modules.billing.services.capacity_forecast_service import (
CapacityForecastService,
capacity_forecast_service,
)
from app.modules.billing.services.platform_pricing_service import (
PlatformPricingService,
platform_pricing_service,
)
__all__ = [
"SubscriptionService",
@@ -43,4 +58,13 @@ __all__ = [
"StripePriceNotConfiguredError",
"NoActiveSubscriptionError",
"SubscriptionNotCancelledError",
"FeatureService",
"feature_service",
"FeatureInfo",
"FeatureUpgradeInfo",
"FeatureCode",
"CapacityForecastService",
"capacity_forecast_service",
"PlatformPricingService",
"platform_pricing_service",
]

View File

@@ -162,7 +162,7 @@ class BillingService:
Raises:
VendorNotFoundException from app.exceptions
"""
from app.exceptions import VendorNotFoundException
from app.modules.tenancy.exceptions import VendorNotFoundException
vendor = db.query(Vendor).filter(Vendor.id == vendor_id).first()
if not vendor:

View File

@@ -1,4 +1,4 @@
# app/services/capacity_forecast_service.py
# app/modules/billing/services/capacity_forecast_service.py
"""
Capacity forecasting service for growth trends and scaling recommendations.
@@ -47,8 +47,8 @@ class CapacityForecastService:
Should be called by a daily background job.
"""
from app.services.image_service import image_service
from app.services.platform_health_service import platform_health_service
from app.modules.core.services.image_service import image_service
from app.modules.monitoring.services.platform_health_service import platform_health_service
now = datetime.now(UTC)
today = now.replace(hour=0, minute=0, second=0, microsecond=0)
@@ -228,7 +228,7 @@ class CapacityForecastService:
Returns prioritized list of recommendations.
"""
from app.services.platform_health_service import platform_health_service
from app.modules.monitoring.services.platform_health_service import platform_health_service
recommendations = []

View File

@@ -1,4 +1,4 @@
# app/services/feature_service.py
# app/modules/billing/services/feature_service.py
"""
Feature service for tier-based access control.
@@ -9,7 +9,7 @@ Provides:
- Cache invalidation on subscription changes
Usage:
from app.services.feature_service import feature_service
from app.modules.billing.services.feature_service import feature_service
# Check if vendor has feature
if feature_service.has_feature(db, vendor_id, FeatureCode.ANALYTICS_DASHBOARD):
@@ -29,7 +29,7 @@ from functools import lru_cache
from sqlalchemy.orm import Session, joinedload
from app.exceptions.feature import (
from app.modules.billing.exceptions import (
FeatureNotFoundError,
InvalidFeatureCodesError,
TierNotFoundError,

View File

@@ -1,4 +1,4 @@
# app/services/platform_pricing_service.py
# app/modules/billing/services/platform_pricing_service.py
"""
Platform pricing service.

View File

@@ -1,6 +1,6 @@
{# app/templates/platform/pricing.html #}
{# Standalone Pricing Page #}
{% extends "platform/base.html" %}
{% extends "public/base.html" %}
{% block title %}{{ _("platform.pricing.title") }} - Wizamart{% endblock %}

View File

@@ -1,6 +1,6 @@
{# app/templates/platform/signup-success.html #}
{# Signup Success Page #}
{% extends "platform/base.html" %}
{% extends "public/base.html" %}
{% block title %}{{ _("platform.success.title") }}{% endblock %}

View File

@@ -1,6 +1,6 @@
{# app/templates/platform/signup.html #}
{# Multi-step Signup Wizard #}
{% extends "platform/base.html" %}
{% extends "public/base.html" %}
{% block title %}Start Your Free Trial - Wizamart{% endblock %}
@@ -321,7 +321,7 @@ function signupWizard() {
async startSignup() {
this.loading = true;
try {
const response = await fetch('/api/v1/platform/signup/start', {
const response = await fetch('/api/v1/public/signup/start', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -352,7 +352,7 @@ function signupWizard() {
try {
// First lookup the vendor
const lookupResponse = await fetch('/api/v1/platform/letzshop-vendors/lookup', {
const lookupResponse = await fetch('/api/v1/public/letzshop-vendors/lookup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ url: this.letzshopUrl })
@@ -364,7 +364,7 @@ function signupWizard() {
this.letzshopVendor = lookupData.vendor;
// Claim the vendor
const claimResponse = await fetch('/api/v1/platform/signup/claim-vendor', {
const claimResponse = await fetch('/api/v1/public/signup/claim-vendor', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -411,7 +411,7 @@ function signupWizard() {
this.accountError = null;
try {
const response = await fetch('/api/v1/platform/signup/create-account', {
const response = await fetch('/api/v1/public/signup/create-account', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
@@ -461,7 +461,7 @@ function signupWizard() {
// Get SetupIntent
try {
const response = await fetch('/api/v1/platform/signup/setup-payment', {
const response = await fetch('/api/v1/public/signup/setup-payment', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ session_id: this.sessionId })
@@ -500,7 +500,7 @@ function signupWizard() {
}
// Complete signup
const response = await fetch('/api/v1/platform/signup/complete', {
const response = await fetch('/api/v1/public/signup/complete', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({

View File

@@ -2,7 +2,10 @@
"""
Cart module exceptions.
Module-specific exceptions for shopping cart operations.
This module provides exception classes for cart operations including:
- Cart item management
- Quantity validation
- Inventory checks
"""
from app.exceptions.base import (
@@ -11,6 +14,15 @@ from app.exceptions.base import (
ValidationException,
)
__all__ = [
"CartItemNotFoundException",
"EmptyCartException",
"CartValidationException",
"InsufficientInventoryForCartException",
"InvalidCartQuantityException",
"ProductNotAvailableForCartException",
]
class CartItemNotFoundException(ResourceNotFoundException):
"""Raised when a cart item is not found."""
@@ -115,11 +127,3 @@ class ProductNotAvailableForCartException(BusinessLogicException):
)
__all__ = [
"CartItemNotFoundException",
"CartValidationException",
"EmptyCartException",
"InsufficientInventoryForCartException",
"InvalidCartQuantityException",
"ProductNotAvailableForCartException",
]

View File

@@ -0,0 +1,2 @@
# app/modules/cart/routes/pages/__init__.py
"""Cart module page routes."""

View File

@@ -0,0 +1,46 @@
# app/modules/cart/routes/pages/storefront.py
"""
Cart Storefront Page Routes (HTML rendering).
Storefront (customer shop) pages for shopping cart:
- Cart page
"""
import logging
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.api.deps import get_db
from app.modules.core.utils.page_context import get_storefront_context
from app.templates_config import templates
logger = logging.getLogger(__name__)
router = APIRouter()
# ============================================================================
# SHOPPING CART
# ============================================================================
@router.get("/cart", response_class=HTMLResponse, include_in_schema=False)
async def shop_cart_page(request: Request, db: Session = Depends(get_db)):
"""
Render shopping cart page.
Shows cart items and allows quantity updates.
"""
logger.debug(
"[STOREFRONT] shop_cart_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, "vendor", "NOT SET"),
"context": getattr(request.state, "context_type", "NOT SET"),
},
)
return templates.TemplateResponse(
"cart/storefront/cart.html", get_storefront_context(request, db=db)
)

View File

@@ -16,12 +16,12 @@ import logging
from sqlalchemy import and_
from sqlalchemy.orm import Session
from app.exceptions import (
from app.modules.cart.exceptions import (
CartItemNotFoundException,
InsufficientInventoryForCartException,
InvalidCartQuantityException,
ProductNotFoundException,
)
from app.modules.catalog.exceptions import ProductNotFoundException
from app.utils.money import cents_to_euros
from app.modules.cart.models.cart import CartItem
from app.modules.catalog.models import Product

View File

@@ -2,7 +2,10 @@
"""
Catalog module exceptions.
Module-specific exceptions for product catalog operations.
This module provides exception classes for catalog operations including:
- Product management (CRUD)
- Product validation
- Product dependencies (inventory, orders)
"""
from app.exceptions.base import (
@@ -12,6 +15,19 @@ from app.exceptions.base import (
ValidationException,
)
__all__ = [
"ProductNotFoundException",
"ProductAlreadyExistsException",
"ProductNotInCatalogException",
"ProductNotActiveException",
"InvalidProductDataException",
"ProductValidationException",
"CannotDeleteProductException",
"CannotDeleteProductWithInventoryException",
"CannotDeleteProductWithOrdersException",
"ProductMediaException",
]
class ProductNotFoundException(ResourceNotFoundException):
"""Raised when a product is not found in vendor catalog."""
@@ -56,11 +72,9 @@ class ProductNotInCatalogException(ResourceNotFoundException):
identifier=str(product_id),
message=f"Product {product_id} is not in vendor {vendor_id} catalog",
error_code="PRODUCT_NOT_IN_CATALOG",
details={
"product_id": product_id,
"vendor_id": vendor_id,
},
)
self.details["product_id"] = product_id
self.details["vendor_id"] = vendor_id
class ProductNotActiveException(BusinessLogicException):
@@ -77,6 +91,23 @@ class ProductNotActiveException(BusinessLogicException):
)
class InvalidProductDataException(ValidationException):
"""Raised when product data is invalid."""
def __init__(
self,
message: str = "Invalid product data",
field: str | None = None,
details: dict | None = None,
):
super().__init__(
message=message,
field=field,
details=details,
)
self.error_code = "INVALID_PRODUCT_DATA"
class ProductValidationException(ValidationException):
"""Raised when product data validation fails."""
@@ -109,6 +140,34 @@ class CannotDeleteProductException(BusinessLogicException):
)
class CannotDeleteProductWithInventoryException(BusinessLogicException):
"""Raised when trying to delete a product that has inventory."""
def __init__(self, product_id: int, inventory_count: int):
super().__init__(
message=f"Cannot delete product {product_id} - it has {inventory_count} inventory entries",
error_code="CANNOT_DELETE_PRODUCT_WITH_INVENTORY",
details={
"product_id": product_id,
"inventory_count": inventory_count,
},
)
class CannotDeleteProductWithOrdersException(BusinessLogicException):
"""Raised when trying to delete a product that has been ordered."""
def __init__(self, product_id: int, order_count: int):
super().__init__(
message=f"Cannot delete product {product_id} - it has {order_count} associated orders",
error_code="CANNOT_DELETE_PRODUCT_WITH_ORDERS",
details={
"product_id": product_id,
"order_count": order_count,
},
)
class ProductMediaException(BusinessLogicException):
"""Raised when there's an issue with product media."""
@@ -118,14 +177,3 @@ class ProductMediaException(BusinessLogicException):
error_code="PRODUCT_MEDIA_ERROR",
details={"product_id": product_id},
)
__all__ = [
"CannotDeleteProductException",
"ProductAlreadyExistsException",
"ProductMediaException",
"ProductNotActiveException",
"ProductNotFoundException",
"ProductNotInCatalogException",
"ProductValidationException",
]

View File

@@ -17,8 +17,8 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_admin_api, require_module_access
from app.core.database import get_db
from app.services.subscription_service import subscription_service
from app.services.vendor_product_service import vendor_product_service
from app.modules.billing.services.subscription_service import subscription_service
from app.modules.catalog.services.vendor_product_service import vendor_product_service
from models.schema.auth import UserContext
from app.modules.catalog.schemas import (
CatalogVendor,

View File

@@ -109,7 +109,7 @@ def get_product_details(
# Check if product is active
if not product.is_active:
from app.exceptions import ProductNotActiveException
from app.modules.catalog.exceptions import ProductNotActiveException
raise ProductNotActiveException(str(product_id))

View File

@@ -15,9 +15,9 @@ from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, require_module_access
from app.core.database import get_db
from app.services.product_service import product_service
from app.services.subscription_service import subscription_service
from app.services.vendor_product_service import vendor_product_service
from app.modules.catalog.services.product_service import product_service
from app.modules.billing.services.subscription_service import subscription_service
from app.modules.catalog.services.vendor_product_service import vendor_product_service
from models.schema.auth import UserContext
from app.modules.catalog.schemas import (
ProductCreate,

View File

@@ -0,0 +1,2 @@
# app/modules/catalog/routes/pages/__init__.py
"""Catalog module page routes."""

View File

@@ -0,0 +1,110 @@
# app/modules/catalog/routes/pages/admin.py
"""
Catalog Admin Page Routes (HTML rendering).
Admin pages for vendor product catalog management:
- Vendor products list
- Vendor product create
- Vendor product detail/edit
"""
from fastapi import APIRouter, Depends, Path, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.api.deps import get_db, require_menu_access
from app.modules.core.utils.page_context import get_admin_context
from app.templates_config import templates
from models.database.admin_menu_config import FrontendType
from models.database.user import User
router = APIRouter()
# ============================================================================
# VENDOR PRODUCT CATALOG ROUTES
# ============================================================================
@router.get("/vendor-products", response_class=HTMLResponse, include_in_schema=False)
async def admin_vendor_products_page(
request: Request,
current_user: User = Depends(
require_menu_access("vendor-products", FrontendType.ADMIN)
),
db: Session = Depends(get_db),
):
"""
Render vendor products catalog page.
Browse vendor-specific product catalogs with override capability.
"""
return templates.TemplateResponse(
"catalog/admin/vendor-products.html",
get_admin_context(request, current_user),
)
@router.get(
"/vendor-products/create", response_class=HTMLResponse, include_in_schema=False
)
async def admin_vendor_product_create_page(
request: Request,
current_user: User = Depends(
require_menu_access("vendor-products", FrontendType.ADMIN)
),
db: Session = Depends(get_db),
):
"""
Render vendor product create page.
Create a new vendor product entry.
"""
return templates.TemplateResponse(
"catalog/admin/vendor-product-create.html",
get_admin_context(request, current_user),
)
@router.get(
"/vendor-products/{product_id}",
response_class=HTMLResponse,
include_in_schema=False,
)
async def admin_vendor_product_detail_page(
request: Request,
product_id: int = Path(..., description="Vendor Product ID"),
current_user: User = Depends(
require_menu_access("vendor-products", FrontendType.ADMIN)
),
db: Session = Depends(get_db),
):
"""
Render vendor product detail page.
Shows full product information with vendor-specific overrides.
"""
return templates.TemplateResponse(
"catalog/admin/vendor-product-detail.html",
get_admin_context(request, current_user, product_id=product_id),
)
@router.get(
"/vendor-products/{product_id}/edit",
response_class=HTMLResponse,
include_in_schema=False,
)
async def admin_vendor_product_edit_page(
request: Request,
product_id: int = Path(..., description="Vendor Product ID"),
current_user: User = Depends(
require_menu_access("vendor-products", FrontendType.ADMIN)
),
db: Session = Depends(get_db),
):
"""
Render vendor product edit page.
Edit vendor product information and overrides.
"""
return templates.TemplateResponse(
"catalog/admin/vendor-product-edit.html",
get_admin_context(request, current_user, product_id=product_id),
)

View File

@@ -0,0 +1,171 @@
# app/modules/catalog/routes/pages/storefront.py
"""
Catalog Storefront Page Routes (HTML rendering).
Storefront (customer shop) pages for catalog browsing:
- Shop homepage / product catalog
- Product list
- Product detail
- Category products
- Search results
"""
import logging
from fastapi import APIRouter, Depends, Path, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.api.deps import get_db
from app.modules.core.utils.page_context import get_storefront_context
from app.templates_config import templates
logger = logging.getLogger(__name__)
router = APIRouter()
# ============================================================================
# PUBLIC SHOP ROUTES (No Authentication Required)
# ============================================================================
@router.get("/", response_class=HTMLResponse, include_in_schema=False)
@router.get("/products", response_class=HTMLResponse, include_in_schema=False)
async def shop_products_page(request: Request, db: Session = Depends(get_db)):
"""
Render shop homepage / product catalog.
Shows featured products and categories.
"""
logger.debug(
"[STOREFRONT] shop_products_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, "vendor", "NOT SET"),
"context": getattr(request.state, "context_type", "NOT SET"),
},
)
return templates.TemplateResponse(
"catalog/storefront/products.html", get_storefront_context(request, db=db)
)
@router.get(
"/products/{product_id}", response_class=HTMLResponse, include_in_schema=False
)
async def shop_product_detail_page(
request: Request,
product_id: int = Path(..., description="Product ID"),
db: Session = Depends(get_db),
):
"""
Render product detail page.
Shows product information, images, reviews, and buy options.
"""
logger.debug(
"[STOREFRONT] shop_product_detail_page REACHED",
extra={
"path": request.url.path,
"product_id": product_id,
"vendor": getattr(request.state, "vendor", "NOT SET"),
"context": getattr(request.state, "context_type", "NOT SET"),
},
)
return templates.TemplateResponse(
"catalog/storefront/product.html",
get_storefront_context(request, db=db, product_id=product_id),
)
@router.get(
"/categories/{category_slug}",
response_class=HTMLResponse,
include_in_schema=False,
)
async def shop_category_page(
request: Request,
category_slug: str = Path(..., description="Category slug"),
db: Session = Depends(get_db),
):
"""
Render category products page.
Shows all products in a specific category.
"""
logger.debug(
"[STOREFRONT] shop_category_page REACHED",
extra={
"path": request.url.path,
"category_slug": category_slug,
"vendor": getattr(request.state, "vendor", "NOT SET"),
"context": getattr(request.state, "context_type", "NOT SET"),
},
)
return templates.TemplateResponse(
"catalog/storefront/category.html",
get_storefront_context(request, db=db, category_slug=category_slug),
)
@router.get("/search", response_class=HTMLResponse, include_in_schema=False)
async def shop_search_page(request: Request, db: Session = Depends(get_db)):
"""
Render search results page.
Shows products matching search query.
"""
logger.debug(
"[STOREFRONT] shop_search_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, "vendor", "NOT SET"),
"context": getattr(request.state, "context_type", "NOT SET"),
},
)
return templates.TemplateResponse(
"catalog/storefront/search.html", get_storefront_context(request, db=db)
)
# ============================================================================
# CUSTOMER ACCOUNT - WISHLIST
# ============================================================================
@router.get(
"/account/wishlist", response_class=HTMLResponse, include_in_schema=False
)
async def shop_wishlist_page(
request: Request,
db: Session = Depends(get_db),
):
"""
Render customer wishlist page.
View and manage saved products.
Requires customer authentication (handled by middleware/template).
"""
from app.api.deps import get_current_customer_from_cookie_or_header
# Get customer if authenticated
try:
current_customer = await get_current_customer_from_cookie_or_header(
request=request, db=db
)
except Exception:
current_customer = None
logger.debug(
"[STOREFRONT] shop_wishlist_page REACHED",
extra={
"path": request.url.path,
"vendor": getattr(request.state, "vendor", "NOT SET"),
"context": getattr(request.state, "context_type", "NOT SET"),
},
)
return templates.TemplateResponse(
"catalog/storefront/wishlist.html",
get_storefront_context(request, db=db, user=current_customer),
)

View File

@@ -0,0 +1,64 @@
# app/modules/catalog/routes/pages/vendor.py
"""
Catalog Vendor Page Routes (HTML rendering).
Vendor pages for product management:
- Products list
- Product create
"""
from fastapi import APIRouter, Depends, Path, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_from_cookie_or_header, get_db
from app.modules.core.utils.page_context import get_vendor_context
from app.templates_config import templates
from models.database.user import User
router = APIRouter()
# ============================================================================
# PRODUCT MANAGEMENT
# ============================================================================
@router.get(
"/{vendor_code}/products", response_class=HTMLResponse, include_in_schema=False
)
async def vendor_products_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
db: Session = Depends(get_db),
):
"""
Render products management page.
JavaScript loads product list via API.
"""
return templates.TemplateResponse(
"catalog/vendor/products.html",
get_vendor_context(request, db, current_user, vendor_code),
)
@router.get(
"/{vendor_code}/products/create",
response_class=HTMLResponse,
include_in_schema=False,
)
async def vendor_product_create_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
db: Session = Depends(get_db),
):
"""
Render product creation page.
JavaScript handles form submission via API.
"""
return templates.TemplateResponse(
"catalog/vendor/product-create.html",
get_vendor_context(request, db, current_user, vendor_code),
)

View File

@@ -2,5 +2,16 @@
"""Catalog module services."""
from app.modules.catalog.services.catalog_service import catalog_service
from app.modules.catalog.services.product_service import ProductService, product_service
from app.modules.catalog.services.vendor_product_service import (
VendorProductService,
vendor_product_service,
)
__all__ = ["catalog_service"]
__all__ = [
"catalog_service",
"ProductService",
"product_service",
"VendorProductService",
"vendor_product_service",
]

View File

@@ -17,7 +17,8 @@ import logging
from sqlalchemy import or_
from sqlalchemy.orm import Session, joinedload
from app.exceptions import ProductNotFoundException, ValidationException
from app.exceptions import ValidationException
from app.modules.catalog.exceptions import ProductNotFoundException
from app.modules.catalog.models import Product, ProductTranslation
logger = logging.getLogger(__name__)

View File

@@ -1,4 +1,4 @@
# app/services/product_service.py
# app/modules/catalog/services/product_service.py
"""
Product service for vendor catalog management.
@@ -13,10 +13,10 @@ from datetime import UTC, datetime
from sqlalchemy.orm import Session
from app.exceptions import (
from app.exceptions import ValidationException
from app.modules.catalog.exceptions import (
ProductAlreadyExistsException,
ProductNotFoundException,
ValidationException,
)
from app.modules.marketplace.models import MarketplaceProduct
from app.modules.catalog.models import Product

View File

@@ -1,4 +1,4 @@
# app/services/vendor_product_service.py
# app/modules/catalog/services/vendor_product_service.py
"""
Vendor product service for managing vendor-specific product catalogs.
@@ -14,7 +14,7 @@ import logging
from sqlalchemy import func
from sqlalchemy.orm import Session, joinedload
from app.exceptions import ProductNotFoundException
from app.modules.catalog.exceptions import ProductNotFoundException
from app.modules.catalog.models import Product
from models.database.vendor import Vendor

View File

@@ -507,6 +507,6 @@
document.head.appendChild(script);
})();
</script>
<script src="{{ url_for('static', path='shared/js/media-picker.js') }}"></script>
<script src="{{ url_for('cms_static', path='shared/js/media-picker.js') }}"></script>
<script src="{{ url_for('catalog_static', path='admin/js/product-create.js') }}"></script>
{% endblock %}

View File

@@ -498,6 +498,6 @@
{% endblock %}
{% block extra_scripts %}
<script src="{{ url_for('static', path='shared/js/media-picker.js') }}"></script>
<script src="{{ url_for('cms_static', path='shared/js/media-picker.js') }}"></script>
<script src="{{ url_for('catalog_static', path='admin/js/product-edit.js') }}"></script>
{% endblock %}

View File

@@ -0,0 +1,281 @@
{# app/modules/catalog/templates/catalog/storefront/category.html #}
{% extends "storefront/base.html" %}
{% block title %}{{ category_slug | replace('-', ' ') | title if category_slug else 'Category' }}{% endblock %}
{# Alpine.js component #}
{% block alpine_data %}shopCategory(){% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{# Breadcrumbs #}
<div class="breadcrumb mb-6">
<a href="{{ base_url }}" class="hover:text-primary">Home</a>
<span>/</span>
<a href="{{ base_url }}shop/products" class="hover:text-primary">Products</a>
<span>/</span>
<span class="text-gray-900 dark:text-gray-200 font-medium" x-text="categoryName">{{ category_slug | replace('-', ' ') | title if category_slug else 'Category' }}</span>
</div>
{# Page Header #}
<div class="mb-8">
<h1 class="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-2" x-text="categoryName">
{{ category_slug | replace('-', ' ') | title if category_slug else 'Category' }}
</h1>
<p class="text-gray-600 dark:text-gray-400" x-show="!loading && total > 0">
<span x-text="total" class="font-semibold"></span> product<span x-show="total !== 1">s</span> in this category
</p>
</div>
{# Sort Bar #}
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm p-4 mb-6">
<div class="flex flex-wrap items-center justify-between gap-4">
<div class="text-sm text-gray-600 dark:text-gray-400">
Showing <span x-text="products.length" class="font-semibold"></span> of <span x-text="total" class="font-semibold"></span> products
</div>
<div class="flex items-center gap-2">
<label class="text-sm text-gray-600 dark:text-gray-400">Sort by:</label>
<select
x-model="sortBy"
@change="loadProducts()"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg focus:ring-2 focus:ring-primary focus:border-transparent dark:bg-gray-700 dark:text-white"
>
<option value="newest">Newest First</option>
<option value="price-low">Price: Low to High</option>
<option value="price-high">Price: High to Low</option>
<option value="popular">Most Popular</option>
</select>
</div>
</div>
</div>
{# Products Grid #}
<div>
{# Loading State #}
<div x-show="loading" class="flex justify-center items-center py-12">
<div class="spinner"></div>
</div>
{# Products Grid #}
<div x-show="!loading && products.length > 0" class="product-grid">
<template x-for="product in products" :key="product.id">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm hover:shadow-md transition-shadow overflow-hidden">
<a :href="`{{ base_url }}shop/products/${product.id}`">
<img :src="product.marketplace_product?.image_link || '/static/shop/img/placeholder.svg'"
@error="$el.src = '/static/shop/img/placeholder.svg'"
:alt="product.marketplace_product?.title"
class="w-full h-48 object-cover">
</a>
<div class="p-4">
<a :href="`{{ base_url }}shop/products/${product.id}`" class="block">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2 line-clamp-2" x-text="product.marketplace_product?.title"></h3>
</a>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2" x-text="product.marketplace_product?.description"></p>
<div class="flex items-center justify-between gap-2">
<div class="min-w-0">
<span class="text-xl sm:text-2xl font-bold text-primary" x-text="formatPrice(product.price)"></span>
<span x-show="product.sale_price" class="text-sm text-gray-500 line-through ml-2" x-text="formatPrice(product.sale_price)"></span>
</div>
<button @click.prevent="addToCart(product)"
class="flex-shrink-0 p-2 sm:px-4 sm:py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors flex items-center justify-center gap-2"
style="background-color: var(--color-primary)"
:title="'Add to Cart'">
<span class="w-5 h-5" x-html="$icon('shopping-cart', 'w-5 h-5')"></span>
<span class="hidden sm:inline">Add to Cart</span>
</button>
</div>
</div>
</div>
</template>
</div>
{# No Products Message #}
<div x-show="!loading && products.length === 0" class="col-span-full text-center py-12 bg-white dark:bg-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600">
<div class="text-6xl mb-4">📦</div>
<h3 class="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-2">
No Products in This Category
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-4">
Check back later or browse other categories.
</p>
<a href="{{ base_url }}shop/products" class="inline-block px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors" style="background-color: var(--color-primary)">
Browse All Products
</a>
</div>
{# Pagination #}
<div x-show="!loading && totalPages > 1" class="mt-8 flex justify-center">
<div class="flex gap-2">
<button
@click="goToPage(currentPage - 1)"
:disabled="currentPage === 1"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Previous
</button>
<template x-for="page in visiblePages" :key="page">
<button
@click="goToPage(page)"
:class="page === currentPage ? 'bg-primary text-white' : 'border border-gray-300 dark:border-gray-600 hover:bg-gray-100 dark:hover:bg-gray-700'"
class="px-4 py-2 rounded-lg"
:style="page === currentPage ? 'background-color: var(--color-primary)' : ''"
x-text="page"
></button>
</template>
<button
@click="goToPage(currentPage + 1)"
:disabled="currentPage === totalPages"
class="px-4 py-2 border border-gray-300 dark:border-gray-600 rounded-lg hover:bg-gray-100 dark:hover:bg-gray-700 disabled:opacity-50 disabled:cursor-not-allowed"
>
Next
</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
// Pass category slug from template to JavaScript
window.CATEGORY_SLUG = '{{ category_slug | default("") }}';
document.addEventListener('alpine:init', () => {
Alpine.data('shopCategory', () => ({
...shopLayoutData(),
// Data
categorySlug: window.CATEGORY_SLUG,
categoryName: '',
products: [],
total: 0,
loading: true,
sortBy: 'newest',
// Pagination
currentPage: 1,
perPage: 12,
get totalPages() {
return Math.ceil(this.total / this.perPage);
},
get visiblePages() {
const pages = [];
const total = this.totalPages;
const current = this.currentPage;
let start = Math.max(1, current - 2);
let end = Math.min(total, current + 2);
if (end - start < 4) {
if (start === 1) {
end = Math.min(total, 5);
} else {
start = Math.max(1, total - 4);
}
}
for (let i = start; i <= end; i++) {
pages.push(i);
}
return pages;
},
async init() {
console.log('[SHOP] Category page initializing...');
console.log('[SHOP] Category slug:', this.categorySlug);
// Convert slug to display name
this.categoryName = this.categorySlug
.split('-')
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
await this.loadProducts();
},
async loadProducts() {
this.loading = true;
try {
const params = new URLSearchParams({
skip: (this.currentPage - 1) * this.perPage,
limit: this.perPage,
category: this.categorySlug
});
if (this.sortBy) {
params.append('sort', this.sortBy);
}
console.log(`[SHOP] Loading category products from /api/v1/shop/products?${params}`);
const response = await fetch(`/api/v1/shop/products?${params}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log(`[SHOP] Loaded ${data.products.length} products (total: ${data.total})`);
this.products = data.products;
this.total = data.total;
} catch (error) {
console.error('[SHOP] Failed to load category products:', error);
this.showToast('Failed to load products', 'error');
} finally {
this.loading = false;
}
},
async goToPage(page) {
if (page < 1 || page > this.totalPages) return;
this.currentPage = page;
await this.loadProducts();
window.scrollTo({ top: 0, behavior: 'smooth' });
},
async addToCart(product) {
console.log('[SHOP] Adding to cart:', product);
try {
const url = `/api/v1/shop/cart/${this.sessionId}/items`;
const payload = {
product_id: product.id,
quantity: 1
};
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
const result = await response.json();
console.log('[SHOP] Add to cart success:', result);
this.cartCount += 1;
this.showToast(`${product.marketplace_product.title} added to cart`, 'success');
} else {
const error = await response.json();
console.error('[SHOP] Add to cart error:', error);
this.showToast(error.message || 'Failed to add to cart', 'error');
}
} catch (error) {
console.error('[SHOP] Add to cart exception:', error);
this.showToast('Failed to add to cart', 'error');
}
}
}));
});
</script>
{% endblock %}

View File

@@ -1,4 +1,4 @@
{# app/templates/storefront/product.html #}
{# app/modules/catalog/templates/catalog/storefront/product.html #}
{% extends "storefront/base.html" %}
{% block title %}{{ product.name if product else 'Product' }}{% endblock %}

View File

@@ -1,4 +1,4 @@
{# app/templates/storefront/products.html #}
{# app/modules/catalog/templates/catalog/storefront/products.html #}
{% extends "storefront/base.html" %}
{% block title %}Products{% endblock %}

View File

@@ -1,4 +1,4 @@
{# app/templates/storefront/search.html #}
{# app/modules/catalog/templates/catalog/storefront/search.html #}
{# noqa: FE-001 - Shop uses custom pagination with vendor-themed styling (CSS variables) #}
{% extends "storefront/base.html" %}

View File

@@ -0,0 +1,252 @@
{# app/modules/catalog/templates/catalog/storefront/wishlist.html #}
{% extends "storefront/base.html" %}
{% block title %}My Wishlist{% endblock %}
{# Alpine.js component #}
{% block alpine_data %}shopWishlist(){% endblock %}
{% block content %}
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
{# Breadcrumbs #}
<div class="breadcrumb mb-6">
<a href="{{ base_url }}" class="hover:text-primary">Home</a>
<span>/</span>
<a href="{{ base_url }}shop/account/dashboard" class="hover:text-primary">Account</a>
<span>/</span>
<span class="text-gray-900 dark:text-gray-200 font-medium">Wishlist</span>
</div>
{# Page Header #}
<div class="mb-8">
<h1 class="text-3xl md:text-4xl font-bold text-gray-800 dark:text-gray-200 mb-2">
My Wishlist
</h1>
<p class="text-gray-600 dark:text-gray-400" x-show="!loading && items.length > 0">
<span x-text="items.length" class="font-semibold"></span> saved item<span x-show="items.length !== 1">s</span>
</p>
</div>
{# Wishlist Content #}
<div>
{# Loading State #}
<div x-show="loading" class="flex justify-center items-center py-12">
<div class="spinner"></div>
</div>
{# Not Logged In Message #}
<div x-show="!loading && !isLoggedIn" class="text-center py-12 bg-white dark:bg-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600">
<div class="text-6xl mb-4">
<span x-html="$icon('user', 'w-16 h-16 mx-auto text-gray-400')"></span>
</div>
<h3 class="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-2">
Please Log In
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-6">
Log in to your account to view and manage your wishlist.
</p>
<a href="{{ base_url }}shop/account/login" class="inline-block px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors" style="background-color: var(--color-primary)">
Log In
</a>
</div>
{# Wishlist Items Grid #}
<div x-show="!loading && isLoggedIn && items.length > 0" class="product-grid">
<template x-for="item in items" :key="item.id">
<div class="bg-white dark:bg-gray-800 rounded-lg shadow-sm hover:shadow-md transition-shadow overflow-hidden relative">
{# Remove from Wishlist Button #}
<button
@click="removeFromWishlist(item)"
class="absolute top-2 right-2 z-10 p-2 bg-white dark:bg-gray-700 rounded-full shadow-md hover:bg-red-50 dark:hover:bg-red-900 transition-colors group"
title="Remove from wishlist"
>
<span x-html="$icon('heart', 'w-5 h-5 text-red-500 fill-current')"></span>
</button>
<a :href="`{{ base_url }}shop/products/${item.product.id}`">
<img :src="item.product.marketplace_product?.image_link || '/static/shop/img/placeholder.svg'"
@error="$el.src = '/static/shop/img/placeholder.svg'"
:alt="item.product.marketplace_product?.title"
class="w-full h-48 object-cover">
</a>
<div class="p-4">
<a :href="`{{ base_url }}shop/products/${item.product.id}`" class="block">
<h3 class="text-lg font-semibold text-gray-800 dark:text-gray-200 mb-2 line-clamp-2" x-text="item.product.marketplace_product?.title"></h3>
</a>
<p class="text-sm text-gray-600 dark:text-gray-400 mb-3 line-clamp-2" x-text="item.product.marketplace_product?.description"></p>
{# Availability #}
<div class="mb-3">
<span
x-show="item.product.available_inventory > 0"
class="text-sm text-green-600 dark:text-green-400"
>
In Stock
</span>
<span
x-show="!item.product.available_inventory || item.product.available_inventory <= 0"
class="text-sm text-red-600 dark:text-red-400"
>
Out of Stock
</span>
</div>
<div class="flex items-center justify-between gap-2">
<div class="min-w-0">
<span class="text-xl sm:text-2xl font-bold text-primary" x-text="formatPrice(item.product.price)"></span>
<span x-show="item.product.sale_price" class="text-sm text-gray-500 line-through ml-2" x-text="formatPrice(item.product.sale_price)"></span>
</div>
<button @click.prevent="addToCart(item.product)"
:disabled="!item.product.available_inventory || item.product.available_inventory <= 0"
class="flex-shrink-0 p-2 sm:px-4 sm:py-2 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors flex items-center justify-center gap-2 disabled:opacity-50 disabled:cursor-not-allowed"
style="background-color: var(--color-primary)"
:title="'Add to Cart'">
<span class="w-5 h-5" x-html="$icon('shopping-cart', 'w-5 h-5')"></span>
<span class="hidden sm:inline">Add to Cart</span>
</button>
</div>
</div>
</div>
</template>
</div>
{# Empty Wishlist Message #}
<div x-show="!loading && isLoggedIn && items.length === 0" class="text-center py-12 bg-white dark:bg-gray-800 rounded-lg border-2 border-dashed border-gray-300 dark:border-gray-600">
<div class="text-6xl mb-4">
<span x-html="$icon('heart', 'w-16 h-16 mx-auto text-gray-400')"></span>
</div>
<h3 class="text-2xl font-semibold text-gray-700 dark:text-gray-200 mb-2">
Your Wishlist is Empty
</h3>
<p class="text-gray-600 dark:text-gray-400 mb-6">
Save items you like by clicking the heart icon on product pages.
</p>
<a href="{{ base_url }}shop/products" class="inline-block px-6 py-3 bg-primary text-white rounded-lg hover:bg-primary-dark transition-colors" style="background-color: var(--color-primary)">
Browse Products
</a>
</div>
</div>
</div>
{% endblock %}
{% block extra_scripts %}
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('shopWishlist', () => ({
...shopLayoutData(),
// Data
items: [],
loading: true,
isLoggedIn: false,
async init() {
console.log('[SHOP] Wishlist page initializing...');
// Check if user is logged in
this.isLoggedIn = await this.checkLoginStatus();
if (this.isLoggedIn) {
await this.loadWishlist();
} else {
this.loading = false;
}
},
async checkLoginStatus() {
try {
const response = await fetch('/api/v1/shop/customers/me');
return response.ok;
} catch (error) {
return false;
}
},
async loadWishlist() {
this.loading = true;
try {
console.log('[SHOP] Loading wishlist...');
const response = await fetch('/api/v1/shop/wishlist');
if (!response.ok) {
if (response.status === 401) {
this.isLoggedIn = false;
return;
}
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const data = await response.json();
console.log(`[SHOP] Loaded ${data.items?.length || 0} wishlist items`);
this.items = data.items || [];
} catch (error) {
console.error('[SHOP] Failed to load wishlist:', error);
this.showToast('Failed to load wishlist', 'error');
} finally {
this.loading = false;
}
},
async removeFromWishlist(item) {
try {
console.log('[SHOP] Removing from wishlist:', item);
const response = await fetch(`/api/v1/shop/wishlist/${item.id}`, {
method: 'DELETE'
});
if (response.ok) {
this.items = this.items.filter(i => i.id !== item.id);
this.showToast('Removed from wishlist', 'success');
} else {
throw new Error('Failed to remove from wishlist');
}
} catch (error) {
console.error('[SHOP] Failed to remove from wishlist:', error);
this.showToast('Failed to remove from wishlist', 'error');
}
},
async addToCart(product) {
console.log('[SHOP] Adding to cart:', product);
try {
const url = `/api/v1/shop/cart/${this.sessionId}/items`;
const payload = {
product_id: product.id,
quantity: 1
};
const response = await fetch(url, {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(payload)
});
if (response.ok) {
const result = await response.json();
console.log('[SHOP] Add to cart success:', result);
this.cartCount += 1;
this.showToast(`${product.marketplace_product?.title || 'Product'} added to cart`, 'success');
} else {
const error = await response.json();
console.error('[SHOP] Add to cart error:', error);
this.showToast(error.message || 'Failed to add to cart', 'error');
}
} catch (error) {
console.error('[SHOP] Add to cart exception:', error);
this.showToast('Failed to add to cart', 'error');
}
}
}));
});
</script>
{% endblock %}

Some files were not shown because too many files have changed in this diff Show More