закупы в бэк

This commit is contained in:
Artem Tsyrulnikov
2026-01-06 19:25:01 +03:00
parent cb6bdd5580
commit 87643735a1
10 changed files with 334 additions and 30 deletions

View File

@@ -304,8 +304,12 @@ class Postgres(DatabaseBase, Database):
invite_link_type: domain.purchase.InviteLinkType,
*,
status: domain.PurchaseChannelStatus | None = None,
planned_cost: float | 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,
@@ -313,10 +317,18 @@ class Postgres(DatabaseBase, Database):
}
if status is not None:
defaults['status'] = status
if planned_cost is not None:
defaults['planned_cost'] = planned_cost
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,

View File

@@ -13,12 +13,17 @@ if TYPE_CHECKING:
from .workspace import Workspace
class PurchaseStatus(str, enum.Enum):
class PurchaseStatus(enum.StrEnum):
ACTIVE = 'active'
ARCHIVED = 'archived'
class PurchaseChannelStatus(str, enum.Enum):
class CostType(enum.StrEnum):
FIXED = 'fixed'
CPM = 'cpm'
class PurchaseChannelStatus(enum.StrEnum):
PLANNED = 'planned'
APPROVED = 'approved'
REJECTED = 'rejected'
@@ -26,8 +31,21 @@ class PurchaseChannelStatus(str, enum.Enum):
COMPLETED = 'completed'
class PurchaseType(enum.StrEnum):
SELF_PROMO = 'self_promo'
STANDARD = 'standard'
class Purchase(TimestampedModel):
status = fields.CharEnumField(PurchaseStatus, default=PurchaseStatus.ACTIVE)
placement_at = fields.DatetimeField(null=True)
payment_at = fields.DatetimeField(null=True)
cost_type = fields.CharEnumField(CostType, null=True, max_length=8)
cost_value = fields.FloatField(null=True)
cost_before_bargain = fields.FloatField(null=True)
purchase_type = fields.CharEnumField(PurchaseType, null=True, max_length=16)
format = fields.TextField(null=True)
comment = fields.TextField(null=True)
project: fields.ForeignKeyRelation['Project'] = fields.ForeignKeyField(
'models.Project', related_name='purchases', on_delete=fields.CASCADE, index=True
@@ -45,14 +63,18 @@ class Purchase(TimestampedModel):
indexes = (('project', 'status'),)
class InviteLinkType(str, enum.Enum):
class InviteLinkType(enum.StrEnum):
PUBLIC = 'public' # открытая ссылка
APPROVAL = 'approval' # с одобрением ботом
class PurchaseChannel(TimestampedModel):
status = fields.CharEnumField(PurchaseChannelStatus, default=PurchaseChannelStatus.PLANNED)
planned_cost = fields.FloatField(null=True)
placement_at = fields.DatetimeField(null=True)
cost_type = fields.CharEnumField(CostType, null=True, max_length=8)
cost_value = fields.FloatField(null=True)
cost_before_bargain = fields.FloatField(null=True)
format = fields.TextField(null=True)
comment = fields.TextField(null=True)
invite_link = fields.CharField(max_length=512)

View File

@@ -17,6 +17,9 @@ __all__ = (
'AttachChannelToWorkspaceInput',
'PurchaseOutput',
'PurchaseChannelOutput',
'PurchaseDetails',
'PurchaseChannelDetails',
'CostInfo',
'CreatePurchaseInput',
'CreatePurchaseChannelInput',
'GetPurchasesInput',
@@ -117,12 +120,15 @@ from .project import (
UpdateProjectPermissionsInput,
)
from .purchase import (
CostInfo,
CreatePurchaseChannelInput,
CreatePurchaseInput,
GetPurchaseInput,
GetPurchasesInput,
GetPurchasesOutput,
PurchaseChannelDetails,
PurchaseChannelOutput,
PurchaseDetails,
PurchaseOutput,
)
from .user import UserOutput

View File

@@ -1,20 +1,43 @@
import datetime
import uuid
import pydantic
from src.domain.purchase import InviteLinkType, PurchaseChannelStatus, PurchaseStatus
from src.domain.purchase import CostType, InviteLinkType, PurchaseChannelStatus, PurchaseStatus, PurchaseType
from .channel import ChannelOutput
class CostInfo(pydantic.BaseModel):
type: CostType
value: float
class PurchaseDetails(pydantic.BaseModel):
placement_at: datetime.datetime | None = None
payment_at: datetime.datetime | None = None
cost: CostInfo | None = None
cost_before_bargain: float | None = None
purchase_type: PurchaseType | None = None
format: str | None = None
comment: str | None = None
class PurchaseChannelDetails(pydantic.BaseModel):
placement_at: datetime.datetime | None = None
cost: CostInfo | None = None
cost_before_bargain: float | None = None
format: str | None = None
class PurchaseChannelOutput(pydantic.BaseModel):
id: uuid.UUID
status: PurchaseChannelStatus
planned_cost: float | None = None
comment: str | None = None
invite_link: str
invite_link_type: InviteLinkType
channel: ChannelOutput
details: PurchaseChannelDetails | None = None
class PurchaseOutput(pydantic.BaseModel):
@@ -22,18 +45,20 @@ class PurchaseOutput(pydantic.BaseModel):
status: PurchaseStatus
creative_id: uuid.UUID
channels: list[PurchaseChannelOutput]
details: PurchaseDetails | None = None
class CreatePurchaseChannelInput(pydantic.BaseModel):
username: str = pydantic.Field(pattern=r'^[^@]')
status: PurchaseChannelStatus | None = None
planned_cost: float | None = None
comment: str | None = None
details: PurchaseChannelDetails | None = None
class CreatePurchaseInput(pydantic.BaseModel):
creative_id: uuid.UUID
channels: list[CreatePurchaseChannelInput]
details: PurchaseDetails | None = None
class GetPurchasesInput(pydantic.BaseModel):

View File

@@ -174,8 +174,12 @@ class Database(typing.Protocol):
invite_link_type: domain.purchase.InviteLinkType,
*,
status: domain.PurchaseChannelStatus | None = None,
planned_cost: float | 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: ...
async def update_purchase_channel(self, purchase_channel: domain.PurchaseChannel) -> None: ...

View File

@@ -11,6 +11,39 @@ if TYPE_CHECKING:
log = logging.getLogger(__name__)
def _build_cost_info(cost_type: domain.purchase.CostType | None, cost_value: float | None) -> dto.CostInfo | None:
if cost_type is None or cost_value is None:
return None
return dto.CostInfo(type=cost_type, value=cost_value)
def _build_purchase_details(purchase: domain.Purchase) -> dto.PurchaseDetails | None:
details = dto.PurchaseDetails(
placement_at=purchase.placement_at,
payment_at=purchase.payment_at,
cost=_build_cost_info(purchase.cost_type, purchase.cost_value),
cost_before_bargain=purchase.cost_before_bargain,
purchase_type=purchase.purchase_type,
format=purchase.format,
comment=purchase.comment,
)
if details.model_dump(exclude_none=True):
return details
return None
def _build_purchase_channel_details(purchase_channel: domain.PurchaseChannel) -> dto.PurchaseChannelDetails | None:
details = dto.PurchaseChannelDetails(
placement_at=purchase_channel.placement_at,
cost=_build_cost_info(purchase_channel.cost_type, purchase_channel.cost_value),
cost_before_bargain=purchase_channel.cost_before_bargain,
format=purchase_channel.format,
)
if details.model_dump(exclude_none=True):
return details
return None
def _build_purchase_output(
purchase: domain.Purchase, purchase_channels: list[domain.PurchaseChannel]
) -> dto.PurchaseOutput:
@@ -24,7 +57,6 @@ def _build_purchase_output(
dto.PurchaseChannelOutput(
id=purchase_channel.id,
status=purchase_channel.status,
planned_cost=purchase_channel.planned_cost,
comment=purchase_channel.comment,
invite_link=purchase_channel.invite_link,
invite_link_type=purchase_channel.invite_link_type,
@@ -34,6 +66,7 @@ def _build_purchase_output(
title=channel.title,
username=channel.username,
),
details=_build_purchase_channel_details(purchase_channel),
)
)
@@ -42,6 +75,7 @@ def _build_purchase_output(
status=purchase.status,
creative_id=purchase.creative_id,
channels=channels_output,
details=_build_purchase_details(purchase),
)
@@ -69,7 +103,20 @@ async def create_purchase(
channels = await self.database.get_purchase_channels(existing_purchase.id)
return _build_purchase_output(existing_purchase, channels)
purchase = domain.Purchase(workspace_id=workspace_id, project_id=project.id, creative_id=creative.id)
details = input.details
purchase = domain.Purchase(
workspace_id=workspace_id,
project_id=project.id,
creative_id=creative.id,
placement_at=details.placement_at if details else None,
payment_at=details.payment_at if details else None,
cost_type=details.cost.type if details and details.cost else None,
cost_value=details.cost.value if details and details.cost else None,
cost_before_bargain=details.cost_before_bargain if details else None,
purchase_type=details.purchase_type if details else None,
format=details.format if details else None,
comment=details.comment if details else None,
)
await self.database.create_purchase(purchase)
if project.channel.telegram_id is None:
@@ -94,14 +141,19 @@ async def create_purchase(
invite_link = await self.telegram_bot.create_chat_invite_link(project.channel.telegram_id, requires_approval)
channel_details = channel_input.details
purchase_channel = await self.database.add_channel_to_purchase(
purchase.id,
channel.id,
invite_link=invite_link,
invite_link_type=invite_link_type,
status=channel_input.status,
planned_cost=channel_input.planned_cost,
comment=channel_input.comment,
placement_at=channel_details.placement_at if channel_details else None,
cost_type=channel_details.cost.type if channel_details and channel_details.cost else None,
cost_value=channel_details.cost.value if channel_details and channel_details.cost else None,
cost_before_bargain=channel_details.cost_before_bargain if channel_details else None,
format=channel_details.format if channel_details else None,
)
purchase_channel.channel = channel
purchase_channels.append(purchase_channel)