feat: complete analytics module self-containment

Migrate analytics module to fully self-contained structure:

- routes/api/vendor.py - API endpoints
- routes/pages/vendor.py - Page routes with full implementation
- services/stats_service.py - Business logic (moved from app/services)
- services/usage_service.py - Usage tracking (moved from app/services)
- schemas/stats.py - Pydantic schemas (moved from models/schema)
- models/__init__.py - Model exports
- templates/analytics/vendor/ - Templates (moved from app/templates)
- static/vendor/js/ - JavaScript (moved from static/vendor)
- locales/ - Translations (en, de, fr, lu)
- exceptions.py - Module exceptions

Removed legacy files:
- app/modules/analytics/routes/vendor.py (replaced by routes/pages/)
- static/admin/js/analytics.js (unused)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-28 22:21:21 +01:00
parent 2466dfd7ed
commit bd2c99a775
22 changed files with 1870 additions and 50 deletions

View File

@@ -6,8 +6,9 @@ This module provides functions to register analytics routes
with module-based access control.
NOTE: Routers are NOT auto-imported to avoid circular dependencies.
Import directly from vendor.py as needed:
from app.modules.analytics.routes.vendor import vendor_router
Import directly from api/ or pages/ as needed:
from app.modules.analytics.routes.api import vendor_router as vendor_api_router
from app.modules.analytics.routes.pages import vendor_router as vendor_page_router
Note: Analytics module has no admin routes - admin uses dashboard.
"""
@@ -15,12 +16,15 @@ Note: Analytics module has no admin routes - admin uses dashboard.
# Routers are imported on-demand to avoid circular dependencies
# Do NOT add auto-imports here
__all__ = ["vendor_router"]
__all__ = ["vendor_api_router", "vendor_page_router"]
def __getattr__(name: str):
"""Lazy import routers to avoid circular dependencies."""
if name == "vendor_router":
from app.modules.analytics.routes.vendor import vendor_router
if name == "vendor_api_router":
from app.modules.analytics.routes.api import vendor_router
return vendor_router
elif name == "vendor_page_router":
from app.modules.analytics.routes.pages import vendor_router
return vendor_router
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

View File

@@ -0,0 +1,11 @@
# app/modules/analytics/routes/api/__init__.py
"""
Analytics module API routes.
Provides REST API endpoints for analytics and reporting:
- Vendor API: Vendor-scoped analytics data
"""
from app.modules.analytics.routes.api.vendor import router as vendor_router
__all__ = ["vendor_router"]

View File

@@ -0,0 +1,56 @@
# app/modules/analytics/routes/api/vendor.py
"""
Vendor Analytics API
Vendor Context: Uses token_vendor_id from JWT token (authenticated vendor API pattern).
The get_current_vendor_api dependency guarantees token_vendor_id is present.
Feature Requirements:
- basic_reports: Basic analytics (Essential tier)
- analytics_dashboard: Advanced analytics (Business tier)
"""
import logging
from fastapi import APIRouter, Depends, Query
from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_api, get_db, require_module_access
from app.core.feature_gate import RequireFeature
from app.modules.analytics.services import stats_service
from app.modules.analytics.schemas import (
VendorAnalyticsCatalog,
VendorAnalyticsImports,
VendorAnalyticsInventory,
VendorAnalyticsResponse,
)
from models.database.feature import FeatureCode
from models.database.user import User
router = APIRouter(
dependencies=[Depends(require_module_access("analytics"))],
)
logger = logging.getLogger(__name__)
@router.get("", response_model=VendorAnalyticsResponse)
def get_vendor_analytics(
period: str = Query("30d", description="Time period: 7d, 30d, 90d, 1y"),
current_user: User = Depends(get_current_vendor_api),
db: Session = Depends(get_db),
_: None = Depends(RequireFeature(FeatureCode.BASIC_REPORTS, FeatureCode.ANALYTICS_DASHBOARD)),
):
"""Get vendor analytics data for specified time period."""
data = stats_service.get_vendor_analytics(db, current_user.token_vendor_id, period)
return VendorAnalyticsResponse(
period=data["period"],
start_date=data["start_date"],
imports=VendorAnalyticsImports(count=data["imports"]["count"]),
catalog=VendorAnalyticsCatalog(
products_added=data["catalog"]["products_added"]
),
inventory=VendorAnalyticsInventory(
total_locations=data["inventory"]["total_locations"]
),
)

View File

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

View File

@@ -0,0 +1,90 @@
# app/modules/analytics/routes/pages/vendor.py
"""
Analytics Vendor Page Routes (HTML rendering).
Vendor pages for analytics dashboard.
"""
import logging
from fastapi import APIRouter, Depends, Path, Request
from fastapi.responses import HTMLResponse
from sqlalchemy.orm import Session
from app.api.deps import get_current_vendor_from_cookie_or_header, get_db
from app.services.platform_settings_service import platform_settings_service
from app.templates_config import templates
from models.database.user import User
from models.database.vendor import Vendor
logger = logging.getLogger(__name__)
router = APIRouter()
# ============================================================================
# HELPER: Build Vendor Dashboard Context
# ============================================================================
def get_vendor_context(
request: Request,
db: Session,
current_user: User,
vendor_code: str,
**extra_context,
) -> dict:
"""
Build template context for vendor dashboard pages.
Resolves locale/currency using the platform settings service with
vendor override support.
"""
# Load vendor from database
vendor = db.query(Vendor).filter(Vendor.subdomain == vendor_code).first()
# Get platform defaults
platform_config = platform_settings_service.get_storefront_config(db)
# Resolve with vendor override
storefront_locale = platform_config["locale"]
storefront_currency = platform_config["currency"]
if vendor and vendor.storefront_locale:
storefront_locale = vendor.storefront_locale
context = {
"request": request,
"user": current_user,
"vendor": vendor,
"vendor_code": vendor_code,
"storefront_locale": storefront_locale,
"storefront_currency": storefront_currency,
**extra_context,
}
return context
# ============================================================================
# ANALYTICS PAGE
# ============================================================================
@router.get(
"/{vendor_code}/analytics", response_class=HTMLResponse, include_in_schema=False
)
async def vendor_analytics_page(
request: Request,
vendor_code: str = Path(..., description="Vendor code"),
current_user: User = Depends(get_current_vendor_from_cookie_or_header),
db: Session = Depends(get_db),
):
"""
Render analytics and reports page.
JavaScript loads analytics data via API.
"""
return templates.TemplateResponse(
"analytics/vendor/analytics.html",
get_vendor_context(request, db, current_user, vendor_code),
)

View File

@@ -1,25 +0,0 @@
# app/modules/analytics/routes/vendor.py
"""
Analytics module vendor routes.
This module wraps the existing vendor analytics routes and adds
module-based access control. Routes are re-exported from the
original location with the module access dependency.
"""
from fastapi import APIRouter, Depends
from app.api.deps import require_module_access
# Import original router (direct import to avoid circular dependency)
from app.api.v1.vendor.analytics import router as original_router
# Create module-aware router
vendor_router = APIRouter(
prefix="/analytics",
dependencies=[Depends(require_module_access("analytics"))],
)
# Re-export all routes from the original module with module access control
for route in original_router.routes:
vendor_router.routes.append(route)