analytics #8

Merged
k1nq merged 4 commits from analytics into dev 2025-11-29 04:15:57 +00:00
1 changed files with 78 additions and 15 deletions
Showing only changes of commit 92bd3b6c00 - Show all commits

View File

@ -1,32 +1,95 @@
"""Analytics API stubs (deal summary and funnel)."""
"""Analytics API endpoints for summaries and funnels."""
from __future__ import annotations
from fastapi import APIRouter, Depends, Query, status
from decimal import Decimal
from app.api.deps import get_organization_context
from fastapi import APIRouter, Depends, Query
from pydantic import BaseModel, ConfigDict, field_serializer
from app.api.deps import get_analytics_service, get_organization_context
from app.models.deal import DealStage, DealStatus
from app.services.analytics_service import AnalyticsService, DealSummary, StageBreakdown
from app.services.organization_service import OrganizationContext
def _decimal_to_str(value: Decimal) -> str:
normalized = value.normalize()
return format(normalized, "f")
router = APIRouter(prefix="/analytics", tags=["analytics"])
def _stub(endpoint: str) -> dict[str, str]:
return {"detail": f"{endpoint} is not implemented yet"}
class StatusSummaryModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
status: DealStatus
count: int
amount_sum: Decimal
@field_serializer("amount_sum")
def serialize_amount_sum(self, value: Decimal) -> str:
return _decimal_to_str(value)
@router.get("/deals/summary", status_code=status.HTTP_501_NOT_IMPLEMENTED)
class WonStatisticsModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
count: int
amount_sum: Decimal
average_amount: Decimal
@field_serializer("amount_sum", "average_amount")
def serialize_decimal_fields(self, value: Decimal) -> str:
return _decimal_to_str(value)
class NewDealsWindowModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
days: int
count: int
class DealSummaryResponse(BaseModel):
model_config = ConfigDict(from_attributes=True)
by_status: list[StatusSummaryModel]
won: WonStatisticsModel
new_deals: NewDealsWindowModel
total_deals: int
class StageBreakdownModel(BaseModel):
model_config = ConfigDict(from_attributes=True)
stage: DealStage
total: int
by_status: dict[DealStatus, int]
conversion_to_next: float | None
class DealFunnelResponse(BaseModel):
stages: list[StageBreakdownModel]
@router.get("/deals/summary", response_model=DealSummaryResponse)
async def deals_summary(
days: int = Query(30, ge=1, le=180),
context: OrganizationContext = Depends(get_organization_context),
) -> dict[str, str]:
"""Placeholder for aggregated deal statistics."""
_ = (days, context)
return _stub("GET /analytics/deals/summary")
service: AnalyticsService = Depends(get_analytics_service),
) -> DealSummaryResponse:
"""Return aggregated deal statistics for the current organization."""
summary: DealSummary = await service.get_deal_summary(context.organization_id, days=days)
return DealSummaryResponse.model_validate(summary)
@router.get("/deals/funnel", status_code=status.HTTP_501_NOT_IMPLEMENTED)
@router.get("/deals/funnel", response_model=DealFunnelResponse)
async def deals_funnel(
context: OrganizationContext = Depends(get_organization_context),
) -> dict[str, str]:
"""Placeholder for funnel analytics."""
_ = context
return _stub("GET /analytics/deals/funnel")
service: AnalyticsService = Depends(get_analytics_service),
) -> DealFunnelResponse:
"""Return funnel breakdown by stages and statuses."""
breakdowns: list[StageBreakdown] = await service.get_deal_funnel(context.organization_id)
return DealFunnelResponse(stages=[StageBreakdownModel.model_validate(item) for item in breakdowns])