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))
+}