From 26a579266531109750a6eae510bd546119cf184b Mon Sep 17 00:00:00 2001 From: Artem Tsyrulnikov Date: Tue, 3 Feb 2026 14:12:23 +0300 Subject: [PATCH] =?UTF-8?q?=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/dto/purchase.py | 1 - src/usecase/purchase/create_placements.py | 33 ++--- tg_bot/backend/backend_client.go | 1 - tg_bot/screens/purchase_optional_details.go | 124 +++++++----------- .../adapter/telegram/get_channel_diff.go | 4 +- 5 files changed, 62 insertions(+), 101 deletions(-) diff --git a/src/dto/purchase.py b/src/dto/purchase.py index 81ccb59..26dee9e 100644 --- a/src/dto/purchase.py +++ b/src/dto/purchase.py @@ -81,7 +81,6 @@ class CreatePlacementsInput(pydantic.BaseModel): creative_id: uuid.UUID | None = None channels: list[CreatePlacementChannelInput] - details: PlacementDetails | None = None # Общие детали для всех placements class GetPlacementsInput(pydantic.BaseModel): diff --git a/src/usecase/purchase/create_placements.py b/src/usecase/purchase/create_placements.py index 6bdcff9..5430473 100644 --- a/src/usecase/purchase/create_placements.py +++ b/src/usecase/purchase/create_placements.py @@ -178,7 +178,6 @@ async def create_placements( if project.channel.telegram_id is None: raise domain.ChannelNotFound(project.channel.id) - details = input.details placements: list[domain.Placement] = [] creatives_by_id: dict[uuid.UUID, domain.Creative] = {} @@ -187,11 +186,9 @@ async def create_placements( if not channel: raise domain.ChannelNotFound(channel_input.channel_id) - # Объединяем общие детали с деталями конкретного канала channel_details = channel_input.details creative_id = ( (channel_details.creative_id if channel_details and channel_details.creative_id else None) - or (details.creative_id if details and details.creative_id else None) or input.creative_id ) @@ -211,34 +208,30 @@ async def create_placements( channel_id=channel.id, invite_link=None, invite_link_type=( - channel_details.invite_link_type if channel_details and channel_details.invite_link_type else None + channel_details.invite_link_type + if channel_details and channel_details.invite_link_type + else None ) - or (details.invite_link_type if details and details.invite_link_type else None) or project.purchase_invite_type_default, status=channel_input.status or domain.PlacementStatus.NO_STATUS, - comment=channel_input.comment, - # Приоритет: channel details > общие details - placement_at=(channel_details.placement_at if channel_details else None) - or (details.placement_at if details else None), - payment_at=details.payment_at if details else None, - cost_type=(channel_details.cost.type if channel_details and channel_details.cost else None) - or (details.cost.type if details and details.cost else None), - cost_value=(channel_details.cost.value if channel_details and channel_details.cost else None) - or (details.cost.value if details and details.cost else None), + comment=channel_input.comment + or (channel_details.comment if channel_details else None), + placement_at=channel_details.placement_at if channel_details else None, + payment_at=channel_details.payment_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_type=( channel_details.cost_before_bargain.type if channel_details and channel_details.cost_before_bargain else None - ) - or (details.cost_before_bargain.type if details and details.cost_before_bargain else None), + ), cost_before_bargain=( channel_details.cost_before_bargain.value if channel_details and channel_details.cost_before_bargain else None - ) - or (details.cost_before_bargain.value if details and details.cost_before_bargain else None), - placement_type=details.placement_type if details else None, - format=(channel_details.format if channel_details else None) or (details.format if details else None), + ), + placement_type=channel_details.placement_type if channel_details else None, + format=channel_details.format if channel_details else None, ) await self.database.create_placement(placement) diff --git a/tg_bot/backend/backend_client.go b/tg_bot/backend/backend_client.go index 3371fa4..db2fd83 100644 --- a/tg_bot/backend/backend_client.go +++ b/tg_bot/backend/backend_client.go @@ -645,7 +645,6 @@ type CreatePlacementChannelInput struct { type CreatePlacementsInput struct { CreativeID *string `json:"creative_id,omitempty"` Channels []CreatePlacementChannelInput `json:"channels"` - Details *PlacementDetails `json:"details,omitempty"` } type GetPlacementsOutput struct { diff --git a/tg_bot/screens/purchase_optional_details.go b/tg_bot/screens/purchase_optional_details.go index 3d0d010..66c833a 100644 --- a/tg_bot/screens/purchase_optional_details.go +++ b/tg_bot/screens/purchase_optional_details.go @@ -2193,14 +2193,9 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) { return } - placementDetails := s.buildPlacementDetails(placementType) - if placementDetails != nil && s.isPlacementDetailsEmpty(placementDetails) { - placementDetails = nil - } - apiChannels := make([]backend.CreatePlacementChannelInput, 0, len(s.Channels)) for _, ch := range s.Channels { - channelDetails := s.buildChannelDetails(channelKey(ch)) + channelDetails := s.buildChannelDetails(channelKey(ch), placementType) if channelDetails != nil && s.isChannelDetailsEmpty(channelDetails) { channelDetails = nil } @@ -2214,7 +2209,6 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) { input := backend.CreatePlacementsInput{ CreativeID: &s.CreativeID, Channels: apiChannels, - Details: placementDetails, } placements, err := b.Backend.CreatePlacements(context.Background(), jwt, b.Session.WorkspaceID, s.ProjectID, input) @@ -2247,102 +2241,82 @@ func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) { }, bot.NewMessage) } -func (s *PurchaseOptionalDetails) buildPlacementDetails(placementType *string) *backend.PlacementDetails { +func (s *PurchaseOptionalDetails) buildChannelDetails(channelKey string, placementType *string) *backend.PlacementDetails { details := &backend.PlacementDetails{} - if s.PlacementMode != "per_channel" || len(s.Channels) <= 1 { - if s.PlacementDateTime != nil { - formatted := s.PlacementDateTime.Format(time.RFC3339) - details.PlacementAt = &formatted - } - } - if s.PaymentDateMode != "per_channel" || len(s.Channels) <= 1 { - if s.PaymentDate != nil { - formatted := s.PaymentDate.Format(time.RFC3339) - details.PaymentAt = &formatted - } - } - if s.CostMode != "per_channel" || len(s.Channels) <= 1 { - details.Cost = s.buildCostInfo(s.CostType, s.CostValue) - } - if s.CostBeforeMode != "per_channel" || len(s.Channels) <= 1 { - if s.CostBeforeBargain != nil && s.CostBeforeBargain.Value != nil { - details.CostBeforeBargain = s.buildCostInfo(s.CostBeforeBargain.Type, s.CostBeforeBargain.Value) - } - } - if s.PurchaseTypeMode != "per_channel" || len(s.Channels) <= 1 { - if s.PurchaseType != "" { - details.PlacementType = &s.PurchaseType - } - } - if s.CommentMode != "per_channel" || len(s.Channels) <= 1 { - if s.Comment != "" { - details.Comment = &s.Comment - } - } - if s.FormatMode != "per_channel" || len(s.Channels) <= 1 { - if s.Format != "" { - details.Format = &s.Format - } - } - if s.InviteLinkTypeMode != "per_channel" || len(s.Channels) <= 1 { - if s.InviteLinkType != "" { - details.InviteLinkType = &s.InviteLinkType - } - } - if placementType != nil && (s.PurchaseTypeMode == "per_channel" || len(s.Channels) <= 1) { - details.PlacementType = placementType - } - - return details -} - -func (s *PurchaseOptionalDetails) buildChannelDetails(channelKey string) *backend.PlacementDetails { - if len(s.Channels) <= 1 { - return nil - } - - details := &backend.PlacementDetails{} - if s.PlacementMode == "per_channel" { + // Placement date + if s.PlacementMode == "per_channel" && len(s.Channels) > 1 { if value, ok := s.PlacementByChannel[channelKey]; ok && value != nil { formatted := value.Format(time.RFC3339) details.PlacementAt = &formatted } + } else if s.PlacementDateTime != nil { + formatted := s.PlacementDateTime.Format(time.RFC3339) + details.PlacementAt = &formatted } - if s.PaymentDateMode == "per_channel" { + + // Payment date + if s.PaymentDateMode == "per_channel" && len(s.Channels) > 1 { if value, ok := s.PaymentDateByChannel[channelKey]; ok && value != nil { formatted := value.Format(time.RFC3339) details.PaymentAt = &formatted } + } else if s.PaymentDate != nil { + formatted := s.PaymentDate.Format(time.RFC3339) + details.PaymentAt = &formatted } - if s.CostMode == "per_channel" { + + // Cost + if s.CostMode == "per_channel" && len(s.Channels) > 1 { entry := s.CostByChannel[channelKey] details.Cost = s.buildCostInfo(entry.Type, entry.Value) + } else { + details.Cost = s.buildCostInfo(s.CostType, s.CostValue) } - if s.CostBeforeMode == "per_channel" { + + // Cost before bargain + if s.CostBeforeMode == "per_channel" && len(s.Channels) > 1 { if entry, ok := s.CostBeforeByChannel[channelKey]; ok && entry.Value != nil { details.CostBeforeBargain = s.buildCostInfo(entry.Type, entry.Value) } + } else if s.CostBeforeBargain != nil && s.CostBeforeBargain.Value != nil { + details.CostBeforeBargain = s.buildCostInfo(s.CostBeforeBargain.Type, s.CostBeforeBargain.Value) } - if s.PurchaseTypeMode == "per_channel" { + + // Placement type + if s.PurchaseTypeMode == "per_channel" && len(s.Channels) > 1 { if value, ok := s.PurchaseTypeByChannel[channelKey]; ok && value != "" { details.PlacementType = &value } + } else if placementType != nil { + details.PlacementType = placementType } - if s.CommentMode == "per_channel" { + + // Comment + if s.CommentMode == "per_channel" && len(s.Channels) > 1 { if value, ok := s.CommentByChannel[channelKey]; ok && value != "" { details.Comment = &value } + } else if s.Comment != "" { + details.Comment = &s.Comment } - if s.FormatMode == "per_channel" { + + // Format + if s.FormatMode == "per_channel" && len(s.Channels) > 1 { if value, ok := s.FormatByChannel[channelKey]; ok && value != "" { details.Format = &value } + } else if s.Format != "" { + details.Format = &s.Format } - if s.InviteLinkTypeMode == "per_channel" { + + // Invite link type + if s.InviteLinkTypeMode == "per_channel" && len(s.Channels) > 1 { if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok && value != "" { details.InviteLinkType = &value } + } else if s.InviteLinkType != "" { + details.InviteLinkType = &s.InviteLinkType } return details @@ -2362,21 +2336,15 @@ func (s *PurchaseOptionalDetails) buildCostInfo(costType string, value *float64) } } -func (s *PurchaseOptionalDetails) isPlacementDetailsEmpty(details *backend.PlacementDetails) bool { +func (s *PurchaseOptionalDetails) isChannelDetailsEmpty(details *backend.PlacementDetails) bool { return details.PlacementAt == nil && details.PaymentAt == nil && details.Cost == nil && details.CostBeforeBargain == nil && details.PlacementType == nil && details.Format == nil && - details.Comment == nil -} - -func (s *PurchaseOptionalDetails) isChannelDetailsEmpty(details *backend.PlacementDetails) bool { - return details.PlacementAt == nil && - details.Cost == nil && - details.CostBeforeBargain == nil && - details.Format == nil + details.Comment == nil && + details.InviteLinkType == nil } func (s *PurchaseOptionalDetails) normalizePurchaseType() (*string, bool) { diff --git a/tg_parser/internal/adapter/telegram/get_channel_diff.go b/tg_parser/internal/adapter/telegram/get_channel_diff.go index 9fcb405..28f02b4 100644 --- a/tg_parser/internal/adapter/telegram/get_channel_diff.go +++ b/tg_parser/internal/adapter/telegram/get_channel_diff.go @@ -95,7 +95,8 @@ func extractChannelMeta(currentChannel domain.Channel, chats []tg.ChatClass) *do continue } - username := "" + // Preserve username if Telegram doesn't provide a new one (private channels may not have username) + username := currentChannel.Username if usernameVal, ok := ch.GetUsername(); ok && usernameVal != "" { username = usernameVal } @@ -112,6 +113,7 @@ func extractChannelMeta(currentChannel domain.Channel, chats []tg.ChatClass) *do Username: username, Title: ch.Title, AccessHash: accessHash, + InviteLink: currentChannel.InviteLink, // Preserve invite link (never returned in channel diff) Pts: currentChannel.Pts, IsAccessible: currentChannel.IsAccessible, }