From fb8cb145065e771de36b68fba0a31494d8fd81f0 Mon Sep 17 00:00:00 2001 From: Samir Boulahtit Date: Mon, 2 Feb 2026 18:49:39 +0100 Subject: [PATCH] refactor: rename public routes and templates to platform Complete the public -> platform naming migration across the codebase. This aligns with the naming convention where "platform" refers to the marketing/public-facing pages of the platform itself. Changes: - Update all imports from public to platform modules - Update template references from public/ to platform/ - Update route registrations to use platform prefix - Update documentation to reflect new naming - Update test files for platform API endpoints Files affected: - app/api/main.py - router imports - app/modules/*/routes/*/platform.py - route definitions - app/modules/*/templates/*/platform/ - template files - app/modules/routes.py - route discovery - docs/* - documentation updates - tests/integration/api/v1/platform/ - test files Co-Authored-By: Claude Opus 4.5 --- app/api/main.py | 8 +- app/api/v1/platform/__init__.py | 16 +- app/api/v1/platform/signup.py | 2 +- app/modules/billing/routes/api/platform.py | 6 +- app/modules/billing/routes/pages/platform.py | 20 +- .../templates/billing/platform/pricing.html | 2 +- .../billing/platform/signup-success.html | 2 +- .../templates/billing/platform/signup.html | 14 +- app/modules/cms/routes/pages/platform.py | 20 +- .../templates/cms/platform/content-page.html | 2 +- .../cms/platform/homepage-default.html | 2 +- .../cms/platform/homepage-minimal.html | 2 +- .../cms/platform/homepage-modern.html | 2 +- .../cms/platform/homepage-wizamart.html | 4 +- app/modules/core/routes/api/platform.py | 6 +- app/modules/core/utils/__init__.py | 4 +- app/modules/core/utils/page_context.py | 4 +- app/modules/loyalty/routes/api/platform.py | 24 +- .../marketplace/routes/api/platform.py | 6 +- .../marketplace/routes/pages/platform.py | 12 +- .../marketplace/platform/find-shop.html | 4 +- app/modules/routes.py | 26 +- .../templates/tenancy/admin/login.html | 2 +- app/templates/platform/base.html | 6 +- docs/api/authentication-quick-reference.md | 2 +- docs/api/authentication.md | 6 +- docs/api/rbac.md | 2 +- docs/api/storefront-api-reference.md | 4 +- .../api-consolidation-proposal.md | 62 +- docs/architecture/api-migration-status.md | 58 +- docs/architecture/frontend-structure.md | 2 +- docs/deployment/stripe-integration.md | 6 +- docs/development/naming-conventions.md | 2 +- .../platform-marketing-homepage.md | 38 +- docs/proposals/plan-perms.md | 653 ++++++++++++++++++ docs/testing/test-structure.md | 8 +- main.py | 18 +- scripts/test_auth_complete.py | 2 +- tests/integration/api/v1/README.md | 2 +- tests/integration/api/v1/platform/README.md | 6 +- tests/integration/api/v1/platform/__init__.py | 12 +- .../api/v1/platform/test_letzshop_vendors.py | 60 +- .../api/v1/platform/test_pricing.py | 48 +- .../api/v1/platform/test_signup.py | 120 ++-- 44 files changed, 980 insertions(+), 327 deletions(-) create mode 100644 docs/proposals/plan-perms.md diff --git a/app/api/main.py b/app/api/main.py index 438a066f..41bd7708 100644 --- a/app/api/main.py +++ b/app/api/main.py @@ -10,7 +10,7 @@ This module provides: from fastapi import APIRouter -from app.api.v1 import admin, public, storefront, vendor, webhooks +from app.api.v1 import admin, platform, storefront, vendor, webhooks api_router = APIRouter() @@ -37,12 +37,12 @@ api_router.include_router(vendor.router, prefix="/v1/vendor", tags=["vendor"]) api_router.include_router(storefront.router, prefix="/v1/storefront", tags=["storefront"]) # ============================================================================ -# PUBLIC ROUTES (Unauthenticated endpoints) -# Prefix: /api/v1/public +# PLATFORM ROUTES (Unauthenticated endpoints) +# Prefix: /api/v1/platform # Includes: /signup, /pricing, /letzshop-vendors, /language # ============================================================================ -api_router.include_router(public.router, prefix="/v1/public", tags=["public"]) +api_router.include_router(platform.router, prefix="/v1/platform", tags=["platform"]) # ============================================================================ # WEBHOOK ROUTES (External service callbacks via auto-discovery) diff --git a/app/api/v1/platform/__init__.py b/app/api/v1/platform/__init__.py index 9a5850a8..be41bdc4 100644 --- a/app/api/v1/platform/__init__.py +++ b/app/api/v1/platform/__init__.py @@ -1,11 +1,11 @@ -# app/api/v1/public/__init__.py +# app/api/v1/platform/__init__.py """ -Public API endpoints (no authentication required). +Platform API endpoints (no authentication required). Includes: - signup: /signup/* (multi-step signup flow - cross-cutting) -Auto-discovers and aggregates public routes from self-contained modules: +Auto-discovers and aggregates platform routes from self-contained modules: - billing: /pricing/* (subscription tiers and add-ons) - marketplace: /letzshop-vendors/* (vendor lookup for signup) - core: /language/* (language preferences) @@ -15,16 +15,16 @@ 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 +from app.api.v1.platform import signup +from app.modules.routes import get_platform_api_routes router = APIRouter() # Cross-cutting signup flow (spans auth, vendors, billing, payments) -router.include_router(signup.router, tags=["public-signup"]) +router.include_router(signup.router, tags=["platform-signup"]) -# Auto-discover public routes from modules -for route_info in get_public_api_routes(): +# Auto-discover platform routes from modules +for route_info in get_platform_api_routes(): if route_info.custom_prefix: router.include_router( route_info.router, diff --git a/app/api/v1/platform/signup.py b/app/api/v1/platform/signup.py index ea0385e6..a1a021ea 100644 --- a/app/api/v1/platform/signup.py +++ b/app/api/v1/platform/signup.py @@ -1,4 +1,4 @@ -# app/api/v1/public/signup.py +# app/api/v1/platform/signup.py """ Platform signup API endpoints. diff --git a/app/modules/billing/routes/api/platform.py b/app/modules/billing/routes/api/platform.py index 6f1370e0..ab7123c8 100644 --- a/app/modules/billing/routes/api/platform.py +++ b/app/modules/billing/routes/api/platform.py @@ -1,11 +1,11 @@ -# app/modules/billing/routes/api/public.py +# app/modules/billing/routes/api/platform.py """ -Public pricing API endpoints. +Platform 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). +All endpoints are unauthenticated (no authentication required). """ from fastapi import APIRouter, Depends diff --git a/app/modules/billing/routes/pages/platform.py b/app/modules/billing/routes/pages/platform.py index a6659df8..1866ca45 100644 --- a/app/modules/billing/routes/pages/platform.py +++ b/app/modules/billing/routes/pages/platform.py @@ -1,8 +1,8 @@ -# app/modules/billing/routes/pages/public.py +# app/modules/billing/routes/pages/platform.py """ -Billing Public Page Routes (HTML rendering). +Billing Platform Page Routes (HTML rendering). -Public (unauthenticated) pages for pricing and signup: +Platform (unauthenticated) pages for pricing and signup: - Pricing page - Signup wizard - Signup success @@ -14,7 +14,7 @@ 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.modules.core.utils.page_context import get_platform_context from app.templates_config import templates router = APIRouter() @@ -57,12 +57,12 @@ async def pricing_page( """ Standalone pricing page with detailed tier comparison. """ - context = get_public_context(request, db) + context = get_platform_context(request, db) context["tiers"] = _get_tiers_data() context["page_title"] = "Pricing" return templates.TemplateResponse( - "billing/public/pricing.html", + "billing/platform/pricing.html", context, ) @@ -86,14 +86,14 @@ async def signup_page( - tier: Pre-selected tier code - annual: Pre-select annual billing """ - context = get_public_context(request, db) + context = get_platform_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", + "billing/platform/signup.html", context, ) @@ -111,11 +111,11 @@ async def signup_success_page( Shown after successful account creation. """ - context = get_public_context(request, db) + context = get_platform_context(request, db) context["page_title"] = "Welcome to Wizamart!" context["vendor_code"] = vendor_code return templates.TemplateResponse( - "billing/public/signup-success.html", + "billing/platform/signup-success.html", context, ) diff --git a/app/modules/billing/templates/billing/platform/pricing.html b/app/modules/billing/templates/billing/platform/pricing.html index 0d94c005..99c963db 100644 --- a/app/modules/billing/templates/billing/platform/pricing.html +++ b/app/modules/billing/templates/billing/platform/pricing.html @@ -1,6 +1,6 @@ {# app/templates/platform/pricing.html #} {# Standalone Pricing Page #} -{% extends "public/base.html" %} +{% extends "platform/base.html" %} {% block title %}{{ _("cms.platform.pricing.title") }} - Wizamart{% endblock %} diff --git a/app/modules/billing/templates/billing/platform/signup-success.html b/app/modules/billing/templates/billing/platform/signup-success.html index cf2110b0..5bef07bb 100644 --- a/app/modules/billing/templates/billing/platform/signup-success.html +++ b/app/modules/billing/templates/billing/platform/signup-success.html @@ -1,6 +1,6 @@ {# app/templates/platform/signup-success.html #} {# Signup Success Page #} -{% extends "public/base.html" %} +{% extends "platform/base.html" %} {% block title %}{{ _("cms.platform.success.title") }}{% endblock %} diff --git a/app/modules/billing/templates/billing/platform/signup.html b/app/modules/billing/templates/billing/platform/signup.html index f7f47fff..e79de3e1 100644 --- a/app/modules/billing/templates/billing/platform/signup.html +++ b/app/modules/billing/templates/billing/platform/signup.html @@ -1,6 +1,6 @@ {# app/templates/platform/signup.html #} {# Multi-step Signup Wizard #} -{% extends "public/base.html" %} +{% extends "platform/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/public/signup/start', { + const response = await fetch('/api/v1/platform/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/public/letzshop-vendors/lookup', { + const lookupResponse = await fetch('/api/v1/platform/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/public/signup/claim-vendor', { + const claimResponse = await fetch('/api/v1/platform/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/public/signup/create-account', { + const response = await fetch('/api/v1/platform/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/public/signup/setup-payment', { + const response = await fetch('/api/v1/platform/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/public/signup/complete', { + const response = await fetch('/api/v1/platform/signup/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ diff --git a/app/modules/cms/routes/pages/platform.py b/app/modules/cms/routes/pages/platform.py index e6b5e079..903d1580 100644 --- a/app/modules/cms/routes/pages/platform.py +++ b/app/modules/cms/routes/pages/platform.py @@ -1,8 +1,8 @@ -# app/modules/cms/routes/pages/public.py +# app/modules/cms/routes/pages/platform.py """ -CMS Public Page Routes (HTML rendering). +CMS Platform Page Routes (HTML rendering). -Public (unauthenticated) pages for platform content: +Platform (unauthenticated) pages for platform content: - Homepage - Generic content pages (/{slug} catch-all) """ @@ -16,7 +16,7 @@ from sqlalchemy.orm import Session from app.core.database import get_db from app.modules.billing.models import TIER_LIMITS, TierCode from app.modules.cms.services import content_page_service -from app.modules.core.utils.page_context import get_public_context +from app.modules.core.utils.page_context import get_platform_context from app.templates_config import templates logger = logging.getLogger(__name__) @@ -147,19 +147,19 @@ async def homepage( if cms_homepage: # Use CMS-based homepage with template selection - context = get_public_context(request, db) + context = get_platform_context(request, db) context["page"] = cms_homepage context["tiers"] = _get_tiers_data() template_name = cms_homepage.template or "default" - template_path = f"cms/public/homepage-{template_name}.html" + template_path = f"cms/platform/homepage-{template_name}.html" logger.info(f"[HOMEPAGE] Rendering CMS homepage with template: {template_path}") return templates.TemplateResponse(template_path, context) # Fallback: Default wizamart homepage (no CMS content) logger.info("[HOMEPAGE] No CMS homepage found, using default wizamart template") - context = get_public_context(request, db) + context = get_platform_context(request, db) context["tiers"] = _get_tiers_data() # Add-ons (hardcoded for now, will come from DB) @@ -196,7 +196,7 @@ async def homepage( ] return templates.TemplateResponse( - "cms/public/homepage-wizamart.html", + "cms/platform/homepage-wizamart.html", context, ) @@ -231,11 +231,11 @@ async def content_page( if not page: raise HTTPException(status_code=404, detail=f"Page not found: {slug}") - context = get_public_context(request, db) + context = get_platform_context(request, db) context["page"] = page context["page_title"] = page.title return templates.TemplateResponse( - "cms/public/content-page.html", + "cms/platform/content-page.html", context, ) diff --git a/app/modules/cms/templates/cms/platform/content-page.html b/app/modules/cms/templates/cms/platform/content-page.html index 6a785c03..b4abe57b 100644 --- a/app/modules/cms/templates/cms/platform/content-page.html +++ b/app/modules/cms/templates/cms/platform/content-page.html @@ -1,6 +1,6 @@ {# app/templates/platform/content-page.html #} {# Generic template for platform content pages (About, FAQ, Terms, Contact, etc.) #} -{% extends "public/base.html" %} +{% extends "platform/base.html" %} {% block title %}{{ page.title }} - Marketplace{% endblock %} diff --git a/app/modules/cms/templates/cms/platform/homepage-default.html b/app/modules/cms/templates/cms/platform/homepage-default.html index 2a48cd21..7ed85a63 100644 --- a/app/modules/cms/templates/cms/platform/homepage-default.html +++ b/app/modules/cms/templates/cms/platform/homepage-default.html @@ -1,6 +1,6 @@ {# app/templates/platform/homepage-default.html #} {# Default platform homepage template with section-based rendering #} -{% extends "public/base.html" %} +{% extends "platform/base.html" %} {# Import section partials #} {% from 'platform/sections/_hero.html' import render_hero %} diff --git a/app/modules/cms/templates/cms/platform/homepage-minimal.html b/app/modules/cms/templates/cms/platform/homepage-minimal.html index 5ded043f..a8e75d0e 100644 --- a/app/modules/cms/templates/cms/platform/homepage-minimal.html +++ b/app/modules/cms/templates/cms/platform/homepage-minimal.html @@ -1,6 +1,6 @@ {# app/templates/platform/homepage-minimal.html #} {# Minimal/clean platform homepage template #} -{% extends "public/base.html" %} +{% extends "platform/base.html" %} {% block title %} {% if page %}{{ page.title }}{% else %}Home{% endif %} - Marketplace diff --git a/app/modules/cms/templates/cms/platform/homepage-modern.html b/app/modules/cms/templates/cms/platform/homepage-modern.html index 9e0bb9c9..f9804930 100644 --- a/app/modules/cms/templates/cms/platform/homepage-modern.html +++ b/app/modules/cms/templates/cms/platform/homepage-modern.html @@ -1,6 +1,6 @@ {# app/templates/platform/homepage-modern.html #} {# Wizamart OMS - Luxembourg-focused homepage inspired by Veeqo #} -{% extends "public/base.html" %} +{% extends "platform/base.html" %} {% block title %} Wizamart - The Back-Office for Letzshop Sellers diff --git a/app/modules/cms/templates/cms/platform/homepage-wizamart.html b/app/modules/cms/templates/cms/platform/homepage-wizamart.html index 1a5075da..c73eb4c5 100644 --- a/app/modules/cms/templates/cms/platform/homepage-wizamart.html +++ b/app/modules/cms/templates/cms/platform/homepage-wizamart.html @@ -1,6 +1,6 @@ {# app/templates/platform/homepage-wizamart.html #} {# Wizamart Marketing Homepage - Letzshop OMS Platform #} -{% extends "public/base.html" %} +{% extends "platform/base.html" %} {% from 'shared/macros/inputs.html' import toggle_switch %} {% block title %}Wizamart - Order Management for Letzshop Sellers{% endblock %} @@ -407,7 +407,7 @@ function homepageData() { this.vendorResult = null; try { - const response = await fetch('/api/v1/public/letzshop-vendors/lookup', { + const response = await fetch('/api/v1/platform/letzshop-vendors/lookup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: this.shopUrl }) diff --git a/app/modules/core/routes/api/platform.py b/app/modules/core/routes/api/platform.py index a7bb2a41..10d06190 100644 --- a/app/modules/core/routes/api/platform.py +++ b/app/modules/core/routes/api/platform.py @@ -1,13 +1,13 @@ -# app/modules/core/routes/api/public.py +# app/modules/core/routes/api/platform.py """ -Public language API endpoints. +Platform language API endpoints. Handles: - Setting language preference via cookie - Getting current language info - Listing available languages -All endpoints are public (no authentication required). +All endpoints are unauthenticated (no authentication required). """ import logging diff --git a/app/modules/core/utils/__init__.py b/app/modules/core/utils/__init__.py index d27fae7e..89860b0b 100644 --- a/app/modules/core/utils/__init__.py +++ b/app/modules/core/utils/__init__.py @@ -5,12 +5,12 @@ from .page_context import ( get_admin_context, get_vendor_context, get_storefront_context, - get_public_context, + get_platform_context, ) __all__ = [ "get_admin_context", "get_vendor_context", "get_storefront_context", - "get_public_context", + "get_platform_context", ] diff --git a/app/modules/core/utils/page_context.py b/app/modules/core/utils/page_context.py index cb6f07fd..7d67f06d 100644 --- a/app/modules/core/utils/page_context.py +++ b/app/modules/core/utils/page_context.py @@ -253,13 +253,13 @@ def get_storefront_context( return context -def get_public_context( +def get_platform_context( request: Request, db: Session, **extra_context, ) -> dict: """ - Build context for public/marketing pages. + Build context for platform/marketing pages. Includes platform info, i18n globals, and CMS navigation pages. diff --git a/app/modules/loyalty/routes/api/platform.py b/app/modules/loyalty/routes/api/platform.py index a5294aeb..bd5319e8 100644 --- a/app/modules/loyalty/routes/api/platform.py +++ b/app/modules/loyalty/routes/api/platform.py @@ -1,8 +1,8 @@ -# app/modules/loyalty/routes/api/public.py +# app/modules/loyalty/routes/api/platform.py """ -Loyalty module public routes. +Loyalty module platform routes. -Public endpoints for: +Platform endpoints for: - Customer enrollment (by vendor code) - Apple Wallet pass download - Apple Web Service endpoints for device registration/updates @@ -29,8 +29,8 @@ from app.modules.loyalty.services import ( logger = logging.getLogger(__name__) -# Public router (no auth required for some endpoints) -public_router = APIRouter(prefix="/loyalty") +# Platform router (no auth required for some endpoints) +platform_router = APIRouter(prefix="/loyalty") # ============================================================================= @@ -38,7 +38,7 @@ public_router = APIRouter(prefix="/loyalty") # ============================================================================= -@public_router.get("/programs/{vendor_code}") +@platform_router.get("/programs/{vendor_code}") def get_program_by_vendor_code( vendor_code: str = Path(..., min_length=1, max_length=50), db: Session = Depends(get_db), @@ -85,7 +85,7 @@ def get_program_by_vendor_code( # ============================================================================= -@public_router.get("/passes/apple/{serial_number}.pkpass") +@platform_router.get("/passes/apple/{serial_number}.pkpass") def download_apple_pass( serial_number: str = Path(...), db: Session = Depends(get_db), @@ -122,7 +122,7 @@ def download_apple_pass( # ============================================================================= -@public_router.post("/apple/v1/devices/{device_id}/registrations/{pass_type_id}/{serial_number}") +@platform_router.post("/apple/v1/devices/{device_id}/registrations/{pass_type_id}/{serial_number}") def register_device( device_id: str = Path(...), pass_type_id: str = Path(...), @@ -165,7 +165,7 @@ def register_device( raise HTTPException(status_code=500) -@public_router.delete("/apple/v1/devices/{device_id}/registrations/{pass_type_id}/{serial_number}") +@platform_router.delete("/apple/v1/devices/{device_id}/registrations/{pass_type_id}/{serial_number}") def unregister_device( device_id: str = Path(...), pass_type_id: str = Path(...), @@ -205,7 +205,7 @@ def unregister_device( raise HTTPException(status_code=500) -@public_router.get("/apple/v1/devices/{device_id}/registrations/{pass_type_id}") +@platform_router.get("/apple/v1/devices/{device_id}/registrations/{pass_type_id}") def get_serial_numbers( device_id: str = Path(...), pass_type_id: str = Path(...), @@ -256,7 +256,7 @@ def get_serial_numbers( } -@public_router.get("/apple/v1/passes/{pass_type_id}/{serial_number}") +@platform_router.get("/apple/v1/passes/{pass_type_id}/{serial_number}") def get_latest_pass( pass_type_id: str = Path(...), serial_number: str = Path(...), @@ -302,7 +302,7 @@ def get_latest_pass( ) -@public_router.post("/apple/v1/log") +@platform_router.post("/apple/v1/log") def log_errors(): """ Receive error logs from Apple. diff --git a/app/modules/marketplace/routes/api/platform.py b/app/modules/marketplace/routes/api/platform.py index 729b42a5..01a10674 100644 --- a/app/modules/marketplace/routes/api/platform.py +++ b/app/modules/marketplace/routes/api/platform.py @@ -1,11 +1,11 @@ -# app/modules/marketplace/routes/api/public.py +# app/modules/marketplace/routes/api/platform.py """ -Public Letzshop vendor lookup API endpoints. +Platform Letzshop vendor lookup API endpoints. Allows potential vendors to find themselves in the Letzshop marketplace and claim their shop during signup. -All endpoints are public (no authentication required). +All endpoints are unauthenticated (no authentication required). """ import logging diff --git a/app/modules/marketplace/routes/pages/platform.py b/app/modules/marketplace/routes/pages/platform.py index 4d18fcc6..9da249a4 100644 --- a/app/modules/marketplace/routes/pages/platform.py +++ b/app/modules/marketplace/routes/pages/platform.py @@ -1,8 +1,8 @@ -# app/modules/marketplace/routes/pages/public.py +# app/modules/marketplace/routes/pages/platform.py """ -Marketplace Public Page Routes (HTML rendering). +Marketplace Platform Page Routes (HTML rendering). -Public (unauthenticated) pages: +Platform (unauthenticated) pages: - Find shop (Letzshop vendor browser) """ @@ -11,7 +11,7 @@ from fastapi.responses import HTMLResponse from sqlalchemy.orm import Session from app.core.database import get_db -from app.modules.core.utils.page_context import get_public_context +from app.modules.core.utils.page_context import get_platform_context from app.templates_config import templates router = APIRouter() @@ -32,10 +32,10 @@ async def find_shop_page( Allows vendors to search for and claim their Letzshop shop. """ - context = get_public_context(request, db) + context = get_platform_context(request, db) context["page_title"] = "Find Your Letzshop Shop" return templates.TemplateResponse( - "marketplace/public/find-shop.html", + "marketplace/platform/find-shop.html", context, ) diff --git a/app/modules/marketplace/templates/marketplace/platform/find-shop.html b/app/modules/marketplace/templates/marketplace/platform/find-shop.html index e263e737..4b3f1bc4 100644 --- a/app/modules/marketplace/templates/marketplace/platform/find-shop.html +++ b/app/modules/marketplace/templates/marketplace/platform/find-shop.html @@ -1,6 +1,6 @@ {# app/modules/marketplace/templates/marketplace/public/find-shop.html #} {# Letzshop Vendor Finder Page #} -{% extends "public/base.html" %} +{% extends "platform/base.html" %} {% block title %}{{ _("cms.platform.find_shop.title") }} - Wizamart{% endblock %} @@ -151,7 +151,7 @@ function vendorFinderData() { this.result = null; try { - const response = await fetch('/api/v1/public/letzshop-vendors/lookup', { + const response = await fetch('/api/v1/platform/letzshop-vendors/lookup', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ url: this.searchQuery }) diff --git a/app/modules/routes.py b/app/modules/routes.py index 681d3e4f..fbd9e15a 100644 --- a/app/modules/routes.py +++ b/app/modules/routes.py @@ -183,9 +183,9 @@ def _discover_routes_in_dir( "pages_prefix": "/storefront", "include_in_schema": True if route_type == "api" else False, }, - "public": { - "api_prefix": "/api/v1/public", - "pages_prefix": "/public", + "platform": { + "api_prefix": "/api/v1/platform", + "pages_prefix": "/platform", "include_in_schema": True, }, "webhooks": { @@ -335,16 +335,16 @@ def get_storefront_api_routes() -> list[RouteInfo]: return sorted(routes, key=lambda r: r.priority) -def get_public_api_routes() -> list[RouteInfo]: +def get_platform_api_routes() -> list[RouteInfo]: """ - Get public API routes from modules, sorted by priority. + Get platform API routes from modules, sorted by priority. - Public routes are unauthenticated endpoints for marketing pages, + Platform routes are unauthenticated endpoints for marketing pages, pricing info, and other public-facing features. """ routes = [ r for r in discover_module_routes() - if r.route_type == "api" and r.frontend == "public" + if r.route_type == "api" and r.frontend == "platform" ] return sorted(routes, key=lambda r: r.priority) @@ -363,11 +363,11 @@ def get_webhooks_api_routes() -> list[RouteInfo]: return sorted(routes, key=lambda r: r.priority) -def get_public_page_routes() -> list[RouteInfo]: +def get_platform_page_routes() -> list[RouteInfo]: """ - Get public (marketing) page routes from modules, sorted by priority. + Get platform (marketing) page routes from modules, sorted by priority. - Public pages are unauthenticated marketing pages like: + Platform pages are unauthenticated marketing pages like: - Homepage (/) - Pricing (/pricing) - Signup (/signup) @@ -379,7 +379,7 @@ def get_public_page_routes() -> list[RouteInfo]: """ routes = [ r for r in discover_module_routes() - if r.route_type == "pages" and r.frontend == "public" + if r.route_type == "pages" and r.frontend == "platform" ] return sorted(routes, key=lambda r: r.priority) @@ -414,8 +414,8 @@ __all__ = [ "get_admin_api_routes", "get_vendor_api_routes", "get_storefront_api_routes", - "get_public_api_routes", + "get_platform_api_routes", "get_webhooks_api_routes", - "get_public_page_routes", + "get_platform_page_routes", "get_storefront_page_routes", ] diff --git a/app/modules/tenancy/templates/tenancy/admin/login.html b/app/modules/tenancy/templates/tenancy/admin/login.html index 146d8f99..08c999f1 100644 --- a/app/modules/tenancy/templates/tenancy/admin/login.html +++ b/app/modules/tenancy/templates/tenancy/admin/login.html @@ -135,6 +135,6 @@ - + \ No newline at end of file diff --git a/app/templates/platform/base.html b/app/templates/platform/base.html index fe27f16d..a7ef82ac 100644 --- a/app/templates/platform/base.html +++ b/app/templates/platform/base.html @@ -1,5 +1,5 @@ -{# app/templates/public/base.html #} -{# Base template for public pages (homepage, about, faq, pricing, signup, etc.) #} +{# app/templates/platform/base.html #} +{# Base template for platform pages (homepage, about, faq, pricing, signup, etc.) #} @@ -42,7 +42,7 @@ {# Tailwind CSS v4 (built locally via standalone CLI) #} - + {# Flag icons for language selector #} diff --git a/docs/api/authentication-quick-reference.md b/docs/api/authentication-quick-reference.md index 50bd8399..e30899ae 100644 --- a/docs/api/authentication-quick-reference.md +++ b/docs/api/authentication-quick-reference.md @@ -93,7 +93,7 @@ POST /api/v1/vendor/auth/login Body: {"username": "...", "password": "..."} # Customer -POST /api/v1/public/vendors/{vendor_id}/customers/login +POST /api/v1/platform/vendors/{vendor_id}/customers/login Body: {"username": "...", "password": "..."} ``` diff --git a/docs/api/authentication.md b/docs/api/authentication.md index b6b341c9..8184cf68 100644 --- a/docs/api/authentication.md +++ b/docs/api/authentication.md @@ -229,12 +229,12 @@ In path-based development mode, the full URL includes the vendor code (e.g., `/v **Login Endpoint:** ``` -POST /api/v1/public/vendors/{vendor_id}/customers/login +POST /api/v1/platform/vendors/{vendor_id}/customers/login ``` **Example Request:** ```bash -curl -X POST http://localhost:8000/api/v1/public/vendors/1/customers/login \ +curl -X POST http://localhost:8000/api/v1/platform/vendors/1/customers/login \ -H "Content-Type: application/json" \ -d '{"username":"customer","password":"customer123"}' ``` @@ -950,7 +950,7 @@ curl http://localhost:8000/api/v1/admin/vendors \ ```bash # Login -curl -X POST http://localhost:8000/api/v1/public/vendors/1/customers/login \ +curl -X POST http://localhost:8000/api/v1/platform/vendors/1/customers/login \ -H "Content-Type: application/json" \ -d '{"username":"customer","password":"customer123"}' diff --git a/docs/api/rbac.md b/docs/api/rbac.md index 5268cea9..5951edf7 100644 --- a/docs/api/rbac.md +++ b/docs/api/rbac.md @@ -748,7 +748,7 @@ role = Role( │ Client │ └──────┬──────┘ │ - │ POST /api/v1/public/vendors/{id}/customers/login + │ POST /api/v1/platform/vendors/{id}/customers/login │ { username, password } ▼ ┌─────────────────────────────┐ diff --git a/docs/api/storefront-api-reference.md b/docs/api/storefront-api-reference.md index b919d66a..87194304 100644 --- a/docs/api/storefront-api-reference.md +++ b/docs/api/storefront-api-reference.md @@ -814,8 +814,8 @@ X-RateLimit-Reset: 1700000000 **Old Pattern (Deprecated):** ```http -GET /api/v1/public/vendors/{vendor_id}/products -POST /api/v1/public/vendors/auth/{vendor_id}/customers/login +GET /api/v1/platform/vendors/{vendor_id}/products +POST /api/v1/platform/vendors/auth/{vendor_id}/customers/login ``` **New Pattern (Current):** diff --git a/docs/architecture/api-consolidation-proposal.md b/docs/architecture/api-consolidation-proposal.md index d449a3c4..9240eb29 100644 --- a/docs/architecture/api-consolidation-proposal.md +++ b/docs/architecture/api-consolidation-proposal.md @@ -7,7 +7,7 @@ ## Executive Summary The platform currently has **two parallel API structures** for shop/customer-facing endpoints: -1. **Original:** `/api/v1/public/vendors/{vendor_id}/*` +1. **Original:** `/api/v1/platform/vendors/{vendor_id}/*` 2. **New:** `/api/v1/shop/*` This divergence creates confusion, maintenance overhead, and potential bugs. This document analyzes the situation and proposes a consolidation strategy. @@ -16,19 +16,19 @@ This divergence creates confusion, maintenance overhead, and potential bugs. Thi ## Current State Analysis -### 1. Original Architecture (`/api/v1/public/vendors/`) +### 1. Original Architecture (`/api/v1/platform/vendors/`) -**Location:** `app/api/v1/public/vendors/` +**Location:** `app/api/v1/platform/vendors/` **Endpoints:** ``` -GET /api/v1/public/vendors → List active vendors -GET /api/v1/public/vendors/{vendor_id}/products → Product catalog -GET /api/v1/public/vendors/{vendor_id}/products/{product_id} → Product detail -POST /api/v1/public/vendors/{vendor_id}/cart → Cart operations -GET /api/v1/public/vendors/{vendor_id}/orders → Customer orders -POST /api/v1/public/vendors/auth/login → Customer authentication -POST /api/v1/public/vendors/auth/register → Customer registration +GET /api/v1/platform/vendors → List active vendors +GET /api/v1/platform/vendors/{vendor_id}/products → Product catalog +GET /api/v1/platform/vendors/{vendor_id}/products/{product_id} → Product detail +POST /api/v1/platform/vendors/{vendor_id}/cart → Cart operations +GET /api/v1/platform/vendors/{vendor_id}/orders → Customer orders +POST /api/v1/platform/vendors/auth/login → Customer authentication +POST /api/v1/platform/vendors/auth/register → Customer registration ``` **Characteristics:** @@ -60,7 +60,7 @@ GET /api/v1/shop/content-pages/{slug} → CMS page content **Characteristics:** - ✅ **Vendor-agnostic URLs:** Clean paths without vendor_id - ✅ **Middleware-driven:** Relies on `VendorContextMiddleware` to inject vendor -- ✅ **Simpler URLs:** `/api/v1/shop/products` vs `/api/v1/public/vendors/123/products` +- ✅ **Simpler URLs:** `/api/v1/shop/products` vs `/api/v1/platform/vendors/123/products` - ❌ **Incomplete:** Only CMS endpoints implemented - ❌ **Divergent:** Not consistent with existing public API @@ -80,7 +80,7 @@ GET /api/v1/shop/content-pages/{slug} → CMS page content fetch('/api/v1/shop/content-pages/about') // Products use old pattern -fetch('/api/v1/public/vendors/123/products') +fetch('/api/v1/platform/vendors/123/products') ``` ### Confusion @@ -143,7 +143,7 @@ Developers must remember: **Implementation:** - Vendor extracted by `VendorContextMiddleware` from request - All endpoints use `request.state.vendor` instead of path parameter -- URLs are cleaner: `/api/v1/shop/products` instead of `/api/v1/public/vendors/123/products` +- URLs are cleaner: `/api/v1/shop/products` instead of `/api/v1/platform/vendors/123/products` **Pros:** - ✅ Clean, consistent API structure @@ -162,18 +162,18 @@ Developers must remember: --- -### Option 2: Keep `/api/v1/public/vendors/*` and Deprecate `/api/v1/shop/*` +### Option 2: Keep `/api/v1/platform/vendors/*` and Deprecate `/api/v1/shop/*` -**Approach:** Move CMS endpoints to `/api/v1/public/vendors/{vendor_id}/content-pages/*` +**Approach:** Move CMS endpoints to `/api/v1/platform/vendors/{vendor_id}/content-pages/*` **Proposed Changes:** ``` # Move CMS endpoints FROM: /api/v1/shop/content-pages/navigation -TO: /api/v1/public/vendors/{vendor_id}/content-pages/navigation +TO: /api/v1/platform/vendors/{vendor_id}/content-pages/navigation FROM: /api/v1/shop/content-pages/{slug} -TO: /api/v1/public/vendors/{vendor_id}/content-pages/{slug} +TO: /api/v1/platform/vendors/{vendor_id}/content-pages/{slug} ``` **Pros:** @@ -240,7 +240,7 @@ async def get_products_legacy(vendor_id: int, db: Session = Depends(get_db)): fetch('/api/v1/shop/products') // ❌ BAD - fetch('/api/v1/public/vendors/123/products') + fetch('/api/v1/platform/vendors/123/products') ``` 3. **Multi-Tenant Best Practice**: Vendor context should be implicit (from domain/path), not explicit in every API call. @@ -258,7 +258,7 @@ async def get_products_legacy(vendor_id: int, db: Session = Depends(get_db)): **Day 1-2: Move Products** ```bash # Copy and adapt -app/api/v1/public/vendors/products.py → app/api/v1/shop/products.py +app/api/v1/platform/vendors/products.py → app/api/v1/shop/products.py # Changes: - Remove vendor_id path parameter @@ -268,24 +268,24 @@ app/api/v1/public/vendors/products.py → app/api/v1/shop/products.py **Day 3: Move Cart** ```bash -app/api/v1/public/vendors/cart.py → app/api/v1/shop/cart.py +app/api/v1/platform/vendors/cart.py → app/api/v1/shop/cart.py ``` **Day 4: Move Orders** ```bash -app/api/v1/public/vendors/orders.py → app/api/v1/shop/orders.py +app/api/v1/platform/vendors/orders.py → app/api/v1/shop/orders.py ``` **Day 5: Move Auth** ```bash -app/api/v1/public/vendors/auth.py → app/api/v1/shop/auth.py +app/api/v1/platform/vendors/auth.py → app/api/v1/shop/auth.py ``` ### Phase 2: Update Frontend (Week 1) **Templates:** - Update all `fetch()` calls in shop templates -- Change from `/api/v1/public/vendors/${vendorId}/...` to `/api/v1/shop/...` +- Change from `/api/v1/platform/vendors/${vendorId}/...` to `/api/v1/shop/...` **JavaScript:** - Update any shop-related API client code @@ -316,10 +316,10 @@ app/api/v1/public/vendors/auth.py → app/api/v1/shop/auth.py ## Code Examples -### Before (Current - `/api/v1/public/vendors`) +### Before (Current - `/api/v1/platform/vendors`) ```python -# app/api/v1/public/vendors/products.py +# app/api/v1/platform/vendors/products.py @router.get("/{vendor_id}/products") def get_public_product_catalog( vendor_id: int = Path(...), @@ -332,7 +332,7 @@ def get_public_product_catalog( ```javascript // Frontend const vendorId = 123; -fetch(`/api/v1/public/vendors/${vendorId}/products`) +fetch(`/api/v1/platform/vendors/${vendorId}/products`) ``` ### After (Proposed - `/api/v1/shop`) @@ -358,14 +358,14 @@ fetch('/api/v1/shop/products') // Vendor context automatic ## Impact Assessment ### Breaking Changes -- All frontend code calling `/api/v1/public/vendors/*` must update +- All frontend code calling `/api/v1/platform/vendors/*` must update - Mobile apps (if any) must update - Third-party integrations (if any) must update ### Non-Breaking - Admin APIs: `/api/v1/admin/*` → No changes - Vendor APIs: `/api/v1/vendor/*` → No changes -- Vendor listing: Keep `/api/v1/public/vendors` (list all vendors for marketplace) +- Vendor listing: Keep `/api/v1/platform/vendors` (list all vendors for marketplace) ### Risk Mitigation 1. **Deprecation Period**: Keep old endpoints for 2-4 weeks @@ -383,7 +383,7 @@ If full migration is not approved immediately, we can do a **minimal fix** for t ```python # Move: app/api/v1/shop/content_pages.py -# To: app/api/v1/public/vendors/content_pages.py +# To: app/api/v1/platform/vendors/content_pages.py # Update routes: @router.get("/{vendor_id}/content-pages/navigation") @@ -402,7 +402,7 @@ If full migration is not approved immediately, we can do a **minimal fix** for t Should we: 1. ✅ **Consolidate to `/api/v1/shop/*`** (Recommended) -2. ❌ **Keep `/api/v1/public/vendors/*`** and move CMS there +2. ❌ **Keep `/api/v1/platform/vendors/*`** and move CMS there 3. ❌ **Hybrid approach** with both patterns 4. ❌ **Quick fix only** - move CMS, address later @@ -412,7 +412,7 @@ Should we: ## Appendix: Current Endpoint Inventory -### `/api/v1/public/vendors/*` +### `/api/v1/platform/vendors/*` - ✅ `vendors.py` - Vendor listing - ✅ `auth.py` - Customer authentication - ✅ `products.py` - Product catalog diff --git a/docs/architecture/api-migration-status.md b/docs/architecture/api-migration-status.md index 9f9cf924..7cec88aa 100644 --- a/docs/architecture/api-migration-status.md +++ b/docs/architecture/api-migration-status.md @@ -174,15 +174,15 @@ Updated all shop templates to use new API endpoints: | Template | Old Endpoint | New Endpoint | Status | |----------|-------------|--------------|---------| -| `shop/account/login.html` | `/api/v1/public/vendors/${id}/customers/login` | `/api/v1/shop/auth/login` | ✅ Complete | -| `shop/account/register.html` | `/api/v1/public/vendors/${id}/customers/register` | `/api/v1/shop/auth/register` | ✅ Complete | -| `shop/product.html` | `/api/v1/public/vendors/${id}/products/${pid}` | `/api/v1/shop/products/${pid}` | ✅ Complete | -| `shop/product.html` | `/api/v1/public/vendors/${id}/products?limit=4` | `/api/v1/shop/products?limit=4` | ✅ Complete | -| `shop/product.html` | `/api/v1/public/vendors/${id}/cart/${sid}` | `/api/v1/shop/cart/${sid}` | ✅ Complete | -| `shop/product.html` | `/api/v1/public/vendors/${id}/cart/${sid}/items` | `/api/v1/shop/cart/${sid}/items` | ✅ Complete | -| `shop/cart.html` | `/api/v1/public/vendors/${id}/cart/${sid}` | `/api/v1/shop/cart/${sid}` | ✅ Complete | -| `shop/cart.html` | `/api/v1/public/vendors/${id}/cart/${sid}/items/${pid}` (PUT) | `/api/v1/shop/cart/${sid}/items/${pid}` | ✅ Complete | -| `shop/cart.html` | `/api/v1/public/vendors/${id}/cart/${sid}/items/${pid}` (DELETE) | `/api/v1/shop/cart/${sid}/items/${pid}` | ✅ Complete | +| `shop/account/login.html` | `/api/v1/platform/vendors/${id}/customers/login` | `/api/v1/shop/auth/login` | ✅ Complete | +| `shop/account/register.html` | `/api/v1/platform/vendors/${id}/customers/register` | `/api/v1/shop/auth/register` | ✅ Complete | +| `shop/product.html` | `/api/v1/platform/vendors/${id}/products/${pid}` | `/api/v1/shop/products/${pid}` | ✅ Complete | +| `shop/product.html` | `/api/v1/platform/vendors/${id}/products?limit=4` | `/api/v1/shop/products?limit=4` | ✅ Complete | +| `shop/product.html` | `/api/v1/platform/vendors/${id}/cart/${sid}` | `/api/v1/shop/cart/${sid}` | ✅ Complete | +| `shop/product.html` | `/api/v1/platform/vendors/${id}/cart/${sid}/items` | `/api/v1/shop/cart/${sid}/items` | ✅ Complete | +| `shop/cart.html` | `/api/v1/platform/vendors/${id}/cart/${sid}` | `/api/v1/shop/cart/${sid}` | ✅ Complete | +| `shop/cart.html` | `/api/v1/platform/vendors/${id}/cart/${sid}/items/${pid}` (PUT) | `/api/v1/shop/cart/${sid}/items/${pid}` | ✅ Complete | +| `shop/cart.html` | `/api/v1/platform/vendors/${id}/cart/${sid}/items/${pid}` (DELETE) | `/api/v1/shop/cart/${sid}/items/${pid}` | ✅ Complete | | `shop/products.html` | Already using `/api/v1/shop/products` | (No change needed) | ✅ Already Updated | | `shop/home.html` | Already using `/api/v1/shop/products?featured=true` | (No change needed) | ✅ Already Updated | @@ -196,7 +196,7 @@ grep -r "api/v1/public/vendors" app/templates/shop --include="*.html" ### ✅ Phase 3: Old Endpoint Cleanup (COMPLETE) -Cleaned up old `/api/v1/public/vendors/*` endpoints: +Cleaned up old `/api/v1/platform/vendors/*` endpoints: **Files Removed:** - ❌ `auth.py` - Migrated to `/api/v1/shop/auth.py` @@ -209,14 +209,14 @@ Cleaned up old `/api/v1/public/vendors/*` endpoints: **Files Kept:** - ✅ `vendors.py` - Vendor lookup endpoints (truly public, not shop-specific) - - `GET /api/v1/public/vendors/by-code/{vendor_code}` - - `GET /api/v1/public/vendors/by-subdomain/{subdomain}` - - `GET /api/v1/public/vendors/{vendor_id}/info` + - `GET /api/v1/platform/vendors/by-code/{vendor_code}` + - `GET /api/v1/platform/vendors/by-subdomain/{subdomain}` + - `GET /api/v1/platform/vendors/{vendor_id}/info` **Updated:** -- ✅ `/app/api/v1/public/__init__.py` - Now only includes vendor lookup endpoints +- ✅ `/app/api/v1/platform/__init__.py` - Now only includes vendor lookup endpoints -**Result:** Old shop endpoints completely removed, only vendor lookup remains in `/api/v1/public/vendors/*` +**Result:** Old shop endpoints completely removed, only vendor lookup remains in `/api/v1/platform/vendors/*` ### ⚠️ Phase 4: Deprecation Warnings (SKIPPED - Not Needed) @@ -252,16 +252,16 @@ Old endpoint cleanup completed immediately (no gradual migration needed): 1. ✅ Removed old endpoint files: ```bash - rm app/api/v1/public/vendors/products.py - rm app/api/v1/public/vendors/cart.py - rm app/api/v1/public/vendors/orders.py - rm app/api/v1/public/vendors/auth.py - rm app/api/v1/public/vendors/payments.py - rm app/api/v1/public/vendors/search.py - rm app/api/v1/public/vendors/shop.py + rm app/api/v1/platform/vendors/products.py + rm app/api/v1/platform/vendors/cart.py + rm app/api/v1/platform/vendors/orders.py + rm app/api/v1/platform/vendors/auth.py + rm app/api/v1/platform/vendors/payments.py + rm app/api/v1/platform/vendors/search.py + rm app/api/v1/platform/vendors/shop.py ``` -2. ✅ Updated `/api/v1/public/__init__.py`: +2. ✅ Updated `/api/v1/platform/__init__.py`: ```python # Only import vendor lookup endpoints from .vendors import vendors @@ -280,12 +280,12 @@ Old endpoint cleanup completed immediately (no gradual migration needed): ### Before (Old Pattern) ``` # Verbose - requires vendor_id everywhere -/api/v1/public/vendors/123/products -/api/v1/public/vendors/123/products/456 -/api/v1/public/vendors/123/cart/abc-session-id -/api/v1/public/vendors/123/cart/abc-session-id/items -/api/v1/public/vendors/123/customers/789/orders -/api/v1/public/vendors/auth/123/customers/login +/api/v1/platform/vendors/123/products +/api/v1/platform/vendors/123/products/456 +/api/v1/platform/vendors/123/cart/abc-session-id +/api/v1/platform/vendors/123/cart/abc-session-id/items +/api/v1/platform/vendors/123/customers/789/orders +/api/v1/platform/vendors/auth/123/customers/login ``` ### After (New Pattern) diff --git a/docs/architecture/frontend-structure.md b/docs/architecture/frontend-structure.md index a2c6a4cf..726e257f 100644 --- a/docs/architecture/frontend-structure.md +++ b/docs/architecture/frontend-structure.md @@ -452,7 +452,7 @@ All frontends communicate with backend via APIs: - `/api/v1/admin/*` - Admin APIs - `/api/v1/vendor/*` - Vendor APIs - `/api/v1/storefront/*` - Storefront APIs -- `/api/v1/public/*` - Platform APIs +- `/api/v1/platform/*` - Platform APIs **Benefits:** - Clear backend contracts diff --git a/docs/deployment/stripe-integration.md b/docs/deployment/stripe-integration.md index 83392032..113c6302 100644 --- a/docs/deployment/stripe-integration.md +++ b/docs/deployment/stripe-integration.md @@ -472,7 +472,7 @@ async def setup_payments( } -# app/api/v1/public/vendors/payments.py +# app/api/v1/platform/vendors/payments.py @router.post("/{vendor_id}/payments/create-intent") async def create_payment_intent( vendor_id: int, @@ -535,7 +535,7 @@ class CheckoutManager { async initializePayment(orderData) { // Create payment intent - const response = await fetch(`/api/v1/public/vendors/${this.vendorId}/payments/create-intent`, { + const response = await fetch(`/api/v1/platform/vendors/${this.vendorId}/payments/create-intent`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ @@ -585,7 +585,7 @@ Customer proceeds to checkout ↓ System creates Order (payment_status: pending) ↓ -Frontend calls POST /api/v1/public/vendors/{vendor_id}/payments/create-intent +Frontend calls POST /api/v1/platform/vendors/{vendor_id}/payments/create-intent ↓ PaymentService creates Stripe PaymentIntent with vendor destination ↓ diff --git a/docs/development/naming-conventions.md b/docs/development/naming-conventions.md index 9b1e7cdc..4ce4d0e6 100644 --- a/docs/development/naming-conventions.md +++ b/docs/development/naming-conventions.md @@ -53,7 +53,7 @@ app/api/v1/vendor/ ├── inventory.py # Handles inventory items └── settings.py # Exception: not a resource collection -app/api/v1/public/vendors/ +app/api/v1/platform/vendors/ ├── products.py # Public product catalog ├── orders.py # Order placement └── auth.py # Exception: authentication service diff --git a/docs/implementation/platform-marketing-homepage.md b/docs/implementation/platform-marketing-homepage.md index 05f5935e..f6d914da 100644 --- a/docs/implementation/platform-marketing-homepage.md +++ b/docs/implementation/platform-marketing-homepage.md @@ -132,24 +132,24 @@ Standalone page with: ## API Endpoints -All endpoints under `/api/v1/public/`: +All endpoints under `/api/v1/platform/`: ### Pricing Endpoints ``` -GET /api/v1/public/tiers +GET /api/v1/platform/tiers Returns all public subscription tiers Response: TierResponse[] -GET /api/v1/public/tiers/{tier_code} +GET /api/v1/platform/tiers/{tier_code} Returns specific tier by code Response: TierResponse -GET /api/v1/public/addons +GET /api/v1/platform/addons Returns all active add-on products Response: AddOnResponse[] -GET /api/v1/public/pricing +GET /api/v1/platform/pricing Returns complete pricing info (tiers + addons + trial_days) Response: PricingResponse ``` @@ -157,17 +157,17 @@ GET /api/v1/public/pricing ### Letzshop Vendor Endpoints ``` -GET /api/v1/public/letzshop-vendors +GET /api/v1/platform/letzshop-vendors Query params: ?search=&category=&city=&page=1&limit=20 Returns paginated vendor list (placeholder for future) Response: LetzshopVendorListResponse -POST /api/v1/public/letzshop-vendors/lookup +POST /api/v1/platform/letzshop-vendors/lookup Body: { "url": "letzshop.lu/vendors/my-shop" } Returns vendor info from URL lookup Response: LetzshopLookupResponse -GET /api/v1/public/letzshop-vendors/{slug} +GET /api/v1/platform/letzshop-vendors/{slug} Returns vendor info by slug Response: LetzshopVendorInfo ``` @@ -175,17 +175,17 @@ GET /api/v1/public/letzshop-vendors/{slug} ### Signup Endpoints ``` -POST /api/v1/public/signup/start +POST /api/v1/platform/signup/start Body: { "tier_code": "professional", "is_annual": false } Creates signup session Response: { "session_id": "...", "tier_code": "...", "is_annual": false } -POST /api/v1/public/signup/claim-vendor +POST /api/v1/platform/signup/claim-vendor Body: { "session_id": "...", "letzshop_slug": "my-shop" } Claims Letzshop vendor for session Response: { "session_id": "...", "letzshop_slug": "...", "vendor_name": "..." } -POST /api/v1/public/signup/create-account +POST /api/v1/platform/signup/create-account Body: { "session_id": "...", "email": "user@example.com", @@ -197,17 +197,17 @@ POST /api/v1/public/signup/create-account Creates User, Company, Vendor, Stripe Customer Response: { "session_id": "...", "user_id": 1, "vendor_id": 1, "stripe_customer_id": "cus_..." } -POST /api/v1/public/signup/setup-payment +POST /api/v1/platform/signup/setup-payment Body: { "session_id": "..." } Creates Stripe SetupIntent Response: { "session_id": "...", "client_secret": "seti_...", "stripe_customer_id": "cus_..." } -POST /api/v1/public/signup/complete +POST /api/v1/platform/signup/complete Body: { "session_id": "...", "setup_intent_id": "seti_..." } Completes signup, attaches payment method Response: { "success": true, "vendor_code": "...", "vendor_id": 1, "redirect_url": "...", "trial_ends_at": "..." } -GET /api/v1/public/signup/session/{session_id} +GET /api/v1/platform/signup/session/{session_id} Returns session status for resuming signup Response: { "session_id": "...", "step": "...", ... } ``` @@ -430,7 +430,7 @@ STRIPE_TRIAL_DAYS=30 ### Automated Tests -Test files located in `tests/integration/api/v1/public/`: +Test files located in `tests/integration/api/v1/platform/`: | File | Tests | Description | |------|-------|-------------| @@ -440,7 +440,7 @@ Test files located in `tests/integration/api/v1/public/`: **Run tests:** ```bash -pytest tests/integration/api/v1/public/ -v +pytest tests/integration/api/v1/platform/ -v ``` **Test categories:** @@ -479,15 +479,15 @@ pytest tests/integration/api/v1/public/ -v ```bash # Get pricing -curl http://localhost:8000/api/v1/public/pricing +curl http://localhost:8000/api/v1/platform/pricing # Lookup vendor -curl -X POST http://localhost:8000/api/v1/public/letzshop-vendors/lookup \ +curl -X POST http://localhost:8000/api/v1/platform/letzshop-vendors/lookup \ -H "Content-Type: application/json" \ -d '{"url": "letzshop.lu/vendors/test-shop"}' # Start signup -curl -X POST http://localhost:8000/api/v1/public/signup/start \ +curl -X POST http://localhost:8000/api/v1/platform/signup/start \ -H "Content-Type: application/json" \ -d '{"tier_code": "professional", "is_annual": false}' ``` diff --git a/docs/proposals/plan-perms.md b/docs/proposals/plan-perms.md new file mode 100644 index 00000000..014bc9b4 --- /dev/null +++ b/docs/proposals/plan-perms.md @@ -0,0 +1,653 @@ +# Plan: Flexible Role & Permission Management with Platform Controls + +## Status: READY FOR APPROVAL + +## Summary + +Design a flexible role/permission management system that: +1. **Modules define permissions** - Each module declares its available permissions +2. **Platforms control availability** - Platforms can restrict which permissions vendors can use +3. **Vendors customize roles** - Vendors create custom roles within platform constraints +4. **Multi-tier hierarchy** - Platform → Vendor → User permission inheritance + +--- + +## Current State Analysis + +### What Exists Today + +| Component | Location | Description | +|-----------|----------|-------------| +| **Role Model** | `app/modules/tenancy/models/vendor.py` | `vendor_id`, `name`, `permissions` (JSON array) | +| **VendorUser Model** | Same file | Links user → vendor with `role_id` | +| **PermissionDiscoveryService** | `app/modules/tenancy/services/permission_discovery_service.py` | Discovers permissions from modules | +| **VendorTeamService** | `app/modules/tenancy/services/vendor_team_service.py` | Manages team invitations, role assignment | +| **Role Presets** | In discovery service code | Hardcoded `ROLE_PRESETS` dict | +| **Platform Model** | `models/database/platform.py` | Multi-platform support | +| **PlatformModule** | `models/database/platform_module.py` | Controls which modules are enabled per platform | +| **VendorPlatform** | `models/database/vendor_platform.py` | Vendor-platform relationship with `tier_id` | + +### Current Gaps + +1. **No platform-level permission control** - Platforms cannot restrict which permissions vendors can assign +2. **No custom role CRUD API** - Roles are created implicitly when inviting team members +3. **Presets are code-only** - Cannot customize role templates per platform +4. **No role templates table** - Platform admins cannot define default roles for their vendors + +--- + +## Proposed Architecture + +### Tier 1: Module-Defined Permissions (Exists) + +Each module declares permissions in `definition.py`: + +```python +permissions=[ + PermissionDefinition( + id="products.view", + label_key="catalog.permissions.products_view", + category="products", + ), + PermissionDefinition( + id="products.create", + label_key="catalog.permissions.products_create", + category="products", + ), +] +``` + +**Discovery Service** aggregates all permissions at runtime. + +### Tier 2: Platform Permission Control (New) + +New `PlatformPermissionConfig` model to control: +- Which permissions are available to vendors on this platform +- Default role templates for vendor onboarding +- Permission bundles based on subscription tier + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ PLATFORM │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ PlatformPermissionConfig │ │ +│ │ - platform_id │ │ +│ │ - allowed_permissions: ["products.*", "orders.*"] │ │ +│ │ - blocked_permissions: ["team.manage"] │ │ +│ │ - tier_restrictions: {free: [...], pro: [...]} │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ PlatformRoleTemplate │ │ +│ │ - platform_id │ │ +│ │ - name: "Manager", "Staff", etc. │ │ +│ │ - permissions: [...] │ │ +│ │ - is_default: bool (create for new vendors) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +### Tier 3: Vendor Role Customization (Enhanced) + +Vendors can: +- View roles available (from platform templates or custom) +- Create custom roles (within platform constraints) +- Edit role permissions (within allowed set) +- Assign roles to team members + +``` +┌─────────────────────────────────────────────────────────────────┐ +│ VENDOR │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ Role (existing model, enhanced) │ │ +│ │ - vendor_id │ │ +│ │ - name │ │ +│ │ - permissions: [...] (validated against platform) │ │ +│ │ - is_from_template: bool │ │ +│ │ - source_template_id: FK (nullable) │ │ +│ └─────────────────────────────────────────────────────────┘ │ +│ │ +│ ┌─────────────────────────────────────────────────────────┐ │ +│ │ VendorUser (existing, unchanged) │ │ +│ │ - user_id │ │ +│ │ - vendor_id │ │ +│ │ - role_id │ │ +│ └─────────────────────────────────────────────────────────┘ │ +└─────────────────────────────────────────────────────────────────┘ +``` + +--- + +## Data Model Changes + +### New Models + +#### 1. PlatformPermissionConfig + +```python +# app/modules/tenancy/models/platform_permission_config.py + +class PlatformPermissionConfig(Base): + """Platform-level permission configuration""" + __tablename__ = "platform_permission_configs" + + id: Mapped[int] = mapped_column(primary_key=True) + platform_id: Mapped[int] = mapped_column(ForeignKey("platforms.id"), unique=True) + + # Permissions this platform allows vendors to use + # Empty = all discovered permissions allowed + allowed_permissions: Mapped[list[str]] = mapped_column(JSON, default=list) + + # Explicit blocklist (takes precedence over allowed) + blocked_permissions: Mapped[list[str]] = mapped_column(JSON, default=list) + + # Tier-based restrictions: {"free": ["products.view"], "pro": ["products.*"]} + tier_permissions: Mapped[dict] = mapped_column(JSON, default=dict) + + created_at: Mapped[datetime] = mapped_column(default=func.now()) + updated_at: Mapped[datetime] = mapped_column(onupdate=func.now()) + + # Relationships + platform: Mapped["Platform"] = relationship(back_populates="permission_config") +``` + +#### 2. PlatformRoleTemplate + +```python +# app/modules/tenancy/models/platform_role_template.py + +class PlatformRoleTemplate(Base): + """Role templates defined at platform level""" + __tablename__ = "platform_role_templates" + + id: Mapped[int] = mapped_column(primary_key=True) + platform_id: Mapped[int] = mapped_column(ForeignKey("platforms.id")) + + name: Mapped[str] = mapped_column(String(50)) # "Manager", "Staff", etc. + display_name: Mapped[str] = mapped_column(String(100)) # i18n key or display name + description: Mapped[str | None] = mapped_column(String(255)) + + # Permissions for this template + permissions: Mapped[list[str]] = mapped_column(JSON, default=list) + + # Configuration + is_default: Mapped[bool] = mapped_column(default=False) # Auto-create for new vendors + is_system: Mapped[bool] = mapped_column(default=False) # Cannot be deleted + order: Mapped[int] = mapped_column(default=100) # Display order + + created_at: Mapped[datetime] = mapped_column(default=func.now()) + updated_at: Mapped[datetime] = mapped_column(onupdate=func.now()) + + # Relationships + platform: Mapped["Platform"] = relationship(back_populates="role_templates") + + __table_args__ = ( + UniqueConstraint("platform_id", "name", name="uq_platform_role_template"), + ) +``` + +### Enhanced Existing Models + +#### Role Model Enhancement + +```python +# Add to existing Role model in app/modules/tenancy/models/vendor.py + +class Role(Base): + # ... existing fields ... + + # NEW: Track template origin + source_template_id: Mapped[int | None] = mapped_column( + ForeignKey("platform_role_templates.id"), + nullable=True + ) + is_custom: Mapped[bool] = mapped_column(default=False) # Vendor-created custom role + + # Relationship + source_template: Mapped["PlatformRoleTemplate"] = relationship() +``` + +--- + +## Service Layer Changes + +### 1. PlatformPermissionService (New) + +```python +# app/modules/tenancy/services/platform_permission_service.py + +class PlatformPermissionService: + """Manages platform-level permission configuration""" + + def get_allowed_permissions( + self, + db: Session, + platform_id: int, + tier_id: int | None = None + ) -> set[str]: + """ + Get permissions allowed for a platform/tier combination. + + 1. Start with all discovered permissions + 2. Filter by platform's allowed_permissions (if set) + 3. Remove blocked_permissions + 4. Apply tier restrictions (if tier_id provided) + """ + pass + + def validate_permissions( + self, + db: Session, + platform_id: int, + tier_id: int | None, + permissions: list[str] + ) -> tuple[list[str], list[str]]: + """ + Validate permissions against platform constraints. + Returns (valid_permissions, invalid_permissions) + """ + pass + + def update_platform_config( + self, + db: Session, + platform_id: int, + allowed_permissions: list[str] | None = None, + blocked_permissions: list[str] | None = None, + tier_permissions: dict | None = None + ) -> PlatformPermissionConfig: + """Update platform permission configuration""" + pass +``` + +### 2. PlatformRoleTemplateService (New) + +```python +# app/modules/tenancy/services/platform_role_template_service.py + +class PlatformRoleTemplateService: + """Manages platform role templates""" + + def get_templates(self, db: Session, platform_id: int) -> list[PlatformRoleTemplate]: + """Get all role templates for a platform""" + pass + + def create_template( + self, + db: Session, + platform_id: int, + name: str, + permissions: list[str], + is_default: bool = False + ) -> PlatformRoleTemplate: + """Create a new role template (validates permissions)""" + pass + + def create_default_roles_for_vendor( + self, + db: Session, + vendor: Vendor + ) -> list[Role]: + """ + Create vendor roles from platform's default templates. + Called during vendor onboarding. + """ + pass + + def seed_default_templates(self, db: Session, platform_id: int): + """Seed platform with standard role templates (Manager, Staff, etc.)""" + pass +``` + +### 3. Enhanced VendorTeamService + +```python +# Updates to app/modules/tenancy/services/vendor_team_service.py + +class VendorTeamService: + + def get_available_permissions( + self, + db: Session, + vendor: Vendor + ) -> list[PermissionDefinition]: + """ + Get permissions available to this vendor based on: + 1. Platform constraints + 2. Vendor's subscription tier + """ + platform_perm_service = PlatformPermissionService() + vendor_platform = db.query(VendorPlatform).filter(...).first() + + allowed = platform_perm_service.get_allowed_permissions( + db, + vendor_platform.platform_id, + vendor_platform.tier_id + ) + + # Return PermissionDefinitions filtered to allowed set + all_perms = permission_discovery_service.get_all_permissions() + return [p for p in all_perms if p.id in allowed] + + def create_custom_role( + self, + db: Session, + vendor: Vendor, + name: str, + permissions: list[str] + ) -> Role: + """ + Create a custom role for the vendor. + Validates permissions against platform constraints. + """ + # Validate permissions + valid, invalid = self.platform_permission_service.validate_permissions( + db, vendor.platform_id, vendor.tier_id, permissions + ) + if invalid: + raise InvalidPermissionsException(invalid) + + role = Role( + vendor_id=vendor.id, + name=name, + permissions=valid, + is_custom=True + ) + db.add(role) + return role + + def update_role( + self, + db: Session, + vendor: Vendor, + role_id: int, + name: str | None = None, + permissions: list[str] | None = None + ) -> Role: + """Update an existing role (validates permissions)""" + pass + + def delete_role( + self, + db: Session, + vendor: Vendor, + role_id: int + ) -> bool: + """Delete a custom role (cannot delete if in use)""" + pass +``` + +--- + +## API Endpoints + +### Platform Admin Endpoints (Admin Panel) + +```python +# app/modules/tenancy/routes/admin/platform_permissions.py + +@router.get("/platforms/{platform_id}/permissions") +def get_platform_permission_config(platform_id: int): + """Get platform permission configuration""" + +@router.put("/platforms/{platform_id}/permissions") +def update_platform_permission_config(platform_id: int, config: PermissionConfigUpdate): + """Update platform permission configuration""" + +@router.get("/platforms/{platform_id}/role-templates") +def list_role_templates(platform_id: int): + """List role templates for a platform""" + +@router.post("/platforms/{platform_id}/role-templates") +def create_role_template(platform_id: int, template: RoleTemplateCreate): + """Create a new role template""" + +@router.put("/platforms/{platform_id}/role-templates/{template_id}") +def update_role_template(platform_id: int, template_id: int, template: RoleTemplateUpdate): + """Update a role template""" + +@router.delete("/platforms/{platform_id}/role-templates/{template_id}") +def delete_role_template(platform_id: int, template_id: int): + """Delete a role template""" +``` + +### Vendor Dashboard Endpoints + +```python +# app/modules/tenancy/routes/api/vendor_roles.py + +@router.get("/roles") +def list_vendor_roles(): + """List all roles for current vendor""" + +@router.post("/roles") +def create_custom_role(role: RoleCreate): + """Create a custom role (validates permissions against platform)""" + +@router.put("/roles/{role_id}") +def update_role(role_id: int, role: RoleUpdate): + """Update a role""" + +@router.delete("/roles/{role_id}") +def delete_role(role_id: int): + """Delete a custom role""" + +@router.get("/available-permissions") +def get_available_permissions(): + """Get permissions available to this vendor (filtered by platform/tier)""" +``` + +--- + +## Permission Flow Diagram + +``` +┌─────────────────────────────────────────────────────────────────────────────┐ +│ PERMISSION FLOW │ +└─────────────────────────────────────────────────────────────────────────────┘ + +1. MODULE DEFINES PERMISSIONS + ┌──────────────┐ + │ catalog │ → products.view, products.create, products.edit, ... + │ orders │ → orders.view, orders.manage, orders.refund, ... + │ team │ → team.view, team.manage, team.invite, ... + └──────────────┘ + ↓ + +2. DISCOVERY SERVICE AGGREGATES + ┌────────────────────────────────────────────────┐ + │ PermissionDiscoveryService │ + │ get_all_permissions() → 50+ permissions │ + └────────────────────────────────────────────────┘ + ↓ + +3. PLATFORM FILTERS PERMISSIONS + ┌────────────────────────────────────────────────┐ + │ PlatformPermissionConfig │ + │ allowed: ["products.*", "orders.view"] │ + │ blocked: ["orders.refund"] │ + │ tier_permissions: │ + │ free: ["products.view", "orders.view"] │ + │ pro: ["products.*", "orders.*"] │ + └────────────────────────────────────────────────┘ + ↓ + +4. VENDOR CREATES/USES ROLES + ┌────────────────────────────────────────────────┐ + │ Role (vendor-specific) │ + │ Manager: [products.*, orders.view] │ + │ Staff: [products.view, orders.view] │ + └────────────────────────────────────────────────┘ + ↓ + +5. USER GETS PERMISSIONS VIA ROLE + ┌────────────────────────────────────────────────┐ + │ VendorUser │ + │ user_id: 123 │ + │ role_id: 5 (Staff) │ + │ → permissions: [products.view, orders.view] │ + └────────────────────────────────────────────────┘ +``` + +--- + +## Database Migrations + +### Migration 1: Add Platform Permission Config + +```sql +CREATE TABLE platform_permission_configs ( + id SERIAL PRIMARY KEY, + platform_id INTEGER NOT NULL UNIQUE REFERENCES platforms(id), + allowed_permissions JSONB DEFAULT '[]', + blocked_permissions JSONB DEFAULT '[]', + tier_permissions JSONB DEFAULT '{}', + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP +); +``` + +### Migration 2: Add Platform Role Templates + +```sql +CREATE TABLE platform_role_templates ( + id SERIAL PRIMARY KEY, + platform_id INTEGER NOT NULL REFERENCES platforms(id), + name VARCHAR(50) NOT NULL, + display_name VARCHAR(100) NOT NULL, + description VARCHAR(255), + permissions JSONB DEFAULT '[]', + is_default BOOLEAN DEFAULT FALSE, + is_system BOOLEAN DEFAULT FALSE, + "order" INTEGER DEFAULT 100, + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP, + UNIQUE (platform_id, name) +); +``` + +### Migration 3: Enhance Roles Table + +```sql +ALTER TABLE roles +ADD COLUMN source_template_id INTEGER REFERENCES platform_role_templates(id), +ADD COLUMN is_custom BOOLEAN DEFAULT FALSE; +``` + +--- + +## Implementation Phases + +### Phase 1: Data Models (Foundation) + +**Files to create:** +- `app/modules/tenancy/models/platform_permission_config.py` +- `app/modules/tenancy/models/platform_role_template.py` +- `migrations/versions/xxx_add_platform_permission_tables.py` +- `migrations/versions/xxx_enhance_roles_table.py` + +**Files to modify:** +- `app/modules/tenancy/models/__init__.py` - Export new models +- `app/modules/tenancy/models/vendor.py` - Add Role enhancement + +### Phase 2: Service Layer + +**Files to create:** +- `app/modules/tenancy/services/platform_permission_service.py` +- `app/modules/tenancy/services/platform_role_template_service.py` + +**Files to modify:** +- `app/modules/tenancy/services/vendor_team_service.py` - Add role CRUD, permission validation + +### Phase 3: API Endpoints + +**Files to create:** +- `app/modules/tenancy/routes/admin/platform_permissions.py` +- `app/modules/tenancy/routes/api/vendor_roles.py` +- `app/modules/tenancy/schemas/platform_permissions.py` +- `app/modules/tenancy/schemas/roles.py` + +**Files to modify:** +- `app/modules/tenancy/routes/__init__.py` - Register new routers + +### Phase 4: Vendor Onboarding Integration + +**Files to modify:** +- `app/modules/tenancy/services/vendor_service.py` - Create default roles from templates during vendor creation + +### Phase 5: Admin UI (Optional, Future) + +**Files to create/modify:** +- Admin panel for platform permission configuration +- Admin panel for role template management +- Vendor dashboard for custom role management + +--- + +## Verification + +1. **App loads:** `python -c "from main import app; print('OK')"` + +2. **Migrations run:** `make migrate-up` + +3. **Architecture validation:** `python scripts/validate_architecture.py -v` + +4. **Unit tests:** Test permission filtering logic + - Platform with no config → all permissions allowed + - Platform with allowed list → only those permissions + - Platform with blocked list → all except blocked + - Tier restrictions → correct subset per tier + +5. **Integration tests:** + - Create vendor → gets default roles from platform templates + - Create custom role → validates against platform constraints + - Assign role → user gets correct permissions + - Change tier → available permissions update + +6. **API tests:** + - Platform admin can configure permissions + - Vendor owner can create/edit custom roles + - Invalid permissions are rejected + +--- + +## Files Summary + +### New Files (9) + +| File | Purpose | +|------|---------| +| `app/modules/tenancy/models/platform_permission_config.py` | Platform permission config model | +| `app/modules/tenancy/models/platform_role_template.py` | Platform role template model | +| `app/modules/tenancy/services/platform_permission_service.py` | Platform permission logic | +| `app/modules/tenancy/services/platform_role_template_service.py` | Role template logic | +| `app/modules/tenancy/routes/admin/platform_permissions.py` | Admin API endpoints | +| `app/modules/tenancy/routes/api/vendor_roles.py` | Vendor API endpoints | +| `app/modules/tenancy/schemas/platform_permissions.py` | Pydantic schemas | +| `app/modules/tenancy/schemas/roles.py` | Role schemas | +| `migrations/versions/xxx_platform_permission_tables.py` | Database migration | + +### Modified Files (5) + +| File | Changes | +|------|---------| +| `app/modules/tenancy/models/__init__.py` | Export new models | +| `app/modules/tenancy/models/vendor.py` | Enhance Role model | +| `app/modules/tenancy/services/vendor_team_service.py` | Add role CRUD, validation | +| `app/modules/tenancy/services/vendor_service.py` | Create default roles on vendor creation | +| `app/modules/tenancy/routes/__init__.py` | Register new routers | + +--- + +## Key Design Decisions + +1. **Wildcard support in permissions** - `products.*` matches `products.view`, `products.create`, etc. + +2. **Tier inheritance** - Higher tiers include all permissions of lower tiers + +3. **Template-based vendor roles** - Default roles created from platform templates, but vendor can customize + +4. **Soft validation** - Invalid permissions in existing roles are not automatically removed (audit trail) + +5. **Backward compatible** - Existing roles without `source_template_id` continue to work +cl \ No newline at end of file diff --git a/docs/testing/test-structure.md b/docs/testing/test-structure.md index f833a2b0..be57b984 100644 --- a/docs/testing/test-structure.md +++ b/docs/testing/test-structure.md @@ -31,7 +31,7 @@ The test structure directly mirrors the API code structure: ``` app/api/v1/admin/ → tests/integration/api/v1/admin/ app/api/v1/vendor/ → tests/integration/api/v1/vendor/ -app/api/v1/public/ → tests/integration/api/v1/public/ +app/api/v1/platform/ → tests/integration/api/v1/platform/ app/api/v1/shared/ → tests/integration/api/v1/shared/ ``` @@ -83,7 +83,7 @@ Different teams can work in parallel with fewer conflicts: ``` Admin Team: works in tests/integration/api/v1/admin/ Vendor Team: works in tests/integration/api/v1/vendor/ -Public Team: works in tests/integration/api/v1/public/ +Public Team: works in tests/integration/api/v1/platform/ ``` ## Running Tests @@ -98,7 +98,7 @@ pytest tests/integration/api/v1/vendor/ -v pytest tests/integration/api/v1/admin/ -v # All public tests -pytest tests/integration/api/v1/public/ -v +pytest tests/integration/api/v1/platform/ -v # All shared tests pytest tests/integration/api/v1/shared/ -v @@ -277,7 +277,7 @@ See [Vendor API Testing Guide](vendor-api-testing.md) for details. ### Public Tests (`public/`) -Tests for public endpoints at `/api/v1/public/*`: +Tests for public endpoints at `/api/v1/platform/*`: - Product catalog browsing - Public vendor profiles diff --git a/main.py b/main.py index 6e4e525e..b3a1c633 100644 --- a/main.py +++ b/main.py @@ -64,7 +64,7 @@ from app.exceptions.handler import setup_exception_handlers # Module route auto-discovery - all page routes now come from modules from app.modules.routes import ( get_admin_page_routes, - get_public_page_routes, + get_platform_page_routes, get_storefront_page_routes, get_vendor_page_routes, ) @@ -323,18 +323,18 @@ logger.info("ROUTE REGISTRATION (AUTO-DISCOVERY)") logger.info("=" * 80) # ============================================================================= -# PUBLIC PAGES (Marketing pages - homepage, pricing, signup, etc.) +# PLATFORM PAGES (Marketing pages - homepage, pricing, signup, etc.) # ============================================================================= -# Public pages are served at root level (/) for platform marketing -logger.info("Auto-discovering public (marketing) page routes...") -public_page_routes = get_public_page_routes() -logger.info(f" Found {len(public_page_routes)} public page route modules") +# Platform pages are served at root level (/) for platform marketing +logger.info("Auto-discovering platform (marketing) page routes...") +platform_page_routes = get_platform_page_routes() +logger.info(f" Found {len(platform_page_routes)} platform page route modules") -for route_info in public_page_routes: - logger.info(f" Registering {route_info.module_code} public pages (priority={route_info.priority})") +for route_info in platform_page_routes: + logger.info(f" Registering {route_info.module_code} platform pages (priority={route_info.priority})") app.include_router( route_info.router, - prefix="", # Public pages at root + prefix="", # Platform pages at root tags=route_info.tags, include_in_schema=route_info.include_in_schema, ) diff --git a/scripts/test_auth_complete.py b/scripts/test_auth_complete.py index 170d52ab..bcf3601e 100644 --- a/scripts/test_auth_complete.py +++ b/scripts/test_auth_complete.py @@ -273,7 +273,7 @@ def test_customer_login() -> dict | None: try: response = requests.post( - f"{BASE_URL}/api/v1/public/vendors/1/customers/login", + f"{BASE_URL}/api/v1/platform/vendors/1/customers/login", json={"username": "customer", "password": "customer123"}, ) diff --git a/tests/integration/api/v1/README.md b/tests/integration/api/v1/README.md index add7f9c8..7f048ed3 100644 --- a/tests/integration/api/v1/README.md +++ b/tests/integration/api/v1/README.md @@ -15,7 +15,7 @@ pytest tests/integration/api/v1/ -v # Run specific area pytest tests/integration/api/v1/vendor/ -v pytest tests/integration/api/v1/admin/ -v -pytest tests/integration/api/v1/public/ -v +pytest tests/integration/api/v1/platform/ -v pytest tests/integration/api/v1/shared/ -v ``` diff --git a/tests/integration/api/v1/platform/README.md b/tests/integration/api/v1/platform/README.md index c97e5283..39edc92a 100644 --- a/tests/integration/api/v1/platform/README.md +++ b/tests/integration/api/v1/platform/README.md @@ -1,4 +1,4 @@ -# Public API Integration Tests +# Platform API Integration Tests ## Documentation @@ -9,8 +9,8 @@ For comprehensive testing documentation, see: ## Quick Start ```bash -# Run all public tests -pytest tests/integration/api/v1/public/ -v +# Run all platform tests +pytest tests/integration/api/v1/platform/ -v ``` ## Status diff --git a/tests/integration/api/v1/platform/__init__.py b/tests/integration/api/v1/platform/__init__.py index 8a3ad401..c7d0e7a4 100644 --- a/tests/integration/api/v1/platform/__init__.py +++ b/tests/integration/api/v1/platform/__init__.py @@ -1,8 +1,8 @@ -# tests/integration/api/v1/public/__init__.py -"""Public API integration tests. +# tests/integration/api/v1/platform/__init__.py +"""Platform API integration tests. -Tests for unauthenticated public endpoints: -- /api/v1/public/signup/* - Multi-step signup flow -- /api/v1/public/pricing/* - Subscription tiers and pricing -- /api/v1/public/letzshop-vendors/* - Vendor lookup for signup +Tests for unauthenticated platform endpoints: +- /api/v1/platform/signup/* - Multi-step signup flow +- /api/v1/platform/pricing/* - Subscription tiers and pricing +- /api/v1/platform/letzshop-vendors/* - Vendor lookup for signup """ diff --git a/tests/integration/api/v1/platform/test_letzshop_vendors.py b/tests/integration/api/v1/platform/test_letzshop_vendors.py index ddb939a2..752cff00 100644 --- a/tests/integration/api/v1/platform/test_letzshop_vendors.py +++ b/tests/integration/api/v1/platform/test_letzshop_vendors.py @@ -1,7 +1,7 @@ -# tests/integration/api/v1/public/test_letzshop_vendors.py +# tests/integration/api/v1/platform/test_letzshop_vendors.py """Integration tests for platform Letzshop vendor lookup API endpoints. -Tests the /api/v1/public/letzshop-vendors/* endpoints. +Tests the /api/v1/platform/letzshop-vendors/* endpoints. """ import pytest @@ -62,15 +62,15 @@ def claimed_vendor(db, test_company): @pytest.mark.api @pytest.mark.platform class TestLetzshopVendorLookupAPI: - """Test Letzshop vendor lookup endpoints at /api/v1/public/letzshop-vendors/*.""" + """Test Letzshop vendor lookup endpoints at /api/v1/platform/letzshop-vendors/*.""" # ========================================================================= - # GET /api/v1/public/letzshop-vendors + # GET /api/v1/platform/letzshop-vendors # ========================================================================= def test_list_vendors_returns_empty_list(self, client): """Test listing vendors returns empty list (placeholder).""" - response = client.get("/api/v1/public/letzshop-vendors") + response = client.get("/api/v1/platform/letzshop-vendors") assert response.status_code == 200 data = response.json() @@ -83,7 +83,7 @@ class TestLetzshopVendorLookupAPI: def test_list_vendors_with_pagination(self, client): """Test listing vendors with pagination parameters.""" - response = client.get("/api/v1/public/letzshop-vendors?page=2&limit=10") + response = client.get("/api/v1/platform/letzshop-vendors?page=2&limit=10") assert response.status_code == 200 data = response.json() @@ -92,7 +92,7 @@ class TestLetzshopVendorLookupAPI: def test_list_vendors_with_search(self, client): """Test listing vendors with search parameter.""" - response = client.get("/api/v1/public/letzshop-vendors?search=my-shop") + response = client.get("/api/v1/platform/letzshop-vendors?search=my-shop") assert response.status_code == 200 data = response.json() @@ -101,7 +101,7 @@ class TestLetzshopVendorLookupAPI: def test_list_vendors_with_filters(self, client): """Test listing vendors with category and city filters.""" response = client.get( - "/api/v1/public/letzshop-vendors?category=fashion&city=luxembourg" + "/api/v1/platform/letzshop-vendors?category=fashion&city=luxembourg" ) assert response.status_code == 200 @@ -111,17 +111,17 @@ class TestLetzshopVendorLookupAPI: def test_list_vendors_limit_validation(self, client): """Test that limit parameter is validated.""" # Maximum limit is 50 - response = client.get("/api/v1/public/letzshop-vendors?limit=100") + response = client.get("/api/v1/platform/letzshop-vendors?limit=100") assert response.status_code == 422 # ========================================================================= - # POST /api/v1/public/letzshop-vendors/lookup + # POST /api/v1/platform/letzshop-vendors/lookup # ========================================================================= def test_lookup_vendor_by_full_url(self, client): """Test looking up vendor by full Letzshop URL.""" response = client.post( - "/api/v1/public/letzshop-vendors/lookup", + "/api/v1/platform/letzshop-vendors/lookup", json={"url": "https://letzshop.lu/vendors/my-test-shop"}, ) @@ -134,7 +134,7 @@ class TestLetzshopVendorLookupAPI: def test_lookup_vendor_by_url_with_language(self, client): """Test looking up vendor by URL with language prefix.""" response = client.post( - "/api/v1/public/letzshop-vendors/lookup", + "/api/v1/platform/letzshop-vendors/lookup", json={"url": "https://letzshop.lu/en/vendors/my-shop"}, ) @@ -146,7 +146,7 @@ class TestLetzshopVendorLookupAPI: def test_lookup_vendor_by_url_without_protocol(self, client): """Test looking up vendor by URL without https://.""" response = client.post( - "/api/v1/public/letzshop-vendors/lookup", + "/api/v1/platform/letzshop-vendors/lookup", json={"url": "letzshop.lu/vendors/test-shop"}, ) @@ -158,7 +158,7 @@ class TestLetzshopVendorLookupAPI: def test_lookup_vendor_by_slug_only(self, client): """Test looking up vendor by slug alone.""" response = client.post( - "/api/v1/public/letzshop-vendors/lookup", + "/api/v1/platform/letzshop-vendors/lookup", json={"url": "my-shop-name"}, ) @@ -170,7 +170,7 @@ class TestLetzshopVendorLookupAPI: def test_lookup_vendor_normalizes_slug(self, client): """Test that slug is normalized to lowercase.""" response = client.post( - "/api/v1/public/letzshop-vendors/lookup", + "/api/v1/platform/letzshop-vendors/lookup", json={"url": "https://letzshop.lu/vendors/MY-SHOP-NAME"}, ) @@ -181,7 +181,7 @@ class TestLetzshopVendorLookupAPI: def test_lookup_vendor_shows_claimed_status(self, client, claimed_vendor): """Test that lookup shows if vendor is already claimed.""" response = client.post( - "/api/v1/public/letzshop-vendors/lookup", + "/api/v1/platform/letzshop-vendors/lookup", json={"url": "claimed-shop"}, ) @@ -193,7 +193,7 @@ class TestLetzshopVendorLookupAPI: def test_lookup_vendor_shows_unclaimed_status(self, client): """Test that lookup shows if vendor is not claimed.""" response = client.post( - "/api/v1/public/letzshop-vendors/lookup", + "/api/v1/platform/letzshop-vendors/lookup", json={"url": "unclaimed-new-shop"}, ) @@ -205,7 +205,7 @@ class TestLetzshopVendorLookupAPI: def test_lookup_vendor_empty_url(self, client): """Test lookup with empty URL.""" response = client.post( - "/api/v1/public/letzshop-vendors/lookup", + "/api/v1/platform/letzshop-vendors/lookup", json={"url": ""}, ) @@ -217,7 +217,7 @@ class TestLetzshopVendorLookupAPI: def test_lookup_vendor_response_has_expected_fields(self, client): """Test that vendor lookup response has all expected fields.""" response = client.post( - "/api/v1/public/letzshop-vendors/lookup", + "/api/v1/platform/letzshop-vendors/lookup", json={"url": "test-vendor"}, ) @@ -230,12 +230,12 @@ class TestLetzshopVendorLookupAPI: assert "is_claimed" in vendor # ========================================================================= - # GET /api/v1/public/letzshop-vendors/{slug} + # GET /api/v1/platform/letzshop-vendors/{slug} # ========================================================================= def test_get_vendor_by_slug(self, client): """Test getting vendor by slug.""" - response = client.get("/api/v1/public/letzshop-vendors/my-shop") + response = client.get("/api/v1/platform/letzshop-vendors/my-shop") assert response.status_code == 200 data = response.json() @@ -246,7 +246,7 @@ class TestLetzshopVendorLookupAPI: def test_get_vendor_normalizes_slug(self, client): """Test that get vendor normalizes slug to lowercase.""" - response = client.get("/api/v1/public/letzshop-vendors/MY-SHOP") + response = client.get("/api/v1/platform/letzshop-vendors/MY-SHOP") assert response.status_code == 200 data = response.json() @@ -254,7 +254,7 @@ class TestLetzshopVendorLookupAPI: def test_get_claimed_vendor_shows_status(self, client, claimed_vendor): """Test that get vendor shows claimed status correctly.""" - response = client.get("/api/v1/public/letzshop-vendors/claimed-shop") + response = client.get("/api/v1/platform/letzshop-vendors/claimed-shop") assert response.status_code == 200 data = response.json() @@ -262,7 +262,7 @@ class TestLetzshopVendorLookupAPI: def test_get_unclaimed_vendor_shows_status(self, client): """Test that get vendor shows unclaimed status correctly.""" - response = client.get("/api/v1/public/letzshop-vendors/new-unclaimed-shop") + response = client.get("/api/v1/platform/letzshop-vendors/new-unclaimed-shop") assert response.status_code == 200 data = response.json() @@ -278,7 +278,7 @@ class TestLetzshopSlugExtraction: def test_extract_from_full_https_url(self, client): """Test extraction from https://letzshop.lu/vendors/slug.""" response = client.post( - "/api/v1/public/letzshop-vendors/lookup", + "/api/v1/platform/letzshop-vendors/lookup", json={"url": "https://letzshop.lu/vendors/cafe-luxembourg"}, ) assert response.json()["vendor"]["slug"] == "cafe-luxembourg" @@ -286,7 +286,7 @@ class TestLetzshopSlugExtraction: def test_extract_from_http_url(self, client): """Test extraction from http://letzshop.lu/vendors/slug.""" response = client.post( - "/api/v1/public/letzshop-vendors/lookup", + "/api/v1/platform/letzshop-vendors/lookup", json={"url": "http://letzshop.lu/vendors/my-shop"}, ) assert response.json()["vendor"]["slug"] == "my-shop" @@ -294,7 +294,7 @@ class TestLetzshopSlugExtraction: def test_extract_from_url_with_trailing_slash(self, client): """Test extraction from URL with trailing slash.""" response = client.post( - "/api/v1/public/letzshop-vendors/lookup", + "/api/v1/platform/letzshop-vendors/lookup", json={"url": "https://letzshop.lu/vendors/my-shop/"}, ) assert response.json()["vendor"]["slug"] == "my-shop" @@ -302,7 +302,7 @@ class TestLetzshopSlugExtraction: def test_extract_from_url_with_query_params(self, client): """Test extraction from URL with query parameters.""" response = client.post( - "/api/v1/public/letzshop-vendors/lookup", + "/api/v1/platform/letzshop-vendors/lookup", json={"url": "https://letzshop.lu/vendors/my-shop?ref=google"}, ) assert response.json()["vendor"]["slug"] == "my-shop" @@ -310,7 +310,7 @@ class TestLetzshopSlugExtraction: def test_extract_from_french_url(self, client): """Test extraction from French language URL.""" response = client.post( - "/api/v1/public/letzshop-vendors/lookup", + "/api/v1/platform/letzshop-vendors/lookup", json={"url": "https://letzshop.lu/fr/vendors/boulangerie-paul"}, ) assert response.json()["vendor"]["slug"] == "boulangerie-paul" @@ -318,7 +318,7 @@ class TestLetzshopSlugExtraction: def test_extract_from_german_url(self, client): """Test extraction from German language URL.""" response = client.post( - "/api/v1/public/letzshop-vendors/lookup", + "/api/v1/platform/letzshop-vendors/lookup", json={"url": "https://letzshop.lu/de/vendors/backerei-muller"}, ) assert response.json()["vendor"]["slug"] == "backerei-muller" diff --git a/tests/integration/api/v1/platform/test_pricing.py b/tests/integration/api/v1/platform/test_pricing.py index 082177c1..104102b3 100644 --- a/tests/integration/api/v1/platform/test_pricing.py +++ b/tests/integration/api/v1/platform/test_pricing.py @@ -1,7 +1,7 @@ -# tests/integration/api/v1/public/test_pricing.py +# tests/integration/api/v1/platform/test_pricing.py """Integration tests for platform pricing API endpoints. -Tests the /api/v1/public/pricing/* endpoints. +Tests the /api/v1/platform/pricing/* endpoints. """ import pytest @@ -18,15 +18,15 @@ from app.modules.billing.models import ( @pytest.mark.api @pytest.mark.platform class TestPlatformPricingAPI: - """Test platform pricing endpoints at /api/v1/public/*.""" + """Test platform pricing endpoints at /api/v1/platform/*.""" # ========================================================================= - # GET /api/v1/public/tiers + # GET /api/v1/platform/pricing/tiers # ========================================================================= def test_get_tiers_returns_all_public_tiers(self, client): """Test getting all subscription tiers.""" - response = client.get("/api/v1/public/tiers") + response = client.get("/api/v1/platform/pricing/tiers") assert response.status_code == 200 data = response.json() @@ -35,7 +35,7 @@ class TestPlatformPricingAPI: def test_get_tiers_has_expected_fields(self, client): """Test that tier response has all expected fields.""" - response = client.get("/api/v1/public/tiers") + response = client.get("/api/v1/platform/pricing/tiers") assert response.status_code == 200 data = response.json() @@ -55,7 +55,7 @@ class TestPlatformPricingAPI: def test_get_tiers_includes_essential(self, client): """Test that Essential tier is included.""" - response = client.get("/api/v1/public/tiers") + response = client.get("/api/v1/platform/pricing/tiers") assert response.status_code == 200 data = response.json() @@ -64,7 +64,7 @@ class TestPlatformPricingAPI: def test_get_tiers_includes_professional(self, client): """Test that Professional tier is included and marked as popular.""" - response = client.get("/api/v1/public/tiers") + response = client.get("/api/v1/platform/pricing/tiers") assert response.status_code == 200 data = response.json() @@ -77,7 +77,7 @@ class TestPlatformPricingAPI: def test_get_tiers_includes_enterprise(self, client): """Test that Enterprise tier is included and marked appropriately.""" - response = client.get("/api/v1/public/tiers") + response = client.get("/api/v1/platform/pricing/tiers") assert response.status_code == 200 data = response.json() @@ -108,7 +108,7 @@ class TestPlatformPricingAPI: db.add(tier) db.commit() - response = client.get("/api/v1/public/tiers") + response = client.get("/api/v1/platform/pricing/tiers") assert response.status_code == 200 data = response.json() @@ -116,12 +116,12 @@ class TestPlatformPricingAPI: assert "test_tier" in tier_codes # ========================================================================= - # GET /api/v1/public/tiers/{tier_code} + # GET /api/v1/platform/pricing/tiers/{tier_code} # ========================================================================= def test_get_tier_by_code_success(self, client): """Test getting a specific tier by code.""" - response = client.get(f"/api/v1/public/tiers/{TierCode.PROFESSIONAL.value}") + response = client.get(f"/api/v1/platform/pricing/tiers/{TierCode.PROFESSIONAL.value}") assert response.status_code == 200 data = response.json() @@ -130,7 +130,7 @@ class TestPlatformPricingAPI: def test_get_tier_by_code_essential(self, client): """Test getting Essential tier details.""" - response = client.get(f"/api/v1/public/tiers/{TierCode.ESSENTIAL.value}") + response = client.get(f"/api/v1/platform/pricing/tiers/{TierCode.ESSENTIAL.value}") assert response.status_code == 200 data = response.json() @@ -139,19 +139,19 @@ class TestPlatformPricingAPI: def test_get_tier_by_code_not_found(self, client): """Test getting a non-existent tier returns 404.""" - response = client.get("/api/v1/public/tiers/nonexistent_tier") + response = client.get("/api/v1/platform/pricing/tiers/nonexistent_tier") assert response.status_code == 404 data = response.json() assert "not found" in data["message"].lower() # ========================================================================= - # GET /api/v1/public/addons + # GET /api/v1/platform/pricing/addons # ========================================================================= def test_get_addons_empty_when_none_configured(self, client): """Test getting add-ons when none are configured.""" - response = client.get("/api/v1/public/addons") + response = client.get("/api/v1/platform/pricing/addons") assert response.status_code == 200 data = response.json() @@ -173,7 +173,7 @@ class TestPlatformPricingAPI: db.add(addon) db.commit() - response = client.get("/api/v1/public/addons") + response = client.get("/api/v1/platform/pricing/addons") assert response.status_code == 200 data = response.json() @@ -197,7 +197,7 @@ class TestPlatformPricingAPI: db.add(addon) db.commit() - response = client.get("/api/v1/public/addons") + response = client.get("/api/v1/platform/pricing/addons") assert response.status_code == 200 data = response.json() @@ -236,7 +236,7 @@ class TestPlatformPricingAPI: db.add_all([active_addon, inactive_addon]) db.commit() - response = client.get("/api/v1/public/addons") + response = client.get("/api/v1/platform/pricing/addons") assert response.status_code == 200 data = response.json() @@ -245,12 +245,12 @@ class TestPlatformPricingAPI: assert "inactive_addon" not in addon_codes # ========================================================================= - # GET /api/v1/public/pricing + # GET /api/v1/platform/pricing # ========================================================================= def test_get_pricing_returns_complete_info(self, client): """Test getting complete pricing information.""" - response = client.get("/api/v1/public/pricing") + response = client.get("/api/v1/platform/pricing") assert response.status_code == 200 data = response.json() @@ -262,7 +262,7 @@ class TestPlatformPricingAPI: def test_get_pricing_includes_trial_days(self, client): """Test that pricing includes correct trial period.""" - response = client.get("/api/v1/public/pricing") + response = client.get("/api/v1/platform/pricing") assert response.status_code == 200 data = response.json() @@ -270,7 +270,7 @@ class TestPlatformPricingAPI: def test_get_pricing_includes_annual_discount(self, client): """Test that pricing includes annual discount info.""" - response = client.get("/api/v1/public/pricing") + response = client.get("/api/v1/platform/pricing") assert response.status_code == 200 data = response.json() @@ -278,7 +278,7 @@ class TestPlatformPricingAPI: def test_get_pricing_tiers_not_empty(self, client): """Test that pricing always includes tiers.""" - response = client.get("/api/v1/public/pricing") + response = client.get("/api/v1/platform/pricing") assert response.status_code == 200 data = response.json() diff --git a/tests/integration/api/v1/platform/test_signup.py b/tests/integration/api/v1/platform/test_signup.py index 6883e45b..1757105c 100644 --- a/tests/integration/api/v1/platform/test_signup.py +++ b/tests/integration/api/v1/platform/test_signup.py @@ -1,7 +1,7 @@ -# tests/integration/api/v1/public/test_signup.py +# tests/integration/api/v1/platform/test_signup.py """Integration tests for platform signup API endpoints. -Tests the /api/v1/public/signup/* endpoints. +Tests the /api/v1/platform/signup/* endpoints. """ from unittest.mock import MagicMock, patch @@ -37,7 +37,7 @@ def mock_stripe_service(): def signup_session(client): """Create a signup session for testing.""" response = client.post( - "/api/v1/public/signup/start", + "/api/v1/platform/signup/start", json={"tier_code": TierCode.PROFESSIONAL.value, "is_annual": False}, ) return response.json()["session_id"] @@ -104,12 +104,12 @@ def claimed_letzshop_vendor(db, claimed_owner_user): @pytest.mark.api @pytest.mark.platform class TestSignupStartAPI: - """Test signup start endpoint at /api/v1/public/signup/start.""" + """Test signup start endpoint at /api/v1/platform/signup/start.""" def test_start_signup_success(self, client): """Test starting a signup session.""" response = client.post( - "/api/v1/public/signup/start", + "/api/v1/platform/signup/start", json={"tier_code": TierCode.ESSENTIAL.value, "is_annual": False}, ) @@ -122,7 +122,7 @@ class TestSignupStartAPI: def test_start_signup_with_annual_billing(self, client): """Test starting signup with annual billing.""" response = client.post( - "/api/v1/public/signup/start", + "/api/v1/platform/signup/start", json={"tier_code": TierCode.PROFESSIONAL.value, "is_annual": True}, ) @@ -134,7 +134,7 @@ class TestSignupStartAPI: """Test starting signup for all valid tiers.""" for tier in [TierCode.ESSENTIAL, TierCode.PROFESSIONAL, TierCode.BUSINESS, TierCode.ENTERPRISE]: response = client.post( - "/api/v1/public/signup/start", + "/api/v1/platform/signup/start", json={"tier_code": tier.value, "is_annual": False}, ) assert response.status_code == 200 @@ -143,7 +143,7 @@ class TestSignupStartAPI: def test_start_signup_invalid_tier(self, client): """Test starting signup with invalid tier code.""" response = client.post( - "/api/v1/public/signup/start", + "/api/v1/platform/signup/start", json={"tier_code": "invalid_tier", "is_annual": False}, ) @@ -154,11 +154,11 @@ class TestSignupStartAPI: def test_start_signup_session_id_is_unique(self, client): """Test that each signup session gets a unique ID.""" response1 = client.post( - "/api/v1/public/signup/start", + "/api/v1/platform/signup/start", json={"tier_code": TierCode.ESSENTIAL.value, "is_annual": False}, ) response2 = client.post( - "/api/v1/public/signup/start", + "/api/v1/platform/signup/start", json={"tier_code": TierCode.ESSENTIAL.value, "is_annual": False}, ) @@ -169,12 +169,12 @@ class TestSignupStartAPI: @pytest.mark.api @pytest.mark.platform class TestClaimVendorAPI: - """Test claim vendor endpoint at /api/v1/public/signup/claim-vendor.""" + """Test claim vendor endpoint at /api/v1/platform/signup/claim-vendor.""" def test_claim_vendor_success(self, client, signup_session): """Test claiming a Letzshop vendor.""" response = client.post( - "/api/v1/public/signup/claim-vendor", + "/api/v1/platform/signup/claim-vendor", json={ "session_id": signup_session, "letzshop_slug": "my-new-shop", @@ -190,7 +190,7 @@ class TestClaimVendorAPI: def test_claim_vendor_with_vendor_id(self, client, signup_session): """Test claiming vendor with Letzshop vendor ID.""" response = client.post( - "/api/v1/public/signup/claim-vendor", + "/api/v1/platform/signup/claim-vendor", json={ "session_id": signup_session, "letzshop_slug": "my-shop", @@ -205,7 +205,7 @@ class TestClaimVendorAPI: def test_claim_vendor_invalid_session(self, client): """Test claiming vendor with invalid session.""" response = client.post( - "/api/v1/public/signup/claim-vendor", + "/api/v1/platform/signup/claim-vendor", json={ "session_id": "invalid_session_id", "letzshop_slug": "my-shop", @@ -219,7 +219,7 @@ class TestClaimVendorAPI: def test_claim_vendor_already_claimed(self, client, signup_session, claimed_letzshop_vendor): """Test claiming a vendor that's already claimed.""" response = client.post( - "/api/v1/public/signup/claim-vendor", + "/api/v1/platform/signup/claim-vendor", json={ "session_id": signup_session, "letzshop_slug": "already-claimed-shop", @@ -235,12 +235,12 @@ class TestClaimVendorAPI: @pytest.mark.api @pytest.mark.platform class TestCreateAccountAPI: - """Test create account endpoint at /api/v1/public/signup/create-account.""" + """Test create account endpoint at /api/v1/platform/signup/create-account.""" def test_create_account_success(self, client, signup_session, mock_stripe_service): """Test creating an account.""" response = client.post( - "/api/v1/public/signup/create-account", + "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "newuser@example.com", @@ -261,7 +261,7 @@ class TestCreateAccountAPI: def test_create_account_with_phone(self, client, signup_session, mock_stripe_service): """Test creating an account with phone number.""" response = client.post( - "/api/v1/public/signup/create-account", + "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "user2@example.com", @@ -280,7 +280,7 @@ class TestCreateAccountAPI: def test_create_account_invalid_session(self, client, mock_stripe_service): """Test creating account with invalid session.""" response = client.post( - "/api/v1/public/signup/create-account", + "/api/v1/platform/signup/create-account", json={ "session_id": "invalid_session", "email": "test@example.com", @@ -298,7 +298,7 @@ class TestCreateAccountAPI: ): """Test creating account with existing email.""" response = client.post( - "/api/v1/public/signup/create-account", + "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "existing@example.com", @@ -316,7 +316,7 @@ class TestCreateAccountAPI: def test_create_account_invalid_email(self, client, signup_session): """Test creating account with invalid email format.""" response = client.post( - "/api/v1/public/signup/create-account", + "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "not-an-email", @@ -333,14 +333,14 @@ class TestCreateAccountAPI: """Test creating account after claiming Letzshop vendor.""" # Start signup start_response = client.post( - "/api/v1/public/signup/start", + "/api/v1/platform/signup/start", json={"tier_code": TierCode.PROFESSIONAL.value, "is_annual": False}, ) session_id = start_response.json()["session_id"] # Claim vendor client.post( - "/api/v1/public/signup/claim-vendor", + "/api/v1/platform/signup/claim-vendor", json={ "session_id": session_id, "letzshop_slug": "my-shop-claim", @@ -349,7 +349,7 @@ class TestCreateAccountAPI: # Create account response = client.post( - "/api/v1/public/signup/create-account", + "/api/v1/platform/signup/create-account", json={ "session_id": session_id, "email": "shop@example.com", @@ -369,13 +369,13 @@ class TestCreateAccountAPI: @pytest.mark.api @pytest.mark.platform class TestSetupPaymentAPI: - """Test setup payment endpoint at /api/v1/public/signup/setup-payment.""" + """Test setup payment endpoint at /api/v1/platform/signup/setup-payment.""" def test_setup_payment_success(self, client, signup_session, mock_stripe_service): """Test setting up payment after account creation.""" # Create account first client.post( - "/api/v1/public/signup/create-account", + "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "payment@example.com", @@ -388,7 +388,7 @@ class TestSetupPaymentAPI: # Setup payment response = client.post( - "/api/v1/public/signup/setup-payment", + "/api/v1/platform/signup/setup-payment", json={"session_id": signup_session}, ) @@ -401,7 +401,7 @@ class TestSetupPaymentAPI: def test_setup_payment_invalid_session(self, client, mock_stripe_service): """Test setup payment with invalid session.""" response = client.post( - "/api/v1/public/signup/setup-payment", + "/api/v1/platform/signup/setup-payment", json={"session_id": "invalid_session"}, ) @@ -410,7 +410,7 @@ class TestSetupPaymentAPI: def test_setup_payment_without_account(self, client, signup_session, mock_stripe_service): """Test setup payment without creating account first.""" response = client.post( - "/api/v1/public/signup/setup-payment", + "/api/v1/platform/signup/setup-payment", json={"session_id": signup_session}, ) @@ -423,13 +423,13 @@ class TestSetupPaymentAPI: @pytest.mark.api @pytest.mark.platform class TestCompleteSignupAPI: - """Test complete signup endpoint at /api/v1/public/signup/complete.""" + """Test complete signup endpoint at /api/v1/platform/signup/complete.""" def test_complete_signup_success(self, client, signup_session, mock_stripe_service, db): """Test completing signup after payment setup.""" # Create account client.post( - "/api/v1/public/signup/create-account", + "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "complete@example.com", @@ -442,13 +442,13 @@ class TestCompleteSignupAPI: # Setup payment client.post( - "/api/v1/public/signup/setup-payment", + "/api/v1/platform/signup/setup-payment", json={"session_id": signup_session}, ) # Complete signup response = client.post( - "/api/v1/public/signup/complete", + "/api/v1/platform/signup/complete", json={ "session_id": signup_session, "setup_intent_id": "seti_test_123", @@ -469,7 +469,7 @@ class TestCompleteSignupAPI: """Test that completing signup returns a valid JWT access token for auto-login.""" # Create account client.post( - "/api/v1/public/signup/create-account", + "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "token_test@example.com", @@ -482,13 +482,13 @@ class TestCompleteSignupAPI: # Setup payment client.post( - "/api/v1/public/signup/setup-payment", + "/api/v1/platform/signup/setup-payment", json={"session_id": signup_session}, ) # Complete signup response = client.post( - "/api/v1/public/signup/complete", + "/api/v1/platform/signup/complete", json={ "session_id": signup_session, "setup_intent_id": "seti_test_123", @@ -509,7 +509,7 @@ class TestCompleteSignupAPI: """Test that the returned access token can be used to authenticate API calls.""" # Create account client.post( - "/api/v1/public/signup/create-account", + "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "auth_test@example.com", @@ -522,13 +522,13 @@ class TestCompleteSignupAPI: # Setup payment client.post( - "/api/v1/public/signup/setup-payment", + "/api/v1/platform/signup/setup-payment", json={"session_id": signup_session}, ) # Complete signup complete_response = client.post( - "/api/v1/public/signup/complete", + "/api/v1/platform/signup/complete", json={ "session_id": signup_session, "setup_intent_id": "seti_test_123", @@ -553,7 +553,7 @@ class TestCompleteSignupAPI: """Test that completing signup sets the vendor_token HTTP-only cookie.""" # Create account client.post( - "/api/v1/public/signup/create-account", + "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "cookie_test@example.com", @@ -566,13 +566,13 @@ class TestCompleteSignupAPI: # Setup payment client.post( - "/api/v1/public/signup/setup-payment", + "/api/v1/platform/signup/setup-payment", json={"session_id": signup_session}, ) # Complete signup response = client.post( - "/api/v1/public/signup/complete", + "/api/v1/platform/signup/complete", json={ "session_id": signup_session, "setup_intent_id": "seti_test_123", @@ -588,7 +588,7 @@ class TestCompleteSignupAPI: def test_complete_signup_invalid_session(self, client, mock_stripe_service): """Test completing signup with invalid session.""" response = client.post( - "/api/v1/public/signup/complete", + "/api/v1/platform/signup/complete", json={ "session_id": "invalid_session", "setup_intent_id": "seti_test_123", @@ -603,7 +603,7 @@ class TestCompleteSignupAPI: """Test completing signup when payment setup failed.""" # Create account client.post( - "/api/v1/public/signup/create-account", + "/api/v1/platform/signup/create-account", json={ "session_id": signup_session, "email": "fail@example.com", @@ -616,7 +616,7 @@ class TestCompleteSignupAPI: # Setup payment client.post( - "/api/v1/public/signup/setup-payment", + "/api/v1/platform/signup/setup-payment", json={"session_id": signup_session}, ) @@ -629,7 +629,7 @@ class TestCompleteSignupAPI: # Complete signup response = client.post( - "/api/v1/public/signup/complete", + "/api/v1/platform/signup/complete", json={ "session_id": signup_session, "setup_intent_id": "seti_failed", @@ -645,11 +645,11 @@ class TestCompleteSignupAPI: @pytest.mark.api @pytest.mark.platform class TestGetSignupSessionAPI: - """Test get signup session endpoint at /api/v1/public/signup/session/{session_id}.""" + """Test get signup session endpoint at /api/v1/platform/signup/session/{session_id}.""" def test_get_session_after_start(self, client, signup_session): """Test getting session after starting signup.""" - response = client.get(f"/api/v1/public/signup/session/{signup_session}") + response = client.get(f"/api/v1/platform/signup/session/{signup_session}") assert response.status_code == 200 data = response.json() @@ -661,14 +661,14 @@ class TestGetSignupSessionAPI: def test_get_session_after_claim(self, client, signup_session): """Test getting session after claiming vendor.""" client.post( - "/api/v1/public/signup/claim-vendor", + "/api/v1/platform/signup/claim-vendor", json={ "session_id": signup_session, "letzshop_slug": "my-session-shop", }, ) - response = client.get(f"/api/v1/public/signup/session/{signup_session}") + response = client.get(f"/api/v1/platform/signup/session/{signup_session}") assert response.status_code == 200 data = response.json() @@ -677,7 +677,7 @@ class TestGetSignupSessionAPI: def test_get_session_invalid_id(self, client): """Test getting non-existent session.""" - response = client.get("/api/v1/public/signup/session/invalid_id") + response = client.get("/api/v1/platform/signup/session/invalid_id") assert response.status_code == 404 @@ -692,7 +692,7 @@ class TestSignupFullFlow: """Test the complete signup flow from start to finish.""" # Step 1: Start signup start_response = client.post( - "/api/v1/public/signup/start", + "/api/v1/platform/signup/start", json={"tier_code": TierCode.BUSINESS.value, "is_annual": True}, ) assert start_response.status_code == 200 @@ -700,7 +700,7 @@ class TestSignupFullFlow: # Step 2: Claim Letzshop vendor (optional) claim_response = client.post( - "/api/v1/public/signup/claim-vendor", + "/api/v1/platform/signup/claim-vendor", json={ "session_id": session_id, "letzshop_slug": "full-flow-shop", @@ -710,7 +710,7 @@ class TestSignupFullFlow: # Step 3: Create account account_response = client.post( - "/api/v1/public/signup/create-account", + "/api/v1/platform/signup/create-account", json={ "session_id": session_id, "email": "fullflow@example.com", @@ -726,7 +726,7 @@ class TestSignupFullFlow: # Step 4: Setup payment payment_response = client.post( - "/api/v1/public/signup/setup-payment", + "/api/v1/platform/signup/setup-payment", json={"session_id": session_id}, ) assert payment_response.status_code == 200 @@ -734,7 +734,7 @@ class TestSignupFullFlow: # Step 5: Complete signup complete_response = client.post( - "/api/v1/public/signup/complete", + "/api/v1/platform/signup/complete", json={ "session_id": session_id, "setup_intent_id": "seti_test_123", @@ -753,14 +753,14 @@ class TestSignupFullFlow: """Test signup flow skipping Letzshop claim step.""" # Step 1: Start signup start_response = client.post( - "/api/v1/public/signup/start", + "/api/v1/platform/signup/start", json={"tier_code": TierCode.ESSENTIAL.value, "is_annual": False}, ) session_id = start_response.json()["session_id"] # Skip Step 2, go directly to Step 3 account_response = client.post( - "/api/v1/public/signup/create-account", + "/api/v1/platform/signup/create-account", json={ "session_id": session_id, "email": "noletzshop@example.com", @@ -775,12 +775,12 @@ class TestSignupFullFlow: # Step 4 & 5: Payment and complete client.post( - "/api/v1/public/signup/setup-payment", + "/api/v1/platform/signup/setup-payment", json={"session_id": session_id}, ) complete_response = client.post( - "/api/v1/public/signup/complete", + "/api/v1/platform/signup/complete", json={ "session_id": session_id, "setup_intent_id": "seti_test_123",