This commit is contained in:
Artem Tsyrulnikov
2026-01-15 22:02:16 +03:00
parent 6ec26c96e3
commit 049024ebe8
72 changed files with 2417 additions and 2054 deletions

View File

@@ -311,105 +311,6 @@ class Postgres(DatabaseBase, Database):
project = await domain.Project.get_or_none(channel_id=channel_id, workspace_id=workspace_id)
return project is not None
async def get_active_purchase(self, project_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Purchase | None:
return await domain.Purchase.get_or_none(
project_id=project_id, creative_id=creative_id, status=domain.PurchaseStatus.ACTIVE
)
async def get_purchase(self, workspace_id: uuid.UUID, purchase_id: uuid.UUID) -> domain.Purchase | None:
return await domain.Purchase.get_or_none(id=purchase_id, project__workspace_id=workspace_id)
async def get_project_purchases(self, workspace_id: uuid.UUID, project_id: uuid.UUID) -> list[domain.Purchase]:
return (
await domain.Purchase.filter(project__workspace_id=workspace_id, project_id=project_id)
.prefetch_related('channels', 'channels__channel')
.all()
)
async def create_purchase(self, purchase: domain.Purchase) -> None:
await purchase.save()
async def get_purchase_channels(self, purchase_id: uuid.UUID) -> list[domain.PurchaseChannel]:
return await domain.PurchaseChannel.filter(purchase_id=purchase_id).prefetch_related('channel').all()
async def get_purchase_channel(
self, purchase_channel_id: uuid.UUID, purchase_id: uuid.UUID
) -> domain.PurchaseChannel | None:
return (
await domain.PurchaseChannel.filter(id=purchase_channel_id, purchase_id=purchase_id)
.prefetch_related('channel')
.first()
)
async def get_purchase_channel_by_id(self, purchase_channel_id: uuid.UUID) -> domain.PurchaseChannel | None:
return (
await domain.PurchaseChannel.filter(id=purchase_channel_id)
.prefetch_related('channel', 'purchase', 'purchase__project')
.first()
)
async def get_purchase_channels_by_project(self, project_id: uuid.UUID) -> list[domain.PurchaseChannel]:
return (
await domain.PurchaseChannel.filter(
purchase__project_id=project_id, purchase__status=domain.PurchaseStatus.ACTIVE
)
.prefetch_related('channel')
.all()
)
async def add_channel_to_purchase(
self,
purchase_id: uuid.UUID,
channel_id: uuid.UUID,
invite_link: str,
invite_link_type: domain.purchase.InviteLinkType,
*,
status: domain.PurchaseChannelStatus | None = None,
comment: str | None = None,
placement_at: datetime.datetime | None = None,
cost_type: domain.purchase.CostType | None = None,
cost_value: float | None = None,
cost_before_bargain: float | None = None,
format: str | None = None,
) -> domain.PurchaseChannel:
defaults: dict[str, object | None] = {
'invite_link': invite_link,
'invite_link_type': invite_link_type,
}
if status is not None:
defaults['status'] = status
if comment is not None:
defaults['comment'] = comment
if placement_at is not None:
defaults['placement_at'] = placement_at
if cost_type is not None:
defaults['cost_type'] = cost_type
if cost_value is not None:
defaults['cost_value'] = cost_value
if cost_before_bargain is not None:
defaults['cost_before_bargain'] = cost_before_bargain
if format is not None:
defaults['format'] = format
purchase_channel, created = await domain.PurchaseChannel.get_or_create(
purchase_id=purchase_id,
channel_id=channel_id,
defaults=defaults,
)
if not created and defaults:
for field, value in defaults.items():
setattr(purchase_channel, field, value)
await purchase_channel.save()
return purchase_channel
async def update_purchase_channel(self, purchase_channel: domain.PurchaseChannel) -> None:
await purchase_channel.save()
async def remove_channel_from_purchase(self, purchase_id: uuid.UUID, channel_id: uuid.UUID) -> None:
await domain.PurchaseChannel.filter(purchase_id=purchase_id, channel_id=channel_id).delete()
async def get_creative(self, workspace_id: uuid.UUID, creative_id: uuid.UUID) -> domain.Creative | None:
return await domain.Creative.get_or_none(
id=creative_id, project__workspace_id=workspace_id
@@ -442,26 +343,51 @@ class Postgres(DatabaseBase, Database):
async def delete_creative(self, creative_id: uuid.UUID) -> None:
await domain.Creative.filter(id=creative_id).delete()
# Placement methods (user-managed, запланированные размещения)
async def create_placement(self, placement: domain.Placement) -> None:
await placement.save()
async def create_placements(self, placements: list[domain.Placement]) -> None:
if placements:
await domain.Placement.bulk_create(placements)
async def get_placement(self, workspace_id: uuid.UUID, placement_id: uuid.UUID) -> domain.Placement | None:
return await domain.Placement.get_or_none(
id=placement_id, project__workspace_id=workspace_id
).prefetch_related('project', 'project__channel', 'channel', 'creative')
async def get_project_placements(
self, workspace_id: uuid.UUID, project_id: uuid.UUID, include_archived: bool = False
) -> list[domain.Placement]:
query = domain.Placement.filter(project__workspace_id=workspace_id, project_id=project_id)
if not include_archived:
query = query.filter(status__in=[domain.PlacementStatus.APPROVED, domain.PlacementStatus.IN_PROGRESS])
return (
await query.prefetch_related('project', 'project__channel', 'channel', 'creative')
.order_by('-created_at')
.all()
)
async def update_placement_user(self, placement: domain.Placement) -> None:
await placement.save()
# Publication methods (system-managed, фактически размещенные посты)
async def get_publication(self, workspace_id: uuid.UUID, publication_id: uuid.UUID) -> domain.Publication | None:
return await domain.Publication.get_or_none(
id=publication_id, placement__project__workspace_id=workspace_id
).prefetch_related(
'project',
'project__channel',
'purchase_channel',
'purchase_channel__channel',
'creative',
'placement',
'placement__project',
'placement__project__channel',
'placement__channel',
'placement__creative',
'post',
'post__channel',
)
async def create_placement(self, placement: domain.Placement) -> None:
await placement.save()
async def update_placement(self, placement: domain.Placement) -> None:
await placement.save()
async def get_workspace_placements(
async def get_workspace_publications(
self,
workspace_id: uuid.UUID,
project_id: uuid.UUID | None = None,
@@ -471,75 +397,85 @@ class Postgres(DatabaseBase, Database):
allowed_project_ids: set[uuid.UUID] | None = None,
date_from: datetime.datetime | None = None,
date_to: datetime.datetime | None = None,
) -> list[domain.Placement]:
query = domain.Placement.filter(project__workspace_id=workspace_id)
) -> list[domain.Publication]:
query = domain.Publication.filter(placement__project__workspace_id=workspace_id)
if project_id:
query = query.filter(project_id=project_id)
query = query.filter(placement__project_id=project_id)
elif allowed_project_ids is not None:
if not allowed_project_ids:
return []
query = query.filter(project_id__in=list(allowed_project_ids))
query = query.filter(placement__project_id__in=list(allowed_project_ids))
if placement_channel_id:
query = query.filter(purchase_channel__channel_id=placement_channel_id)
query = query.filter(placement__channel_id=placement_channel_id)
if creative_id:
query = query.filter(creative_id=creative_id)
query = query.filter(placement__creative_id=creative_id)
if date_from:
query = query.filter(wanted_placement_date__gte=date_from)
query = query.filter(created_at__gte=date_from)
if date_to:
query = query.filter(wanted_placement_date__lte=date_to)
query = query.filter(created_at__lte=date_to)
if not include_archived:
query = query.filter(status=domain.PlacementStatus.ACTIVE)
query = query.filter(status=domain.PublicationStatus.ACTIVE)
return (
await query.prefetch_related(
'project',
'project__channel',
'purchase_channel',
'purchase_channel__channel',
'purchase_channel__purchase',
'creative',
'placement',
'placement__project',
'placement__project__channel',
'placement__channel',
'placement__creative',
'post',
'post__channel',
)
.order_by('-wanted_placement_date')
.order_by('-created_at')
.all()
)
async def delete_placement(self, placement_id: uuid.UUID) -> None:
await domain.Placement.filter(id=placement_id).delete()
async def create_publication(self, publication: domain.Publication) -> None:
await publication.save()
async def get_placement_by_invite_link(self, invite_link: str) -> domain.Placement | None:
return await domain.Placement.get_or_none(
purchase_channel__invite_link=invite_link
).prefetch_related('project', 'project__channel', 'purchase_channel', 'purchase_channel__channel', 'creative')
async def update_publication(self, publication: domain.Publication) -> None:
await publication.save()
async def delete_publication(self, publication_id: uuid.UUID) -> None:
await domain.Publication.filter(id=publication_id).delete()
async def get_publication_by_invite_link(self, invite_link: str) -> domain.Publication | None:
return await domain.Publication.get_or_none(placement__invite_link=invite_link).prefetch_related(
'placement',
'placement__project',
'placement__project__channel',
'placement__channel',
'placement__creative',
)
# Subscription methods
async def create_subscription(self, subscription: domain.Subscription) -> None:
await subscription.save()
async def get_subscription_by_subscriber_and_placement(
self, telegram_user_id: uuid.UUID, placement_id: uuid.UUID
async def get_subscription_by_subscriber_and_publication(
self, telegram_user_id: uuid.UUID, publication_id: uuid.UUID
) -> domain.Subscription | None:
return await domain.Subscription.get_or_none(
telegram_user_id=telegram_user_id, placement_id=placement_id
telegram_user_id=telegram_user_id, publication_id=publication_id
)
async def update_subscription(self, subscription: domain.Subscription) -> None:
await subscription.save()
async def get_subscriptions_for_placements(
async def get_subscriptions_for_publications(
self,
placement_ids: list[uuid.UUID],
publication_ids: list[uuid.UUID],
*,
date_from: datetime.datetime | None = None,
date_to: datetime.datetime | None = None,
) -> list[domain.Subscription]:
if not placement_ids:
if not publication_ids:
return []
query = domain.Subscription.filter(placement_id__in=placement_ids)
query = domain.Subscription.filter(publication_id__in=publication_ids)
if date_from:
query = query.filter(created_at__gte=date_from)
@@ -554,10 +490,10 @@ class Postgres(DatabaseBase, Database):
return (
await domain.Subscription.filter(
telegram_user_id=telegram_user_id,
placement__project_id=project_id,
publication__placement__project_id=project_id,
status=domain.SubscriptionStatus.ACTIVE,
)
.prefetch_related('placement', 'telegram_user')
.prefetch_related('publication', 'telegram_user')
.all()
)
@@ -567,10 +503,10 @@ class Postgres(DatabaseBase, Database):
return (
await domain.Subscription.filter(
telegram_user_id=telegram_user_id,
placement__project_id=project_id,
publication__placement__project_id=project_id,
status=domain.SubscriptionStatus.ACTIVE,
)
.prefetch_related('placement', 'telegram_user')
.prefetch_related('publication', 'telegram_user')
.first()
)
@@ -625,43 +561,43 @@ class Postgres(DatabaseBase, Database):
return results
# Count methods
async def count_placements_by_creative(self, creative_id: uuid.UUID) -> int:
return await domain.Placement.filter(creative_id=creative_id).count()
async def count_publications_by_creative(self, creative_id: uuid.UUID) -> int:
return await domain.Publication.filter(placement__creative_id=creative_id).count()
async def count_subscriptions_by_placement(self, placement_id: uuid.UUID) -> int:
return await domain.Subscription.filter(placement_id=placement_id).count()
async def count_subscriptions_by_publication(self, publication_id: uuid.UUID) -> int:
return await domain.Subscription.filter(publication_id=publication_id).count()
async def count_placements_by_creative_batch(self, creative_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]:
async def count_publications_by_creative_batch(self, creative_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]:
if not creative_ids:
return {}
from tortoise.functions import Count
results = (
await domain.Placement.filter(creative_id__in=creative_ids)
.group_by('creative_id')
await domain.Publication.filter(placement__creative_id__in=creative_ids)
.group_by('placement__creative_id')
.annotate(count=Count('id'))
.values('creative_id', 'count')
.values('placement__creative_id', 'count')
)
counts = {row['creative_id']: row['count'] for row in results}
counts = {row['placement__creative_id']: row['count'] for row in results}
return {cid: counts.get(cid, 0) for cid in creative_ids}
async def count_subscriptions_by_placement_batch(self, placement_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]:
if not placement_ids:
async def count_subscriptions_by_publication_batch(self, publication_ids: list[uuid.UUID]) -> dict[uuid.UUID, int]:
if not publication_ids:
return {}
from tortoise.functions import Count
results = (
await domain.Subscription.filter(placement_id__in=placement_ids)
.group_by('placement_id')
await domain.Subscription.filter(publication_id__in=publication_ids)
.group_by('publication_id')
.annotate(count=Count('id'))
.values('placement_id', 'count')
.values('publication_id', 'count')
)
counts = {row['placement_id']: row['count'] for row in results}
return {pid: counts.get(pid, 0) for pid in placement_ids}
counts = {row['publication_id']: row['count'] for row in results}
return {pid: counts.get(pid, 0) for pid in publication_ids}
async def has_placements_for_creative(self, creative_id: uuid.UUID) -> bool:
return await domain.Placement.filter(creative_id=creative_id).exists()
async def has_publications_for_creative(self, creative_id: uuid.UUID) -> bool:
return await domain.Publication.filter(placement__creative_id=creative_id).exists()