feat: add proper Pydantic response_model to all stats endpoints
- Create comprehensive stats schemas in models/schema/stats.py: - ImportStatsResponse, UserStatsResponse, ProductStatsResponse - PlatformStatsResponse, AdminDashboardResponse - VendorDashboardStatsResponse with nested models - VendorAnalyticsResponse, CodeQualityDashboardStatsResponse - Move DashboardStatsResponse from code_quality.py to schema file - Fix get_vendor_statistics() to return pending_vendors field - Fix get_vendor_stats() to return flat structure matching schema - Add response_model to all stats endpoints: - GET /admin/dashboard -> AdminDashboardResponse - GET /admin/dashboard/stats/platform -> PlatformStatsResponse - GET /admin/marketplace-import-jobs/stats -> ImportStatsResponse - GET /vendor/dashboard/stats -> VendorDashboardStatsResponse - GET /vendor/analytics -> VendorAnalyticsResponse - Enhance API-001 architecture rule with detailed guidance - Add SVC-007 rule for service/schema compatibility 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -41,16 +41,51 @@ api_endpoint_rules:
|
|||||||
description: |
|
description: |
|
||||||
All API endpoints must use Pydantic models (BaseModel) for request bodies
|
All API endpoints must use Pydantic models (BaseModel) for request bodies
|
||||||
and response models. Never use raw dicts or SQLAlchemy models directly.
|
and response models. Never use raw dicts or SQLAlchemy models directly.
|
||||||
|
|
||||||
|
WHY THIS MATTERS:
|
||||||
|
- Type safety: Pydantic validates response structure at runtime
|
||||||
|
- Documentation: OpenAPI/Swagger auto-generates accurate docs
|
||||||
|
- Contract stability: Schema changes are explicit and reviewable
|
||||||
|
- IDE support: Consumers get autocomplete and type hints
|
||||||
|
- Prevents bugs: Field name mismatches caught immediately
|
||||||
|
|
||||||
|
COMMON VIOLATION: Service returns dict, frontend expects different field names.
|
||||||
|
Example: Service returns {"total_imports": 5} but frontend expects {"total": 5}.
|
||||||
|
With response_model, this mismatch is caught immediately.
|
||||||
|
|
||||||
|
SCHEMA LOCATION: All response schemas must be defined in models/schema/*.py,
|
||||||
|
never inline in endpoint files. This ensures schemas are reusable and discoverable.
|
||||||
pattern:
|
pattern:
|
||||||
file_pattern: "app/api/v1/**/*.py"
|
file_pattern: "app/api/v1/**/*.py"
|
||||||
anti_patterns:
|
anti_patterns:
|
||||||
- "return dict"
|
- "return dict"
|
||||||
- "-> dict"
|
- "-> dict"
|
||||||
- "return db_object"
|
- "return db_object"
|
||||||
|
- "return {" # Returning inline dict literal
|
||||||
example_good: |
|
example_good: |
|
||||||
@router.post("/vendors", response_model=VendorResponse)
|
# In models/schema/stats.py
|
||||||
async def create_vendor(vendor: VendorCreate):
|
class ImportStatsResponse(BaseModel):
|
||||||
return vendor_service.create_vendor(db, vendor)
|
total: int
|
||||||
|
pending: int
|
||||||
|
completed: int
|
||||||
|
failed: int
|
||||||
|
|
||||||
|
# In app/api/v1/admin/marketplace.py
|
||||||
|
@router.get("/stats", response_model=ImportStatsResponse)
|
||||||
|
def get_import_statistics(db: Session = Depends(get_db)):
|
||||||
|
return stats_service.get_import_statistics(db)
|
||||||
|
example_bad: |
|
||||||
|
# ❌ WRONG: No response_model, returns raw dict
|
||||||
|
@router.get("/stats")
|
||||||
|
def get_import_statistics(db: Session = Depends(get_db)):
|
||||||
|
return stats_service.get_import_statistics(db) # Returns dict
|
||||||
|
|
||||||
|
# ❌ WRONG: Schema defined inline in endpoint file
|
||||||
|
class MyResponse(BaseModel): # Should be in models/schema/
|
||||||
|
...
|
||||||
|
|
||||||
|
@router.get("/data", response_model=MyResponse)
|
||||||
|
def get_data(): ...
|
||||||
|
|
||||||
- id: "API-002"
|
- id: "API-002"
|
||||||
name: "Endpoint must NOT contain business logic"
|
name: "Endpoint must NOT contain business logic"
|
||||||
@@ -238,6 +273,42 @@ service_layer_rules:
|
|||||||
exceptions:
|
exceptions:
|
||||||
- "log_service.py" # Audit logs may need immediate commits
|
- "log_service.py" # Audit logs may need immediate commits
|
||||||
|
|
||||||
|
- id: "SVC-007"
|
||||||
|
name: "Service return types must match API response schemas"
|
||||||
|
severity: "error"
|
||||||
|
description: |
|
||||||
|
When a service method's return value will be used as an API response,
|
||||||
|
the returned dict keys MUST match the corresponding Pydantic schema fields.
|
||||||
|
|
||||||
|
This prevents the common bug where:
|
||||||
|
- Service returns {"total_imports": 5, "completed_imports": 3}
|
||||||
|
- Schema expects {"total": 5, "completed": 3}
|
||||||
|
- Frontend receives wrong/empty values
|
||||||
|
|
||||||
|
RECOMMENDED PATTERNS:
|
||||||
|
|
||||||
|
1. Return Pydantic model directly from service:
|
||||||
|
def get_stats(self, db: Session) -> StatsResponse:
|
||||||
|
return StatsResponse(total=count, completed=done)
|
||||||
|
|
||||||
|
2. Return dict with schema-matching keys:
|
||||||
|
def get_stats(self, db: Session) -> dict:
|
||||||
|
return {"total": count, "completed": done} # Matches StatsResponse
|
||||||
|
|
||||||
|
3. Document the expected schema in service docstring:
|
||||||
|
def get_stats(self, db: Session) -> dict:
|
||||||
|
\"\"\"
|
||||||
|
Returns dict compatible with StatsResponse schema.
|
||||||
|
Keys: total, pending, completed, failed
|
||||||
|
\"\"\"
|
||||||
|
|
||||||
|
TESTING: Write tests that validate service output against schema:
|
||||||
|
result = service.get_stats(db)
|
||||||
|
StatsResponse(**result) # Raises if keys don't match
|
||||||
|
pattern:
|
||||||
|
file_pattern: "app/services/**/*.py"
|
||||||
|
check: "schema_compatibility"
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# MODEL RULES (models/database/*.py, models/schema/*.py)
|
# MODEL RULES (models/database/*.py, models/schema/*.py)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ from app.core.database import get_db
|
|||||||
from app.exceptions import ViolationNotFoundException
|
from app.exceptions import ViolationNotFoundException
|
||||||
from app.services.code_quality_service import code_quality_service
|
from app.services.code_quality_service import code_quality_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
|
from models.schema.stats import CodeQualityDashboardStatsResponse
|
||||||
|
|
||||||
router = APIRouter()
|
router = APIRouter()
|
||||||
|
|
||||||
@@ -107,25 +108,6 @@ class AddCommentRequest(BaseModel):
|
|||||||
comment: str = Field(..., min_length=1, description="Comment text")
|
comment: str = Field(..., min_length=1, description="Comment text")
|
||||||
|
|
||||||
|
|
||||||
class DashboardStatsResponse(BaseModel):
|
|
||||||
"""Response model for dashboard statistics"""
|
|
||||||
|
|
||||||
total_violations: int
|
|
||||||
errors: int
|
|
||||||
warnings: int
|
|
||||||
open: int
|
|
||||||
assigned: int
|
|
||||||
resolved: int
|
|
||||||
ignored: int
|
|
||||||
technical_debt_score: int
|
|
||||||
trend: list
|
|
||||||
by_severity: dict
|
|
||||||
by_rule: dict
|
|
||||||
by_module: dict
|
|
||||||
top_files: list
|
|
||||||
last_scan: str | None = None
|
|
||||||
|
|
||||||
|
|
||||||
# API Endpoints
|
# API Endpoints
|
||||||
|
|
||||||
|
|
||||||
@@ -447,7 +429,7 @@ async def add_comment(
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stats", response_model=DashboardStatsResponse)
|
@router.get("/stats", response_model=CodeQualityDashboardStatsResponse)
|
||||||
async def get_dashboard_stats(
|
async def get_dashboard_stats(
|
||||||
db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_api)
|
db: Session = Depends(get_db), current_user: User = Depends(get_current_admin_api)
|
||||||
):
|
):
|
||||||
@@ -463,4 +445,4 @@ async def get_dashboard_stats(
|
|||||||
"""
|
"""
|
||||||
stats = code_quality_service.get_dashboard_stats(db)
|
stats = code_quality_service.get_dashboard_stats(db)
|
||||||
|
|
||||||
return DashboardStatsResponse(**stats)
|
return CodeQualityDashboardStatsResponse(**stats)
|
||||||
|
|||||||
@@ -13,28 +13,46 @@ from app.core.database import get_db
|
|||||||
from app.services.admin_service import admin_service
|
from app.services.admin_service import admin_service
|
||||||
from app.services.stats_service import stats_service
|
from app.services.stats_service import stats_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
from models.schema.stats import MarketplaceStatsResponse, StatsResponse
|
from models.schema.stats import (
|
||||||
|
AdminDashboardResponse,
|
||||||
|
ImportStatsResponse,
|
||||||
|
MarketplaceStatsResponse,
|
||||||
|
OrderStatsBasicResponse,
|
||||||
|
PlatformStatsResponse,
|
||||||
|
ProductStatsResponse,
|
||||||
|
StatsResponse,
|
||||||
|
UserStatsResponse,
|
||||||
|
VendorStatsResponse,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/dashboard")
|
router = APIRouter(prefix="/dashboard")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("", response_model=AdminDashboardResponse)
|
||||||
def get_admin_dashboard(
|
def get_admin_dashboard(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_admin: User = Depends(get_current_admin_api),
|
current_admin: User = Depends(get_current_admin_api),
|
||||||
):
|
):
|
||||||
"""Get admin dashboard with platform statistics (Admin only)."""
|
"""Get admin dashboard with platform statistics (Admin only)."""
|
||||||
return {
|
user_stats = stats_service.get_user_statistics(db)
|
||||||
"platform": {
|
vendor_stats = stats_service.get_vendor_statistics(db)
|
||||||
|
|
||||||
|
return AdminDashboardResponse(
|
||||||
|
platform={
|
||||||
"name": "Multi-Tenant Ecommerce Platform",
|
"name": "Multi-Tenant Ecommerce Platform",
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
},
|
},
|
||||||
"users": stats_service.get_user_statistics(db),
|
users=UserStatsResponse(**user_stats),
|
||||||
"vendors": stats_service.get_vendor_statistics(db),
|
vendors=VendorStatsResponse(
|
||||||
"recent_vendors": admin_service.get_recent_vendors(db, limit=5),
|
total=vendor_stats.get("total", vendor_stats.get("total_vendors", 0)),
|
||||||
"recent_imports": admin_service.get_recent_import_jobs(db, limit=10),
|
verified=vendor_stats.get("verified", vendor_stats.get("verified_vendors", 0)),
|
||||||
}
|
pending=vendor_stats.get("pending", vendor_stats.get("pending_vendors", 0)),
|
||||||
|
inactive=vendor_stats.get("inactive", vendor_stats.get("inactive_vendors", 0)),
|
||||||
|
),
|
||||||
|
recent_vendors=admin_service.get_recent_vendors(db, limit=5),
|
||||||
|
recent_imports=admin_service.get_recent_import_jobs(db, limit=10),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stats", response_model=StatsResponse)
|
@router.get("/stats", response_model=StatsResponse)
|
||||||
@@ -75,16 +93,27 @@ def get_marketplace_stats(
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stats/platform")
|
@router.get("/stats/platform", response_model=PlatformStatsResponse)
|
||||||
def get_platform_statistics(
|
def get_platform_statistics(
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
current_admin: User = Depends(get_current_admin_api),
|
current_admin: User = Depends(get_current_admin_api),
|
||||||
):
|
):
|
||||||
"""Get comprehensive platform statistics (Admin only)."""
|
"""Get comprehensive platform statistics (Admin only)."""
|
||||||
return {
|
user_stats = stats_service.get_user_statistics(db)
|
||||||
"users": stats_service.get_user_statistics(db),
|
vendor_stats = stats_service.get_vendor_statistics(db)
|
||||||
"vendors": stats_service.get_vendor_statistics(db),
|
product_stats = stats_service.get_product_statistics(db)
|
||||||
"products": stats_service.get_product_statistics(db),
|
order_stats = stats_service.get_order_statistics(db)
|
||||||
"orders": stats_service.get_order_statistics(db),
|
import_stats = stats_service.get_import_statistics(db)
|
||||||
"imports": stats_service.get_import_statistics(db),
|
|
||||||
}
|
return PlatformStatsResponse(
|
||||||
|
users=UserStatsResponse(**user_stats),
|
||||||
|
vendors=VendorStatsResponse(
|
||||||
|
total=vendor_stats.get("total", vendor_stats.get("total_vendors", 0)),
|
||||||
|
verified=vendor_stats.get("verified", vendor_stats.get("verified_vendors", 0)),
|
||||||
|
pending=vendor_stats.get("pending", vendor_stats.get("pending_vendors", 0)),
|
||||||
|
inactive=vendor_stats.get("inactive", vendor_stats.get("inactive_vendors", 0)),
|
||||||
|
),
|
||||||
|
products=ProductStatsResponse(**product_stats),
|
||||||
|
orders=OrderStatsBasicResponse(**order_stats),
|
||||||
|
imports=ImportStatsResponse(**import_stats),
|
||||||
|
)
|
||||||
|
|||||||
@@ -109,11 +109,12 @@ def get_vendor_statistics_endpoint(
|
|||||||
"""Get vendor statistics for admin dashboard (Admin only)."""
|
"""Get vendor statistics for admin dashboard (Admin only)."""
|
||||||
stats = stats_service.get_vendor_statistics(db)
|
stats = stats_service.get_vendor_statistics(db)
|
||||||
|
|
||||||
|
# Use schema-compatible keys (with fallback to legacy keys)
|
||||||
return VendorStatsResponse(
|
return VendorStatsResponse(
|
||||||
total=stats.get("total_vendors", 0),
|
total=stats.get("total", stats.get("total_vendors", 0)),
|
||||||
verified=stats.get("verified_vendors", 0),
|
verified=stats.get("verified", stats.get("verified_vendors", 0)),
|
||||||
pending=stats.get("pending_vendors", 0),
|
pending=stats.get("pending", stats.get("pending_vendors", 0)),
|
||||||
inactive=stats.get("inactive_vendors", 0),
|
inactive=stats.get("inactive", stats.get("inactive_vendors", 0)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
20
app/api/v1/vendor/analytics.py
vendored
20
app/api/v1/vendor/analytics.py
vendored
@@ -15,16 +15,32 @@ from app.api.deps import get_current_vendor_api
|
|||||||
from app.core.database import get_db
|
from app.core.database import get_db
|
||||||
from app.services.stats_service import stats_service
|
from app.services.stats_service import stats_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
|
from models.schema.stats import (
|
||||||
|
VendorAnalyticsCatalog,
|
||||||
|
VendorAnalyticsImports,
|
||||||
|
VendorAnalyticsInventory,
|
||||||
|
VendorAnalyticsResponse,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/analytics")
|
router = APIRouter(prefix="/analytics")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("", response_model=VendorAnalyticsResponse)
|
||||||
def get_vendor_analytics(
|
def get_vendor_analytics(
|
||||||
period: str = Query("30d", description="Time period: 7d, 30d, 90d, 1y"),
|
period: str = Query("30d", description="Time period: 7d, 30d, 90d, 1y"),
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
db: Session = Depends(get_db),
|
db: Session = Depends(get_db),
|
||||||
):
|
):
|
||||||
"""Get vendor analytics data for specified time period."""
|
"""Get vendor analytics data for specified time period."""
|
||||||
return stats_service.get_vendor_analytics(db, current_user.token_vendor_id, 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"]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|||||||
58
app/api/v1/vendor/dashboard.py
vendored
58
app/api/v1/vendor/dashboard.py
vendored
@@ -17,12 +17,20 @@ from app.exceptions import VendorNotActiveException
|
|||||||
from app.services.stats_service import stats_service
|
from app.services.stats_service import stats_service
|
||||||
from app.services.vendor_service import vendor_service
|
from app.services.vendor_service import vendor_service
|
||||||
from models.database.user import User
|
from models.database.user import User
|
||||||
|
from models.schema.stats import (
|
||||||
|
VendorCustomerStats,
|
||||||
|
VendorDashboardStatsResponse,
|
||||||
|
VendorInfo,
|
||||||
|
VendorOrderStats,
|
||||||
|
VendorProductStats,
|
||||||
|
VendorRevenueStats,
|
||||||
|
)
|
||||||
|
|
||||||
router = APIRouter(prefix="/dashboard")
|
router = APIRouter(prefix="/dashboard")
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/stats")
|
@router.get("/stats", response_model=VendorDashboardStatsResponse)
|
||||||
def get_vendor_dashboard_stats(
|
def get_vendor_dashboard_stats(
|
||||||
request: Request,
|
request: Request,
|
||||||
current_user: User = Depends(get_current_vendor_api),
|
current_user: User = Depends(get_current_vendor_api),
|
||||||
@@ -51,27 +59,27 @@ def get_vendor_dashboard_stats(
|
|||||||
# Get vendor-scoped statistics
|
# Get vendor-scoped statistics
|
||||||
stats_data = stats_service.get_vendor_stats(db=db, vendor_id=vendor_id)
|
stats_data = stats_service.get_vendor_stats(db=db, vendor_id=vendor_id)
|
||||||
|
|
||||||
return {
|
return VendorDashboardStatsResponse(
|
||||||
"vendor": {
|
vendor=VendorInfo(
|
||||||
"id": vendor.id,
|
id=vendor.id,
|
||||||
"name": vendor.name,
|
name=vendor.name,
|
||||||
"vendor_code": vendor.vendor_code,
|
vendor_code=vendor.vendor_code,
|
||||||
},
|
),
|
||||||
"products": {
|
products=VendorProductStats(
|
||||||
"total": stats_data.get("total_products", 0),
|
total=stats_data.get("total_products", 0),
|
||||||
"active": stats_data.get("active_products", 0),
|
active=stats_data.get("active_products", 0),
|
||||||
},
|
),
|
||||||
"orders": {
|
orders=VendorOrderStats(
|
||||||
"total": stats_data.get("total_orders", 0),
|
total=stats_data.get("total_orders", 0),
|
||||||
"pending": stats_data.get("pending_orders", 0),
|
pending=stats_data.get("pending_orders", 0),
|
||||||
"completed": stats_data.get("completed_orders", 0),
|
completed=stats_data.get("completed_orders", 0),
|
||||||
},
|
),
|
||||||
"customers": {
|
customers=VendorCustomerStats(
|
||||||
"total": stats_data.get("total_customers", 0),
|
total=stats_data.get("total_customers", 0),
|
||||||
"active": stats_data.get("active_customers", 0),
|
active=stats_data.get("active_customers", 0),
|
||||||
},
|
),
|
||||||
"revenue": {
|
revenue=VendorRevenueStats(
|
||||||
"total": stats_data.get("total_revenue", 0),
|
total=stats_data.get("total_revenue", 0),
|
||||||
"this_month": stats_data.get("revenue_this_month", 0),
|
this_month=stats_data.get("revenue_this_month", 0),
|
||||||
},
|
),
|
||||||
}
|
)
|
||||||
|
|||||||
@@ -130,36 +130,38 @@ class StatsService:
|
|||||||
db.query(Customer).filter(Customer.vendor_id == vendor_id).count()
|
db.query(Customer).filter(Customer.vendor_id == vendor_id).count()
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Return flat structure compatible with VendorDashboardStatsResponse schema
|
||||||
|
# The endpoint will restructure this into nested format
|
||||||
return {
|
return {
|
||||||
"catalog": {
|
# Product stats
|
||||||
"total_products": total_catalog_products,
|
"total_products": total_catalog_products,
|
||||||
"featured_products": featured_products,
|
"active_products": total_catalog_products,
|
||||||
"active_products": total_catalog_products,
|
"featured_products": featured_products,
|
||||||
},
|
# Order stats (TODO: implement when Order model has status field)
|
||||||
"staging": {
|
"total_orders": total_orders,
|
||||||
"imported_products": staging_products,
|
"pending_orders": 0, # TODO: filter by status
|
||||||
},
|
"completed_orders": 0, # TODO: filter by status
|
||||||
"inventory": {
|
# Customer stats
|
||||||
"total_quantity": int(total_inventory),
|
"total_customers": total_customers,
|
||||||
"reserved_quantity": int(reserved_inventory),
|
"active_customers": 0, # TODO: implement active customer logic
|
||||||
"available_quantity": int(total_inventory - reserved_inventory),
|
# Revenue stats (TODO: implement when Order model has amount field)
|
||||||
"locations_count": inventory_locations,
|
"total_revenue": 0,
|
||||||
},
|
"revenue_this_month": 0,
|
||||||
"imports": {
|
# Import stats
|
||||||
"total_imports": total_imports,
|
"total_imports": total_imports,
|
||||||
"successful_imports": successful_imports,
|
"successful_imports": successful_imports,
|
||||||
"success_rate": (
|
"import_success_rate": (
|
||||||
(successful_imports / total_imports * 100)
|
(successful_imports / total_imports * 100)
|
||||||
if total_imports > 0
|
if total_imports > 0
|
||||||
else 0
|
else 0
|
||||||
),
|
),
|
||||||
},
|
# Staging stats
|
||||||
"orders": {
|
"imported_products": staging_products,
|
||||||
"total_orders": total_orders,
|
# Inventory stats
|
||||||
},
|
"total_inventory_quantity": int(total_inventory),
|
||||||
"customers": {
|
"reserved_inventory_quantity": int(reserved_inventory),
|
||||||
"total_customers": total_customers,
|
"available_inventory_quantity": int(total_inventory - reserved_inventory),
|
||||||
},
|
"inventory_locations_count": inventory_locations,
|
||||||
}
|
}
|
||||||
|
|
||||||
except VendorNotFoundException:
|
except VendorNotFoundException:
|
||||||
@@ -255,7 +257,11 @@ class StatsService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_vendor_statistics(self, db: Session) -> dict:
|
def get_vendor_statistics(self, db: Session) -> dict:
|
||||||
"""Get vendor statistics for admin dashboard."""
|
"""Get vendor statistics for admin dashboard.
|
||||||
|
|
||||||
|
Returns dict compatible with VendorStatsResponse schema.
|
||||||
|
Keys: total, verified, pending, inactive (mapped from internal names)
|
||||||
|
"""
|
||||||
try:
|
try:
|
||||||
total_vendors = db.query(Vendor).count()
|
total_vendors = db.query(Vendor).count()
|
||||||
active_vendors = db.query(Vendor).filter(Vendor.is_active == True).count()
|
active_vendors = db.query(Vendor).filter(Vendor.is_active == True).count()
|
||||||
@@ -263,12 +269,25 @@ class StatsService:
|
|||||||
db.query(Vendor).filter(Vendor.is_verified == True).count()
|
db.query(Vendor).filter(Vendor.is_verified == True).count()
|
||||||
)
|
)
|
||||||
inactive_vendors = total_vendors - active_vendors
|
inactive_vendors = total_vendors - active_vendors
|
||||||
|
# Pending = active but not yet verified
|
||||||
|
pending_vendors = (
|
||||||
|
db.query(Vendor)
|
||||||
|
.filter(Vendor.is_active == True, Vendor.is_verified == False)
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
# Schema-compatible fields (VendorStatsResponse)
|
||||||
|
"total": total_vendors,
|
||||||
|
"verified": verified_vendors,
|
||||||
|
"pending": pending_vendors,
|
||||||
|
"inactive": inactive_vendors,
|
||||||
|
# Legacy fields for backward compatibility
|
||||||
"total_vendors": total_vendors,
|
"total_vendors": total_vendors,
|
||||||
"active_vendors": active_vendors,
|
"active_vendors": active_vendors,
|
||||||
"inactive_vendors": inactive_vendors,
|
"inactive_vendors": inactive_vendors,
|
||||||
"verified_vendors": verified_vendors,
|
"verified_vendors": verified_vendors,
|
||||||
|
"pending_vendors": pending_vendors,
|
||||||
"verification_rate": (
|
"verification_rate": (
|
||||||
(verified_vendors / total_vendors * 100) if total_vendors > 0 else 0
|
(verified_vendors / total_vendors * 100) if total_vendors > 0 else 0
|
||||||
),
|
),
|
||||||
@@ -431,9 +450,23 @@ class StatsService:
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
total = db.query(MarketplaceImportJob).count()
|
total = db.query(MarketplaceImportJob).count()
|
||||||
|
pending = (
|
||||||
|
db.query(MarketplaceImportJob)
|
||||||
|
.filter(MarketplaceImportJob.status == "pending")
|
||||||
|
.count()
|
||||||
|
)
|
||||||
|
processing = (
|
||||||
|
db.query(MarketplaceImportJob)
|
||||||
|
.filter(MarketplaceImportJob.status == "processing")
|
||||||
|
.count()
|
||||||
|
)
|
||||||
completed = (
|
completed = (
|
||||||
db.query(MarketplaceImportJob)
|
db.query(MarketplaceImportJob)
|
||||||
.filter(MarketplaceImportJob.status == "completed")
|
.filter(
|
||||||
|
MarketplaceImportJob.status.in_(
|
||||||
|
["completed", "completed_with_errors"]
|
||||||
|
)
|
||||||
|
)
|
||||||
.count()
|
.count()
|
||||||
)
|
)
|
||||||
failed = (
|
failed = (
|
||||||
@@ -443,6 +476,13 @@ class StatsService:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
# Frontend-expected fields
|
||||||
|
"total": total,
|
||||||
|
"pending": pending,
|
||||||
|
"processing": processing,
|
||||||
|
"completed": completed,
|
||||||
|
"failed": failed,
|
||||||
|
# Legacy fields for backward compatibility
|
||||||
"total_imports": total,
|
"total_imports": total,
|
||||||
"completed_imports": completed,
|
"completed_imports": completed,
|
||||||
"failed_imports": failed,
|
"failed_imports": failed,
|
||||||
@@ -451,6 +491,11 @@ class StatsService:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"Failed to get import statistics: {str(e)}")
|
logger.error(f"Failed to get import statistics: {str(e)}")
|
||||||
return {
|
return {
|
||||||
|
"total": 0,
|
||||||
|
"pending": 0,
|
||||||
|
"processing": 0,
|
||||||
|
"completed": 0,
|
||||||
|
"failed": 0,
|
||||||
"total_imports": 0,
|
"total_imports": 0,
|
||||||
"completed_imports": 0,
|
"completed_imports": 0,
|
||||||
"failed_imports": 0,
|
"failed_imports": 0,
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
from datetime import datetime
|
from datetime import datetime
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from pydantic import BaseModel, Field
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
|
||||||
class StatsResponse(BaseModel):
|
class StatsResponse(BaseModel):
|
||||||
|
"""Comprehensive platform statistics response schema."""
|
||||||
|
|
||||||
total_products: int
|
total_products: int
|
||||||
unique_brands: int
|
unique_brands: int
|
||||||
unique_categories: int
|
unique_categories: int
|
||||||
@@ -15,6 +18,8 @@ class StatsResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
class MarketplaceStatsResponse(BaseModel):
|
class MarketplaceStatsResponse(BaseModel):
|
||||||
|
"""Statistics per marketplace response schema."""
|
||||||
|
|
||||||
marketplace: str
|
marketplace: str
|
||||||
total_products: int
|
total_products: int
|
||||||
unique_vendors: int
|
unique_vendors: int
|
||||||
@@ -22,7 +27,246 @@ class MarketplaceStatsResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Customer Statistics
|
# Import Statistics
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class ImportStatsResponse(BaseModel):
|
||||||
|
"""Import job statistics response schema.
|
||||||
|
|
||||||
|
Used by: GET /api/v1/admin/marketplace-import-jobs/stats
|
||||||
|
"""
|
||||||
|
|
||||||
|
total: int = Field(..., description="Total number of import jobs")
|
||||||
|
pending: int = Field(..., description="Jobs waiting to start")
|
||||||
|
processing: int = Field(..., description="Jobs currently running")
|
||||||
|
completed: int = Field(..., description="Successfully completed jobs")
|
||||||
|
failed: int = Field(..., description="Failed jobs")
|
||||||
|
success_rate: float = Field(..., description="Percentage of successful imports")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# User Statistics
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class UserStatsResponse(BaseModel):
|
||||||
|
"""User statistics response schema.
|
||||||
|
|
||||||
|
Used by: Platform statistics endpoints
|
||||||
|
"""
|
||||||
|
|
||||||
|
total_users: int = Field(..., description="Total number of users")
|
||||||
|
active_users: int = Field(..., description="Number of active users")
|
||||||
|
inactive_users: int = Field(..., description="Number of inactive users")
|
||||||
|
admin_users: int = Field(..., description="Number of admin users")
|
||||||
|
activation_rate: float = Field(..., description="Percentage of active users")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Vendor Statistics (Admin)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class VendorStatsResponse(BaseModel):
|
||||||
|
"""Vendor statistics response schema for admin dashboard.
|
||||||
|
|
||||||
|
Used by: GET /api/v1/admin/vendors/stats
|
||||||
|
"""
|
||||||
|
|
||||||
|
total: int = Field(..., description="Total number of vendors")
|
||||||
|
verified: int = Field(..., description="Number of verified vendors")
|
||||||
|
pending: int = Field(..., description="Number of pending verification vendors")
|
||||||
|
inactive: int = Field(..., description="Number of inactive vendors")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Product Statistics
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class ProductStatsResponse(BaseModel):
|
||||||
|
"""Product statistics response schema.
|
||||||
|
|
||||||
|
Used by: Platform statistics endpoints
|
||||||
|
"""
|
||||||
|
|
||||||
|
total_products: int = Field(0, description="Total number of products")
|
||||||
|
active_products: int = Field(0, description="Number of active products")
|
||||||
|
out_of_stock: int = Field(0, description="Number of out-of-stock products")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Platform Statistics (Combined)
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class PlatformStatsResponse(BaseModel):
|
||||||
|
"""Combined platform statistics response schema.
|
||||||
|
|
||||||
|
Used by: GET /api/v1/admin/dashboard/stats/platform
|
||||||
|
"""
|
||||||
|
|
||||||
|
users: UserStatsResponse
|
||||||
|
vendors: VendorStatsResponse
|
||||||
|
products: ProductStatsResponse
|
||||||
|
orders: "OrderStatsBasicResponse"
|
||||||
|
imports: ImportStatsResponse
|
||||||
|
|
||||||
|
|
||||||
|
class OrderStatsBasicResponse(BaseModel):
|
||||||
|
"""Basic order statistics (stub until Order model is fully implemented).
|
||||||
|
|
||||||
|
Used by: Platform statistics endpoints
|
||||||
|
"""
|
||||||
|
|
||||||
|
total_orders: int = Field(0, description="Total number of orders")
|
||||||
|
pending_orders: int = Field(0, description="Number of pending orders")
|
||||||
|
completed_orders: int = Field(0, description="Number of completed orders")
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Admin Dashboard Response
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class AdminDashboardResponse(BaseModel):
|
||||||
|
"""Admin dashboard response schema.
|
||||||
|
|
||||||
|
Used by: GET /api/v1/admin/dashboard
|
||||||
|
"""
|
||||||
|
|
||||||
|
platform: dict[str, Any] = Field(..., description="Platform information")
|
||||||
|
users: UserStatsResponse
|
||||||
|
vendors: VendorStatsResponse
|
||||||
|
recent_vendors: list[dict[str, Any]] = Field(
|
||||||
|
default_factory=list, description="Recent vendors"
|
||||||
|
)
|
||||||
|
recent_imports: list[dict[str, Any]] = Field(
|
||||||
|
default_factory=list, description="Recent import jobs"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Vendor Dashboard Statistics
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class VendorProductStats(BaseModel):
|
||||||
|
"""Vendor product statistics."""
|
||||||
|
|
||||||
|
total: int = Field(0, description="Total products in catalog")
|
||||||
|
active: int = Field(0, description="Active products")
|
||||||
|
|
||||||
|
|
||||||
|
class VendorOrderStats(BaseModel):
|
||||||
|
"""Vendor order statistics."""
|
||||||
|
|
||||||
|
total: int = Field(0, description="Total orders")
|
||||||
|
pending: int = Field(0, description="Pending orders")
|
||||||
|
completed: int = Field(0, description="Completed orders")
|
||||||
|
|
||||||
|
|
||||||
|
class VendorCustomerStats(BaseModel):
|
||||||
|
"""Vendor customer statistics."""
|
||||||
|
|
||||||
|
total: int = Field(0, description="Total customers")
|
||||||
|
active: int = Field(0, description="Active customers")
|
||||||
|
|
||||||
|
|
||||||
|
class VendorRevenueStats(BaseModel):
|
||||||
|
"""Vendor revenue statistics."""
|
||||||
|
|
||||||
|
total: float = Field(0, description="Total revenue")
|
||||||
|
this_month: float = Field(0, description="Revenue this month")
|
||||||
|
|
||||||
|
|
||||||
|
class VendorInfo(BaseModel):
|
||||||
|
"""Vendor basic info for dashboard."""
|
||||||
|
|
||||||
|
id: int
|
||||||
|
name: str
|
||||||
|
vendor_code: str
|
||||||
|
|
||||||
|
|
||||||
|
class VendorDashboardStatsResponse(BaseModel):
|
||||||
|
"""Vendor dashboard statistics response schema.
|
||||||
|
|
||||||
|
Used by: GET /api/v1/vendor/dashboard/stats
|
||||||
|
"""
|
||||||
|
|
||||||
|
vendor: VendorInfo
|
||||||
|
products: VendorProductStats
|
||||||
|
orders: VendorOrderStats
|
||||||
|
customers: VendorCustomerStats
|
||||||
|
revenue: VendorRevenueStats
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Vendor Analytics
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class VendorAnalyticsImports(BaseModel):
|
||||||
|
"""Vendor import analytics."""
|
||||||
|
|
||||||
|
count: int = Field(0, description="Number of imports in period")
|
||||||
|
|
||||||
|
|
||||||
|
class VendorAnalyticsCatalog(BaseModel):
|
||||||
|
"""Vendor catalog analytics."""
|
||||||
|
|
||||||
|
products_added: int = Field(0, description="Products added in period")
|
||||||
|
|
||||||
|
|
||||||
|
class VendorAnalyticsInventory(BaseModel):
|
||||||
|
"""Vendor inventory analytics."""
|
||||||
|
|
||||||
|
total_locations: int = Field(0, description="Total inventory locations")
|
||||||
|
|
||||||
|
|
||||||
|
class VendorAnalyticsResponse(BaseModel):
|
||||||
|
"""Vendor analytics response schema.
|
||||||
|
|
||||||
|
Used by: GET /api/v1/vendor/analytics
|
||||||
|
"""
|
||||||
|
|
||||||
|
period: str = Field(..., description="Analytics period (e.g., '30d')")
|
||||||
|
start_date: str = Field(..., description="Period start date")
|
||||||
|
imports: VendorAnalyticsImports
|
||||||
|
catalog: VendorAnalyticsCatalog
|
||||||
|
inventory: VendorAnalyticsInventory
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Code Quality Dashboard Statistics
|
||||||
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
|
class CodeQualityDashboardStatsResponse(BaseModel):
|
||||||
|
"""Code quality dashboard statistics response schema.
|
||||||
|
|
||||||
|
Used by: GET /api/v1/admin/code-quality/stats
|
||||||
|
"""
|
||||||
|
|
||||||
|
total_violations: int
|
||||||
|
errors: int
|
||||||
|
warnings: int
|
||||||
|
open: int
|
||||||
|
assigned: int
|
||||||
|
resolved: int
|
||||||
|
ignored: int
|
||||||
|
technical_debt_score: int
|
||||||
|
trend: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
by_severity: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
by_rule: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
by_module: dict[str, Any] = Field(default_factory=dict)
|
||||||
|
top_files: list[dict[str, Any]] = Field(default_factory=list)
|
||||||
|
last_scan: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
# ============================================================================
|
||||||
|
# Customer Statistics (Coming Soon)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -39,7 +283,7 @@ class CustomerStatsResponse(BaseModel):
|
|||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# Order Statistics
|
# Order Statistics (Coming Soon)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|
||||||
|
|
||||||
@@ -54,17 +298,3 @@ class OrderStatsResponse(BaseModel):
|
|||||||
cancelled_orders: int
|
cancelled_orders: int
|
||||||
total_revenue: Decimal
|
total_revenue: Decimal
|
||||||
average_order_value: Decimal
|
average_order_value: Decimal
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
|
||||||
# Vendor Statistics
|
|
||||||
# ============================================================================
|
|
||||||
|
|
||||||
|
|
||||||
class VendorStatsResponse(BaseModel):
|
|
||||||
"""Vendor statistics response schema."""
|
|
||||||
|
|
||||||
total: int = Field(..., description="Total number of vendors")
|
|
||||||
verified: int = Field(..., description="Number of verified vendors")
|
|
||||||
pending: int = Field(..., description="Number of pending verification vendors")
|
|
||||||
inactive: int = Field(..., description="Number of inactive vendors")
|
|
||||||
|
|||||||
Reference in New Issue
Block a user