feat: аналитика
This commit is contained in:
114
src/usecase/analytics/get_spending_analytics.py
Normal file
114
src/usecase/analytics/get_spending_analytics.py
Normal file
@@ -0,0 +1,114 @@
|
||||
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:
|
||||
async with self.database.transaction():
|
||||
if input.target_channel_id:
|
||||
target_channel = await self.database.get_target_channel(input.user_id, input.target_channel_id)
|
||||
if not target_channel:
|
||||
raise domain.TargetChannelNotFound(input.target_channel_id)
|
||||
|
||||
placements = await self.database.get_user_placements(
|
||||
input.user_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,
|
||||
)
|
||||
Reference in New Issue
Block a user