Files
tgex-backend/src/usecase/analytics/get_spending_analytics.py
Artem Tsyrulnikov af32e2a507 feat: add workspace
2025-12-13 22:09:55 +03:00

116 lines
3.3 KiB
Python

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,
)