# 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. """ from fastapi import APIRouter, Depends from pydantic import BaseModel from sqlalchemy.orm import Session from app.core.database import get_db from models.database.subscription import ( AddOnProduct, BillingPeriod, SubscriptionTier, TIER_LIMITS, 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", } # ============================================================================= # Endpoints # ============================================================================= @router.get("/tiers", response_model=list[TierResponse]) 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 = ( db.query(SubscriptionTier) .filter( SubscriptionTier.is_active == True, SubscriptionTier.is_public == True, ) .order_by(SubscriptionTier.display_order) .all() ) if db_tiers: 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, ) for tier in db_tiers ] # Fallback to hardcoded tiers tiers = [] for tier_code, limits in TIER_LIMITS.items(): tiers.append( TierResponse( code=tier_code.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_code == TierCode.PROFESSIONAL, is_enterprise=tier_code == TierCode.ENTERPRISE, ) ) return tiers @router.get("/tiers/{tier_code}", response_model=TierResponse) def get_tier(tier_code: str, db: Session = Depends(get_db)) -> TierResponse: """Get a specific tier by code.""" # Try database first tier = ( db.query(SubscriptionTier) .filter( SubscriptionTier.code == tier_code, SubscriptionTier.is_active == True, ) .first() ) if tier: 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, ) # Fallback to hardcoded try: tier_enum = TierCode(tier_code) limits = TIER_LIMITS[tier_enum] 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, ) except ValueError: from fastapi import HTTPException raise HTTPException(status_code=404, detail=f"Tier '{tier_code}' not found") @router.get("/addons", response_model=list[AddOnResponse]) 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 = ( db.query(AddOnProduct) .filter(AddOnProduct.is_active == True) .order_by(AddOnProduct.category, AddOnProduct.display_order) .all() ) 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, ) for addon in addons ] @router.get("/pricing", response_model=PricingResponse) 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 )