import datetime import logging from collections import defaultdict from dataclasses import dataclass from typing import TYPE_CHECKING from src import domain, dto if TYPE_CHECKING: from .. import Usecase log = logging.getLogger(__name__) def _format_period(dt: 'datetime.datetime', grouping: dto.DateGrouping) -> str: match grouping: case dto.DateGrouping.DAY: return str(dt.strftime('%Y-%m-%d')) case dto.DateGrouping.WEEK: # ISO week return str(dt.strftime('%Y-W%W')) case dto.DateGrouping.MONTH: return str(dt.strftime('%Y-%m')) case dto.DateGrouping.QUARTER: quarter = (dt.month - 1) // 3 + 1 return f'{dt.year}-Q{quarter}' case dto.DateGrouping.YEAR: return str(dt.year) raise ValueError('Invalid date grouping') async def get_spending_analytics( self: 'Usecase', input: dto.GetSpendingAnalyticsInput ) -> dto.GetSpendingAnalyticsOutput: await self.ensure_workspace_access(input.workspace_id, input.user_id) if input.target_channel_id: target_channel = await self.database.get_target_channel(input.workspace_id, input.target_channel_id) if not target_channel: raise domain.TargetChannelNotFound(input.target_channel_id) placements = await self.database.get_workspace_placements( input.workspace_id, input.target_channel_id, include_archived=False ) filtered = [ p for p in placements if (not input.date_from or p.placement_date >= input.date_from) and (not input.date_to or p.placement_date <= input.date_to) ] total_cost = 0.0 total_subs = 0 total_views = 0 @dataclass class PeriodData: cost: float = 0.0 subscriptions: int = 0 views: int = 0 # Группировка по периодам period_data: dict[str, PeriodData] = defaultdict(PeriodData) for p in filtered: period = _format_period(p.placement_date, input.grouping) pd = period_data[period] if p.cost is not None: total_cost += p.cost pd.cost += p.cost total_subs += p.subscriptions_count pd.subscriptions += p.subscriptions_count if p.views_count is not None: total_views += p.views_count pd.views += p.views_count avg_cpf = total_cost / total_subs if total_subs > 0 and total_cost > 0 else None avg_cpm = (total_cost / total_views * 1000) if total_views > 0 and total_cost > 0 else None # Данные для графика chart_data = [] for period in sorted(period_data.keys()): d = period_data[period] cost = d.cost subs = d.subscriptions views = d.views cpf = cost / subs if subs > 0 and cost > 0 else None cpm = (cost / views * 1000) if views > 0 and cost > 0 else None chart_data.append( dto.SpendingDataPoint( period=period, cost=cost, subscriptions=subs, views=views, cpf=cpf, cpm=cpm, ) ) return dto.GetSpendingAnalyticsOutput( total_cost=total_cost, total_subscriptions=total_subs, total_views=total_views, avg_cpf=avg_cpf, avg_cpm=avg_cpm, chart_data=chart_data, )