# app/api/v1/platform/pricing.py """ Public pricing API endpoints. Provides subscription tier and add-on product information for the marketing homepage and signup flow. All endpoints are public (no authentication required). """ from fastapi import APIRouter, Depends from pydantic import BaseModel 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 models.database.subscription import TierCode router = APIRouter() # ============================================================================= # Response Schemas # ============================================================================= class TierFeature(BaseModel): """Feature included in a tier.""" code: str name: str description: str | None = None class TierResponse(BaseModel): """Subscription tier details for public display.""" code: str name: str description: str | None price_monthly: float # Price in euros price_annual: float | None # Price in euros (null for enterprise) price_monthly_cents: int price_annual_cents: int | None orders_per_month: int | None # None = unlimited products_limit: int | None # None = unlimited team_members: int | None # None = unlimited order_history_months: int | None # None = unlimited features: list[str] is_popular: bool = False # Highlight as recommended is_enterprise: bool = False # Contact sales class Config: from_attributes = True class AddOnResponse(BaseModel): """Add-on product details for public display.""" code: str name: str description: str | None category: str price: float # Price in euros price_cents: int billing_period: str quantity_unit: str | None quantity_value: int | None class Config: from_attributes = True class PricingResponse(BaseModel): """Complete pricing information.""" tiers: list[TierResponse] addons: list[AddOnResponse] trial_days: int annual_discount_months: int # e.g., 2 = "2 months free" # ============================================================================= # Feature Descriptions # ============================================================================= FEATURE_DESCRIPTIONS = { "letzshop_sync": "Letzshop Order Sync", "inventory_basic": "Basic Inventory Management", "inventory_locations": "Warehouse Locations", "inventory_purchase_orders": "Purchase Orders", "invoice_lu": "Luxembourg VAT Invoicing", "invoice_eu_vat": "EU VAT Invoicing", "invoice_bulk": "Bulk Invoicing", "customer_view": "Customer List", "customer_export": "Customer Export", "analytics_dashboard": "Analytics Dashboard", "accounting_export": "Accounting Export", "api_access": "API Access", "automation_rules": "Automation Rules", "team_roles": "Team Roles & Permissions", "white_label": "White-Label Option", "multi_vendor": "Multi-Vendor Support", "custom_integrations": "Custom Integrations", "sla_guarantee": "SLA Guarantee", "dedicated_support": "Dedicated Account Manager", } # ============================================================================= # Helper Functions # ============================================================================= def _tier_to_response(tier, is_from_db: bool = True) -> TierResponse: """Convert a tier (from DB or hardcoded) to TierResponse.""" if is_from_db: return TierResponse( code=tier.code, name=tier.name, description=tier.description, price_monthly=tier.price_monthly_cents / 100, price_annual=(tier.price_annual_cents / 100) if tier.price_annual_cents else None, price_monthly_cents=tier.price_monthly_cents, price_annual_cents=tier.price_annual_cents, orders_per_month=tier.orders_per_month, products_limit=tier.products_limit, team_members=tier.team_members, order_history_months=tier.order_history_months, features=tier.features or [], is_popular=tier.code == TierCode.PROFESSIONAL.value, is_enterprise=tier.code == TierCode.ENTERPRISE.value, ) else: # Hardcoded tier format tier_enum = tier["tier_enum"] limits = tier["limits"] return TierResponse( code=tier_enum.value, name=limits["name"], description=None, price_monthly=limits["price_monthly_cents"] / 100, price_annual=(limits["price_annual_cents"] / 100) if limits.get("price_annual_cents") else None, price_monthly_cents=limits["price_monthly_cents"], price_annual_cents=limits.get("price_annual_cents"), 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_enum == TierCode.PROFESSIONAL, is_enterprise=tier_enum == TierCode.ENTERPRISE, ) def _addon_to_response(addon) -> AddOnResponse: """Convert an AddOnProduct to AddOnResponse.""" return AddOnResponse( code=addon.code, name=addon.name, description=addon.description, category=addon.category, price=addon.price_cents / 100, price_cents=addon.price_cents, billing_period=addon.billing_period, quantity_unit=addon.quantity_unit, quantity_value=addon.quantity_value, ) # ============================================================================= # Endpoints # ============================================================================= @router.get("/tiers", response_model=list[TierResponse]) # public def get_tiers(db: Session = Depends(get_db)) -> list[TierResponse]: """ Get all public subscription tiers. Returns tiers from database if available, falls back to hardcoded TIER_LIMITS. """ # Try to get from database first db_tiers = platform_pricing_service.get_public_tiers(db) if db_tiers: return [_tier_to_response(tier, is_from_db=True) for tier in db_tiers] # Fallback to hardcoded tiers from models.database.subscription import TIER_LIMITS tiers = [] for tier_code in TIER_LIMITS: tier_data = platform_pricing_service.get_tier_from_hardcoded(tier_code.value) if tier_data: tiers.append(_tier_to_response(tier_data, is_from_db=False)) return tiers @router.get("/tiers/{tier_code}", response_model=TierResponse) # public def get_tier(tier_code: str, db: Session = Depends(get_db)) -> TierResponse: """Get a specific tier by code.""" # Try database first tier = platform_pricing_service.get_tier_by_code(db, tier_code) if tier: return _tier_to_response(tier, is_from_db=True) # Fallback to hardcoded tier_data = platform_pricing_service.get_tier_from_hardcoded(tier_code) if tier_data: return _tier_to_response(tier_data, is_from_db=False) raise ResourceNotFoundException( resource_type="SubscriptionTier", identifier=tier_code, ) @router.get("/addons", response_model=list[AddOnResponse]) # public def get_addons(db: Session = Depends(get_db)) -> list[AddOnResponse]: """ Get all available add-on products. Returns add-ons from database, or empty list if none configured. """ addons = platform_pricing_service.get_active_addons(db) return [_addon_to_response(addon) for addon in addons] @router.get("/pricing", response_model=PricingResponse) # public def get_pricing(db: Session = Depends(get_db)) -> PricingResponse: """ Get complete pricing information (tiers + add-ons). This is the main endpoint for the pricing page. """ from app.core.config import settings return PricingResponse( tiers=get_tiers(db), addons=get_addons(db), trial_days=settings.stripe_trial_days, annual_discount_months=2, # "2 months free" with annual billing )