100 lines
2.9 KiB
Python
100 lines
2.9 KiB
Python
"""Analytics API endpoints for summaries and funnels."""
|
|
|
|
from __future__ import annotations
|
|
|
|
from decimal import Decimal
|
|
|
|
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"])
|
|
|
|
|
|
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)
|
|
|
|
|
|
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),
|
|
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", response_model=DealFunnelResponse)
|
|
async def deals_funnel(
|
|
context: OrganizationContext = Depends(get_organization_context),
|
|
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]
|
|
)
|