feat: consolidate media service, add merchant users page, fix metrics overlap

- Merge ImageService into MediaService with WebP variant generation,
  DB-backed storage stats, and module-driven media usage discovery
  via new MediaUsageProviderProtocol
- Add merchant users admin page with scoped user listing, stats
  endpoint, template, JS, and i18n strings (de/en/fr/lb)
- Fix merchant user metrics so Owners and Team Members are mutually
  exclusive (filter team_members on user_type="member" and exclude
  owner IDs) ensuring stat cards add up correctly
- Update billing and monitoring services to use media_service
- Update subscription-billing and feature-gating docs

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-07 21:17:11 +01:00
parent 4cb2bda575
commit 2250054ba2
30 changed files with 1220 additions and 805 deletions

View File

@@ -43,20 +43,17 @@ from app.modules.cms.schemas.media import (
MediaListResponse,
MediaMetadataUpdate,
MediaUploadResponse,
MediaUsageItem,
MediaUsageResponse,
MessageResponse,
MultipleUploadResponse,
OptimizationResultResponse,
ProductUsageInfo,
UploadedFileInfo,
)
# Image schemas
from app.modules.cms.schemas.image import (
ImageDeleteResponse,
ImageStorageStats,
ImageUploadResponse,
ImageUrls,
)
# Theme schemas
@@ -109,13 +106,10 @@ __all__ = [
"MessageResponse",
"MultipleUploadResponse",
"OptimizationResultResponse",
"ProductUsageInfo",
"MediaUsageItem",
"UploadedFileInfo",
# Image
"ImageDeleteResponse",
"ImageStorageStats",
"ImageUploadResponse",
"ImageUrls",
# Theme
"ThemeDeleteResponse",
"ThemePresetListResponse",

View File

@@ -6,33 +6,6 @@ Pydantic schemas for image operations.
from pydantic import BaseModel
class ImageUrls(BaseModel):
"""URLs for image variants."""
original: str
medium: str | None = None # 800px variant
thumb: str | None = None # 200px variant
# Allow arbitrary keys for flexibility
class Config:
extra = "allow"
class ImageUploadResponse(BaseModel):
"""Response from image upload."""
success: bool
image: dict | None = None
error: str | None = None
class ImageDeleteResponse(BaseModel):
"""Response from image deletion."""
success: bool
message: str
class ImageStorageStats(BaseModel):
"""Image storage statistics."""

View File

@@ -164,22 +164,21 @@ class MediaMetadataUpdate(BaseModel):
# ============================================================================
class ProductUsageInfo(BaseModel):
"""Information about product using this media."""
class MediaUsageItem(BaseModel):
"""Information about an entity using this media."""
product_id: int
product_name: str
usage_type: str # main_image, gallery, variant, etc.
entity_type: str # Defined by provider (e.g. "product")
entity_id: int
entity_name: str
usage_type: str # Defined by provider (e.g. "main_image", "gallery")
class MediaUsageResponse(BaseModel):
"""Response showing where media is being used."""
media_id: int | None = None
products: list[ProductUsageInfo] = []
other_usage: list[dict[str, Any]] = []
usage: list[MediaUsageItem] = []
total_usage_count: int = 0
message: str | None = None
# ============================================================================