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 <noreply@anthropic.com>
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# app/api/v1/public/signup.py
|
||||
# app/api/v1/platform/signup.py
|
||||
"""
|
||||
Platform signup API endpoints.
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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({
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
|
||||
@@ -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 %}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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 })
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
|
||||
@@ -135,6 +135,6 @@
|
||||
</script>
|
||||
|
||||
<!-- 6. Login Logic -->
|
||||
<script src="{{ url_for('tenancy_static', path='admin/js/login.js') }}"></script>
|
||||
<script src="{{ url_for('core_static', path='admin/js/login.js') }}"></script>
|
||||
</body>
|
||||
</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.) #}
|
||||
<!DOCTYPE html>
|
||||
<html lang="{{ current_language|default('en') }}" x-data="platformLayoutData()" x-bind:class="{ 'dark': dark }">
|
||||
<head>
|
||||
@@ -42,7 +42,7 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
||||
|
||||
{# Tailwind CSS v4 (built locally via standalone CLI) #}
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='public/css/tailwind.output.css') }}">
|
||||
<link rel="stylesheet" href="{{ url_for('static', path='platform/css/tailwind.output.css') }}">
|
||||
|
||||
{# Flag icons for language selector #}
|
||||
<link rel="stylesheet" href="https://cdn.jsdelivr.net/gh/lipis/flag-icons@7.0.0/css/flag-icons.min.css"/>
|
||||
|
||||
@@ -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": "..."}
|
||||
```
|
||||
|
||||
|
||||
@@ -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"}'
|
||||
|
||||
|
||||
@@ -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 }
|
||||
▼
|
||||
┌─────────────────────────────┐
|
||||
|
||||
@@ -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):**
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
↓
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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}'
|
||||
```
|
||||
|
||||
653
docs/proposals/plan-perms.md
Normal file
653
docs/proposals/plan-perms.md
Normal file
@@ -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
|
||||
@@ -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
|
||||
|
||||
18
main.py
18
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,
|
||||
)
|
||||
|
||||
@@ -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"},
|
||||
)
|
||||
|
||||
|
||||
@@ -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
|
||||
```
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
"""
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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",
|
||||
|
||||
Reference in New Issue
Block a user