feat: subs and views

This commit is contained in:
Artem Tsyrulnikov
2025-11-10 17:08:56 +03:00
parent 2549242b06
commit 0bed5c266d
34 changed files with 1706 additions and 119 deletions

View File

@@ -5,10 +5,13 @@ __all__ = (
'ExternalChannel',
'Creative',
'Purchase',
'Subscription',
'ViewsSnapshot',
'TargetChannelStatus',
'CreativeStatus',
'PurchaseStatus',
'InviteLinkType',
'PostViewsAvailability',
'LoginToken',
'UserNotFound',
'LoginTokenNotFound',
@@ -42,6 +45,8 @@ from .error import (
)
from .external_channel import ExternalChannel
from .login_token import LoginToken
from .purchase import InviteLinkType, Purchase, PurchaseStatus
from .purchase import InviteLinkType, PostViewsAvailability, Purchase, PurchaseStatus
from .subscription import Subscription
from .target_channel import TargetChannel, TargetChannelStatus
from .user import User
from .views_snapshot import ViewsSnapshot

View File

@@ -24,6 +24,15 @@ class InviteLinkType(StrEnum):
APPROVAL = 'approval' # с одобрением ботом
class PostViewsAvailability(StrEnum):
"""Статус доступности получения просмотров поста."""
UNKNOWN = 'unknown' # Ещё не проверялось
AVAILABLE = 'available' # Просмотры доступны
UNAVAILABLE = 'unavailable' # Нет доступа / битая ссылка / приватный канал
MANUAL = 'manual' # Просмотры вводятся вручную
class Purchase(domain.Base):
# Основные поля
target_channel_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('targetchannels.id'), index=True)
@@ -44,7 +53,11 @@ class Purchase(domain.Base):
# Метрики (будут обновляться из Telegram API)
subscriptions_count: Mapped[int] = mapped_column(default=0, server_default='0')
views_count: Mapped[int | None] # просмотры рекламного поста (если доступны)
views_count: Mapped[int | None] # последние известные просмотры (кэш из последнего snapshot)
# Статус доступности просмотров
views_availability: Mapped[PostViewsAvailability] = mapped_column(default=PostViewsAvailability.UNKNOWN)
last_views_fetch_at: Mapped[datetime.datetime | None] # когда последний раз получали просмотры
# Relationships
target_channel: Mapped['TargetChannel'] = relationship()

View File

@@ -0,0 +1,22 @@
import uuid
from typing import TYPE_CHECKING
from sqlalchemy import BigInteger, ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from src import domain
if TYPE_CHECKING:
from .purchase import Purchase
class Subscription(domain.Base):
"""Подписка пользователя в целевой канал через конкретный закуп."""
purchase_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('purchases.id'), index=True)
user_telegram_id: Mapped[int] = mapped_column(BigInteger, index=True)
username: Mapped[str | None]
invite_link: Mapped[str] = mapped_column(index=True) # По которой пришёл пользователь
# Relationship
purchase: Mapped['Purchase'] = relationship()

View File

@@ -0,0 +1,22 @@
import datetime
import uuid
from typing import TYPE_CHECKING
from sqlalchemy import ForeignKey
from sqlalchemy.orm import Mapped, mapped_column, relationship
from src import domain
if TYPE_CHECKING:
from .purchase import Purchase
class ViewsSnapshot(domain.Base):
"""Снимок просмотров рекламного поста в определенный момент времени."""
purchase_id: Mapped[uuid.UUID] = mapped_column(ForeignKey('purchases.id'), index=True)
views_count: Mapped[int] # Количество просмотров на момент снимка
fetched_at: Mapped[datetime.datetime] = mapped_column(index=True) # Когда получены данные
# Relationship
purchase: Mapped['Purchase'] = relationship()