закуп в боте
This commit is contained in:
@@ -1,14 +1,18 @@
|
|||||||
package screens
|
package screens
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"example.com/m/adapter/backend"
|
||||||
"example.com/m/bot"
|
"example.com/m/bot"
|
||||||
"example.com/m/ui"
|
"example.com/m/ui"
|
||||||
"github.com/NicoNex/echotron/v3"
|
"github.com/NicoNex/echotron/v3"
|
||||||
|
"github.com/rs/zerolog/log"
|
||||||
)
|
)
|
||||||
|
|
||||||
type PurchaseOptionalDetails struct {
|
type PurchaseOptionalDetails struct {
|
||||||
@@ -202,6 +206,18 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
|
|||||||
Channels: s.Channels,
|
Channels: s.Channels,
|
||||||
BackState: s.BackState,
|
BackState: s.BackState,
|
||||||
}, bot.EditMessage)
|
}, 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:
|
default:
|
||||||
if s.handleParamCallback(b, u.CallbackQuery.Data) {
|
if s.handleParamCallback(b, u.CallbackQuery.Data) {
|
||||||
return
|
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) 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) {
|
func (s *PurchaseOptionalDetails) SetDateTimeSelection(key string, value time.Time) {
|
||||||
switch key {
|
switch key {
|
||||||
case "placement_datetime":
|
case "placement_datetime":
|
||||||
@@ -1190,3 +1220,243 @@ func (s *PurchaseOptionalDetails) backAction() string {
|
|||||||
}
|
}
|
||||||
return "back_to_optional"
|
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("<b>Канал:</b> %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(`<a\s+class="tg-link">([^<]*)</a>`)
|
||||||
|
return re.ReplaceAllString(text, fmt.Sprintf(`<a href="%s">$1</a>`, inviteLink))
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user