diff --git a/telegram_bot/screens/purchase_optional_details.go b/telegram_bot/screens/purchase_optional_details.go index 8c2717a..3d06f97 100644 --- a/telegram_bot/screens/purchase_optional_details.go +++ b/telegram_bot/screens/purchase_optional_details.go @@ -1,14 +1,18 @@ package screens import ( + "context" "fmt" + "regexp" "strconv" "strings" "time" + "example.com/m/adapter/backend" "example.com/m/bot" "example.com/m/ui" "github.com/NicoNex/echotron/v3" + "github.com/rs/zerolog/log" ) type PurchaseOptionalDetails struct { @@ -202,6 +206,18 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update) Channels: s.Channels, BackState: s.BackState, }, bot.EditMessage) + case "next": + jwt, err := s.ensureJWT(b) + if err != nil { + log.Error().Err(err).Msg("Failed to get JWT") + b.SendNew("❌ Ошибка авторизации. Попробуйте /start", bot.Keyboard()) + return + } + s.createPurchase(b, jwt) + case "done": + if s.BackState != nil { + b.SetState(s.BackState, bot.EditMessage) + } default: if s.handleParamCallback(b, u.CallbackQuery.Data) { return @@ -269,6 +285,20 @@ func (s *PurchaseOptionalDetails) HandleMessage(b *bot.Bot, u *echotron.Update) func (s *PurchaseOptionalDetails) Handle(_ *bot.Bot, _ *echotron.Update) { return } +func (s *PurchaseOptionalDetails) ensureJWT(b *bot.Bot) (string, error) { + if b.Session.JWT != "" { + return b.Session.JWT, nil + } + + jwt, err := b.Backend.GetJWTByTelegramID(context.Background(), b.ChatID) + if err != nil { + return "", err + } + + b.Session.JWT = jwt + return jwt, nil +} + func (s *PurchaseOptionalDetails) SetDateTimeSelection(key string, value time.Time) { switch key { case "placement_datetime": @@ -1190,3 +1220,243 @@ func (s *PurchaseOptionalDetails) backAction() string { } return "back_to_optional" } + +func (s *PurchaseOptionalDetails) createPurchase(b *bot.Bot, jwt string) { + if len(s.Channels) == 0 { + b.SendNew("❌ Добавьте хотя бы один канал", bot.Keyboard( + bot.Row(bot.Button("← Назад", "back")), + )) + return + } + + purchaseType, ok := s.normalizePurchaseType() + if !ok { + b.SendNew("❌ Тип закупа: используйте «самопиар» или «стандарт»", bot.Keyboard( + bot.Row(bot.Button("← Назад", "back")), + )) + return + } + + purchaseDetails := s.buildPurchaseDetails(purchaseType) + if purchaseDetails != nil && s.isPurchaseDetailsEmpty(purchaseDetails) { + purchaseDetails = nil + } + + apiChannels := make([]backend.CreatePurchaseChannelInput, 0, len(s.Channels)) + for _, ch := range s.Channels { + channelDetails := s.buildChannelDetails(ch.Username) + if channelDetails != nil && s.isChannelDetailsEmpty(channelDetails) { + channelDetails = nil + } + apiChannels = append(apiChannels, backend.CreatePurchaseChannelInput{ + Username: ch.Username, + Comment: ch.Comment, + Details: channelDetails, + }) + } + + input := backend.CreatePurchaseInput{ + CreativeID: s.CreativeID, + Channels: apiChannels, + Details: purchaseDetails, + } + + purchase, err := b.Backend.CreatePurchase( + context.Background(), + jwt, + s.WorkspaceID, + s.ProjectID, + input, + ) + if err != nil { + log.Error().Err(err).Msg("Failed to create purchase") + b.SendNew("❌ Не удалось создать закуп", bot.Keyboard( + bot.Row(bot.Button("← Назад", "back")), + )) + return + } + + creative, err := b.Backend.GetCreative( + context.Background(), + jwt, + s.WorkspaceID, + s.CreativeID, + ) + if err != nil { + log.Error().Err(err).Msg("Failed to get creative") + creative = nil + } + + if creative != nil { + for _, ch := range purchase.Channels { + channelName := s.formatChannelName(ch) + header := fmt.Sprintf("Канал: %s", channelName) + b.SendMessage(header, b.ChatID, &echotron.MessageOptions{ParseMode: echotron.HTML}) + + s.sendCreativeWithInviteLink(b, creative, ch.InviteLink) + } + } + + b.SendNew("✅ Закуп создан", bot.Keyboard( + bot.Row(bot.Button("● Готово", "done")), + )) +} + +func (s *PurchaseOptionalDetails) buildPurchaseDetails(purchaseType *string) *backend.PurchaseDetails { + details := &backend.PurchaseDetails{} + + if s.PlacementMode != "per_channel" || len(s.Channels) <= 1 { + if s.PlacementDateTime != nil { + formatted := s.PlacementDateTime.Format(time.RFC3339) + details.PlacementAt = &formatted + } + } + 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 { + details.CostBeforeBargain = s.CostBeforeBargain + } + } + if s.FormatMode != "per_channel" || len(s.Channels) <= 1 { + if s.Format != "" { + details.Format = &s.Format + } + } + if s.Comment != "" { + details.Comment = &s.Comment + } + if purchaseType != nil { + details.PurchaseType = purchaseType + } + + return details +} + +func (s *PurchaseOptionalDetails) buildChannelDetails(username string) *backend.PurchaseChannelDetails { + if len(s.Channels) <= 1 { + return nil + } + + details := &backend.PurchaseChannelDetails{} + if s.PlacementMode == "per_channel" { + if value, ok := s.PlacementByChannel[username]; ok && value != nil { + formatted := value.Format(time.RFC3339) + details.PlacementAt = &formatted + } + } + if s.CostMode == "per_channel" { + entry := s.CostByChannel[username] + details.Cost = s.buildCostInfo(entry.Type, entry.Value) + } + if s.CostBeforeMode == "per_channel" { + if value, ok := s.CostBeforeByChannel[username]; ok && value != nil { + details.CostBeforeBargain = value + } + } + if s.FormatMode == "per_channel" { + if value, ok := s.FormatByChannel[username]; ok && value != "" { + details.Format = &value + } + } + + return details +} + +func (s *PurchaseOptionalDetails) buildCostInfo(costType string, value *float64) *backend.CostInfo { + if value == nil { + return nil + } + normalized := "fixed" + if costType == "cpm" { + normalized = "cpm" + } + return &backend.CostInfo{ + Type: normalized, + Value: *value, + } +} + +func (s *PurchaseOptionalDetails) isPurchaseDetailsEmpty(details *backend.PurchaseDetails) bool { + return details.PlacementAt == nil && + details.PaymentAt == nil && + details.Cost == nil && + details.CostBeforeBargain == nil && + details.PurchaseType == nil && + details.Format == nil && + details.Comment == nil +} + +func (s *PurchaseOptionalDetails) isChannelDetailsEmpty(details *backend.PurchaseChannelDetails) bool { + return details.PlacementAt == nil && + details.Cost == nil && + details.CostBeforeBargain == nil && + details.Format == nil +} + +func (s *PurchaseOptionalDetails) normalizePurchaseType() (*string, bool) { + raw := strings.TrimSpace(s.PurchaseType) + if raw == "" { + return nil, true + } + normalized := strings.ToLower(raw) + normalized = strings.TrimSpace(strings.Trim(normalized, ".")) + switch normalized { + case "самопиар", "само пиар", "self_promo", "self promo", "вп", "vp": + value := "self_promo" + return &value, true + case "стандарт", "standard": + value := "standard" + return &value, true + default: + return nil, false + } +} + +func (s *PurchaseOptionalDetails) formatChannelName(ch backend.PurchaseChannelOut) string { + if ch.Channel.Title != nil && *ch.Channel.Title != "" { + return *ch.Channel.Title + } + if ch.Channel.Username != nil && *ch.Channel.Username != "" { + return "@" + *ch.Channel.Username + } + return "Без названия" +} + +func (s *PurchaseOptionalDetails) sendCreativeWithInviteLink( + b *bot.Bot, + creative *backend.Creative, + inviteLink string, +) { + linkedText := s.injectInviteLink(creative.Text, inviteLink) + editor := CreativeEditorFields{ + Text: &linkedText, + MediaType: creative.MediaType, + MediaFileID: creative.MediaFileID, + } + + if len(creative.Buttons) > 0 { + editor.Buttons = make([]InlineButton, 0, len(creative.Buttons)) + for _, btn := range creative.Buttons { + editor.Buttons = append(editor.Buttons, InlineButton{ + Text: btn.Text, + URL: btn.URL, + }) + } + } + + editor.SendCreativePreview(b) +} + +func (s *PurchaseOptionalDetails) injectInviteLink(text, inviteLink string) string { + if text == "" { + return text + } + re := regexp.MustCompile(`([^<]*)`) + return re.ReplaceAllString(text, fmt.Sprintf(`$1`, inviteLink)) +}