правки

This commit is contained in:
Artem Tsyrulnikov
2026-02-01 18:13:46 +03:00
parent 4e35c73a66
commit 52cd2ec1a0
8 changed files with 503 additions and 115 deletions

View File

@@ -550,6 +550,7 @@ type Channel struct {
TelegramID *int64 `json:"telegram_id"`
Title *string `json:"title"`
Username *string `json:"username"`
InviteLink *string `json:"invite_link"`
}
type CreateChannelInput struct {
@@ -568,6 +569,12 @@ type CreateChannelResult struct {
Error *string `json:"error,omitempty"`
}
type ProjectOutput struct {
ID string `json:"id"`
Status string `json:"status"`
Channel Channel `json:"channel"`
}
type CreateChannelsOutput struct {
Results []CreateChannelResult `json:"results"`
}
@@ -586,24 +593,30 @@ type PlacementDetails struct {
Format *string `json:"format,omitempty"`
Comment *string `json:"comment,omitempty"`
CreativeID *string `json:"creative_id,omitempty"`
CreativeName *string `json:"creative_name,omitempty"`
InviteLinkType *string `json:"invite_link_type,omitempty"`
}
type PlacementOutput struct {
ID string `json:"id"`
Status string `json:"status"`
CreativeID *string `json:"creative_id,omitempty"`
Comment *string `json:"comment,omitempty"`
InviteLink *string `json:"invite_link,omitempty"`
InviteLinkType string `json:"invite_link_type"`
Channel Channel `json:"channel"`
Details *PlacementDetails `json:"details,omitempty"`
PlacementPost *PlacementPostOutput `json:"placement_post,omitempty"`
ID string `json:"id"`
Status string `json:"status"`
CreativeID *string `json:"creative_id,omitempty"`
CreativeName *string `json:"creative_name,omitempty"`
Comment *string `json:"comment,omitempty"`
InviteLink *string `json:"invite_link,omitempty"`
InviteLinkType string `json:"invite_link_type"`
Channel Channel `json:"channel"`
Project *ProjectOutput `json:"project"`
PlacementNumber int `json:"placement_number"`
Details *PlacementDetails `json:"details,omitempty"`
PlacementPost *PlacementPostOutput `json:"placement_post,omitempty"`
}
type PlacementPostOutput struct {
SubscriptionsCount int `json:"subscriptions_count"`
ViewsCount *int `json:"views_count,omitempty"`
CreatedAt string `json:"created_at"`
TimeOnTop *int `json:"time_on_top,omitempty"`
Post PostOutput `json:"post"`
}

View File

@@ -32,62 +32,164 @@ func (s *PlacementDetails) renderDetails(b *bot.Bot, mode bot.RenderMode) {
text := "<b>Детали размещения</b>\n\n"
channelName := formatChannelName(placement.Channel)
text += fmt.Sprintf(" <b>Канал:</b> %s\n", channelName)
text += fmt.Sprintf("• <b>Статус:</b> %s\n", formatPlacementStatus(placement.Status))
text += fmt.Sprintf("• <b>Ссылка:</b> %s\n", formatInviteLinkType(placement.InviteLinkType))
// Размещение в канале
text += "📺 <b>Размещение в канале:</b>\n"
text += formatChannelWithName(placement.Channel)
text += "\n"
// Рекламируемый проект
if placement.Project != nil {
text += "🎯 <b>Рекламируемый проект:</b>\n"
text += formatProject(*placement.Project)
text += "\n"
}
// Ссылка на пост
if placementPost != nil && placementPost.Post.URL != nil && *placementPost.Post.URL != "" {
text += fmt.Sprintf("🔗 <b>Ссылка на пост:</b> %s\n", *placementPost.Post.URL)
}
// Пригласительная ссылка
if placement.InviteLink != nil && *placement.InviteLink != "" {
text += fmt.Sprintf("📩 <b>Пригласительная ссылка:</b> %s\n", *placement.InviteLink)
}
// Тип ссылки
text += fmt.Sprintf("🔓 <b>Тип ссылки:</b> %s\n", formatInviteLinkType(placement.InviteLinkType))
// Порядковый номер размещения
if placement.PlacementNumber > 0 {
text += fmt.Sprintf("🔢 <b>Размещение №%d</b> в этом канале\n", placement.PlacementNumber)
}
// Креатив
if placement.Details != nil && placement.Details.CreativeName != nil && *placement.Details.CreativeName != "" {
text += fmt.Sprintf("✨ <b>Креатив:</b> %s\n", *placement.Details.CreativeName)
} else if placement.CreativeName != nil && *placement.CreativeName != "" {
text += fmt.Sprintf("✨ <b>Креатив:</b> %s\n", *placement.CreativeName)
}
text += "\n"
// Финансовая информация
if placement.Details != nil {
if placement.Details.PlacementType != nil {
text += fmt.Sprintf("• <b>Тип:</b> %s\n", formatPlacementType(*placement.Details.PlacementType))
}
if placement.Details.PlacementAt != nil && *placement.Details.PlacementAt != "" {
text += fmt.Sprintf("• <b>Дата размещения:</b> %s\n", formatDateTime(*placement.Details.PlacementAt))
}
if placement.Details.PaymentAt != nil && *placement.Details.PaymentAt != "" {
text += fmt.Sprintf("• <b>Дата оплаты:</b> %s\n", formatDateTime(*placement.Details.PaymentAt))
}
// Стоимость
if placement.Details.Cost != nil {
text += fmt.Sprintf("• <b>Стоимость:</b> %s %.0f₽\n",
formatCostType(placement.Details.Cost.Type),
placement.Details.Cost.Value,
)
costType := formatCostType(placement.Details.Cost.Type)
text += fmt.Sprintf("💰 <b>Стоимость:</b> %s %.0f₽\n", costType, placement.Details.Cost.Value)
}
// Стоимость до торга
if placement.Details.CostBeforeBargain != nil {
text += fmt.Sprintf("• <b>До торга:</b> %s %.0f₽\n",
formatCostType(placement.Details.CostBeforeBargain.Type),
placement.Details.CostBeforeBargain.Value,
)
costType := formatCostType(placement.Details.CostBeforeBargain.Type)
text += fmt.Sprintf("💸 <b>До торга:</b> %s %.0f₽\n", costType, placement.Details.CostBeforeBargain.Value)
}
// Дата размещения
if placement.Details.PlacementAt != nil && *placement.Details.PlacementAt != "" {
text += fmt.Sprintf("📅 <b>Дата размещения:</b> %s\n", formatDateTime(*placement.Details.PlacementAt))
}
// Дата оплаты
if placement.Details.PaymentAt != nil && *placement.Details.PaymentAt != "" {
text += fmt.Sprintf("💳 <b>Дата оплаты:</b> %s\n", formatDateTime(*placement.Details.PaymentAt))
}
// Формат
if placement.Details.Format != nil && *placement.Details.Format != "" {
text += fmt.Sprintf(" <b>Формат:</b> %s\n", *placement.Details.Format)
text += fmt.Sprintf("📐 <b>Формат:</b> %s\n", *placement.Details.Format)
}
if placement.Details.Comment != nil && *placement.Details.Comment != "" {
text += fmt.Sprintf("• <b>Комментарий:</b> %s\n", *placement.Details.Comment)
// Тип закупа
if placement.Details.PlacementType != nil {
text += fmt.Sprintf("🔄 <b>Тип закупа:</b> %s\n", formatPlacementType(*placement.Details.PlacementType))
}
}
text += "\n"
// Статистика
if placementPost != nil {
text += "\n<b>Пост</b>\n"
if placementPost.Post.URL != nil && *placementPost.Post.URL != "" {
text += fmt.Sprintf(" <b>Ссылка на пост:</b> %s\n", *placementPost.Post.URL)
} else {
text += "• <b>Ссылка на пост:</b> не найдена\n"
// Кол-во подписчиков
if placementPost.SubscriptionsCount > 0 {
text += fmt.Sprintf("👥 <b>Подписчики:</b> %d\n", placementPost.SubscriptionsCount)
}
// Просмотры поста
if placementPost.ViewsCount != nil {
text += fmt.Sprintf("👁️ <b>Просмотры:</b> %d\n", *placementPost.ViewsCount)
}
// CPF (стоимость подписчика)
if placementPost.SubscriptionsCount > 0 && placement.Details != nil && placement.Details.Cost != nil {
cpf := calculateCPF(placement.Details.Cost.Value, placementPost.SubscriptionsCount)
text += fmt.Sprintf("💵 <b>CPF:</b> %.2f₽\n", cpf)
}
// Конверсия
if placementPost.ViewsCount != nil && *placementPost.ViewsCount > 0 && placementPost.SubscriptionsCount > 0 {
conversion := calculateConversion(placementPost.SubscriptionsCount, *placementPost.ViewsCount)
text += fmt.Sprintf("📊 <b>Конверсия:</b> %.1f%%\n", conversion)
}
}
// Комментарий
if placement.Details != nil && placement.Details.Comment != nil && *placement.Details.Comment != "" {
text += fmt.Sprintf("\n📝 <b>Комментарий:</b> %s\n", *placement.Details.Comment)
}
keyboard := Keyboard(Row(Button("← Назад", "back")))
b.Render(text, keyboard, mode)
}
func formatChannelName(channel backend.Channel) string {
func formatChannelWithName(channel backend.Channel) string {
var name string
if channel.Title != nil && *channel.Title != "" {
return *channel.Title
name = *channel.Title
} else if channel.Username != nil && *channel.Username != "" {
name = "@" + *channel.Username
} else {
name = "Без названия"
}
// Добавляем ссылку если есть username или invite_link
if channel.Username != nil && *channel.Username != "" {
return "@" + *channel.Username
return fmt.Sprintf(" %s (@%s)\n", name, *channel.Username)
} else if channel.InviteLink != nil && *channel.InviteLink != "" {
return fmt.Sprintf(" %s\n", name)
}
return "Без названия"
return fmt.Sprintf(" %s\n", name)
}
func formatProject(project backend.ProjectOutput) string {
var name string
if project.Channel.Title != nil && *project.Channel.Title != "" {
name = *project.Channel.Title
} else if project.Channel.Username != nil && *project.Channel.Username != "" {
name = "@" + *project.Channel.Username
} else {
name = "Без названия"
}
// Добавляем ссылку если есть username
if project.Channel.Username != nil && *project.Channel.Username != "" {
return fmt.Sprintf(" %s (@%s)\n", name, *project.Channel.Username)
}
return fmt.Sprintf(" %s\n", name)
}
func calculateCPF(cost float64, subscriptions int) float64 {
if subscriptions == 0 {
return 0
}
return cost / float64(subscriptions)
}
func calculateConversion(subscriptions int, views int) float64 {
if views == 0 {
return 0
}
return float64(subscriptions) / float64(views) * 100
}
func formatInviteLinkType(value string) string {

View File

@@ -15,48 +15,52 @@ import (
)
type PurchaseOptionalDetails struct {
ProjectID string
ProjectTitle string
CreativeID string
CreativeTitle string
Channels []PurchaseChannelInput
PlacementDateTime *time.Time
PaymentDate *time.Time
CostType string
CostValue *float64
CostBeforeType string
CostBeforeBargain *CostEntry
PurchaseType string
Format string
Comment string
InputMode string
CurrentParam string
CurrentChannel string
ParamPage int
ReturnMode string
PlacementMode string
PaymentDateMode string
CostMode string
CostBeforeMode string
PurchaseTypeMode string
CommentMode string
FormatMode string
PlacementByChannel map[string]*time.Time
PaymentDateByChannel map[string]*time.Time
CostByChannel map[string]CostEntry
CostBeforeByChannel map[string]CostEntry
PurchaseTypeByChannel map[string]string
CommentByChannel map[string]string
FormatByChannel map[string]string
PlacementCopy *time.Time
PaymentDateCopy *time.Time
CostCopy *CostEntry
CostBeforeCopy *CostEntry
PurchaseTypeCopy *string
CommentCopy *string
FormatCopy *string
ChannelEditMode string // "edit" или "copy"
BackState bot.State
ProjectID string
ProjectTitle string
CreativeID string
CreativeTitle string
Channels []PurchaseChannelInput
PlacementDateTime *time.Time
PaymentDate *time.Time
CostType string
CostValue *float64
CostBeforeType string
CostBeforeBargain *CostEntry
PurchaseType string
Format string
Comment string
InviteLinkType string
InputMode string
CurrentParam string
CurrentChannel string
ParamPage int
ReturnMode string
PlacementMode string
PaymentDateMode string
CostMode string
CostBeforeMode string
PurchaseTypeMode string
CommentMode string
FormatMode string
InviteLinkTypeMode string
PlacementByChannel map[string]*time.Time
PaymentDateByChannel map[string]*time.Time
CostByChannel map[string]CostEntry
CostBeforeByChannel map[string]CostEntry
PurchaseTypeByChannel map[string]string
CommentByChannel map[string]string
FormatByChannel map[string]string
InviteLinkTypeByChannel map[string]string
PlacementCopy *time.Time
PaymentDateCopy *time.Time
CostCopy *CostEntry
CostBeforeCopy *CostEntry
PurchaseTypeCopy *string
CommentCopy *string
FormatCopy *string
InviteLinkTypeCopy *string
ChannelEditMode string // "edit" или "copy"
BackState bot.State
}
type CostEntry struct {
@@ -104,6 +108,7 @@ func (s *PurchaseOptionalDetails) Enter(b *bot.Bot, mode bot.RenderMode) {
))
rows = append(rows, Row(
s.formatButton(),
s.inviteLinkTypeButton(),
))
if row := s.costBeforeDeleteRow(); row != nil {
@@ -182,6 +187,16 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
s.openCommonEditor(b, "format")
}
case "opt_invite_link_type":
// Открываем редактор в текущем режиме
if s.InviteLinkTypeMode == "per_channel" && len(s.Channels) > 1 {
s.InputMode = "invite_link_type_channels"
s.Enter(b, bot.EditMessage)
} else {
s.InputMode = "invite_link_type"
s.Enter(b, bot.EditMessage)
}
case "opt_comment":
// Открываем редактор в текущем режиме
if s.CommentMode == "per_channel" && len(s.Channels) > 1 {
@@ -332,6 +347,19 @@ func (s *PurchaseOptionalDetails) HandleCallback(b *bot.Bot, u *echotron.Update)
s.Enter(b, bot.EditMessage)
return
}
if strings.HasPrefix(u.CallbackQuery.Data, "invite_link_type:") {
value := strings.TrimPrefix(u.CallbackQuery.Data, "invite_link_type:")
s.setInviteLinkTypeValue(value)
if s.ReturnMode != "" {
s.InputMode = s.ReturnMode
s.ReturnMode = ""
} else {
s.InputMode = ""
}
s.CurrentChannel = ""
s.Enter(b, bot.EditMessage)
return
}
s.Enter(b, bot.EditMessage)
}
}
@@ -433,6 +461,9 @@ func (s *PurchaseOptionalDetails) formatOptionalSummary() string {
if s.hasFormatValue() {
lines = append(lines, fmt.Sprintf("<i>Формат:</i> <b>%s</b>%s", s.formatFormatSummary(), s.formatFormatDetails()))
}
if s.hasInviteLinkTypeValue() {
lines = append(lines, fmt.Sprintf("<i>Тип ссылки:</i> <b>%s</b>%s", s.formatInviteLinkTypeSummary(), s.formatInviteLinkTypeDetails()))
}
if s.hasCommentValue() {
lines = append(lines, fmt.Sprintf("<i>Комментарий:</i> <b>%s</b>%s", s.formatCommentSummary(), s.formatCommentDetails()))
}
@@ -449,8 +480,7 @@ func (s *PurchaseOptionalDetails) formatDateTime(value *time.Time) string {
return "—"
}
local := value.In(ui2.MskLocation)
text := fmt.Sprintf("%s %02d %s %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month()), local.Format("15:04"))
return fmt.Sprintf("<b>%s</b>", text)
return fmt.Sprintf("%s %02d %s %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month()), local.Format("15:04"))
}
func (s *PurchaseOptionalDetails) formatDate(value *time.Time) string {
@@ -458,23 +488,21 @@ func (s *PurchaseOptionalDetails) formatDate(value *time.Time) string {
return "—"
}
local := value.In(ui2.MskLocation)
text := fmt.Sprintf("%s %02d %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month()))
return fmt.Sprintf("<b>%s</b>", text)
return fmt.Sprintf("%s %02d %s", ui2.WeekdayName(local.Weekday()), local.Day(), ui2.MonthShort(local.Month()))
}
func (s *PurchaseOptionalDetails) formatText(value string) string {
if value == "" {
return "—"
}
return fmt.Sprintf("<b>%s</b>", value)
return value
}
func (s *PurchaseOptionalDetails) formatCostValue() string {
if s.CostValue == nil {
return "—"
}
text := fmt.Sprintf("%s %.0f₽", s.costTypeLabel(), *s.CostValue)
return fmt.Sprintf("<b>%s</b>", text)
return fmt.Sprintf("%s %.0f₽", s.costTypeLabel(), *s.CostValue)
}
func (s *PurchaseOptionalDetails) formatCostBefore() string {
@@ -482,8 +510,7 @@ func (s *PurchaseOptionalDetails) formatCostBefore() string {
return "—"
}
label := s.costBeforeTypeLabelForEntry(*s.CostBeforeBargain)
text := fmt.Sprintf("%s %.0f₽", label, *s.CostBeforeBargain.Value)
return fmt.Sprintf("<b>%s</b>", text)
return fmt.Sprintf("%s %.0f₽", label, *s.CostBeforeBargain.Value)
}
func (s *PurchaseOptionalDetails) costTypeLabel() string {
@@ -576,6 +603,22 @@ func (s *PurchaseOptionalDetails) commentButton() echotron.InlineKeyboardButton
return Button(fmt.Sprintf("%s Комментарий%s", icon, modeIcon), "opt_comment")
}
func (s *PurchaseOptionalDetails) inviteLinkTypeButton() echotron.InlineKeyboardButton {
icon := ""
if s.hasInviteLinkTypeValue() {
icon = "✎"
}
modeIcon := ""
if len(s.Channels) > 1 && s.InviteLinkTypeMode == "per_channel" {
modeIcon = " 👥"
}
label := "Тип ссылки"
if s.InviteLinkType != "" {
label += ": " + s.inviteLinkTypeLabel(s.InviteLinkType)
}
return Button(fmt.Sprintf("%s %s%s", icon, label, modeIcon), "opt_invite_link_type")
}
func (s *PurchaseOptionalDetails) globalModeButton() echotron.InlineKeyboardButton {
return Button("⚙️ Режимы параметров", "toggle_global_mode")
}
@@ -686,6 +729,25 @@ func (s *PurchaseOptionalDetails) renderInputPrompt(b *bot.Bot, mode bot.RenderM
return
case "format_custom":
text += "<b>Введите формат</b>\n\nНапример: <code>пост</code>"
case "invite_link_type":
text = "<i>Страница выбора типа ссылки</i>\n\n"
if s.CurrentChannel != "" {
text += fmt.Sprintf("Канал: <b>%s</b>\n\n", channelLabelByKey(s.Channels, s.CurrentChannel))
}
text += "Выберите тип ссылки\n\n"
keyboard := Keyboard(
Row(
Button("Открытая", "invite_link_type:public"),
Button("С заявками", "invite_link_type:approval"),
),
Row(Button("← Назад", s.backAction())),
)
if mode == bot.EditMessage {
b.Edit(text, keyboard)
} else {
b.SendNew(text, keyboard)
}
return
case "comment":
text += "<b>Введите комментарий</b>"
default:
@@ -761,6 +823,15 @@ func (s *PurchaseOptionalDetails) renderModeSelect(b *bot.Bot, mode bot.RenderMo
}
rows = append(rows, Row(Button(formatLabel, "toggle_param_mode:format")))
// Тип ссылки
inviteLinkTypeLabel := "Тип ссылки: "
if s.InviteLinkTypeMode == "per_channel" {
inviteLinkTypeLabel += "👥"
} else {
inviteLinkTypeLabel += "общий"
}
rows = append(rows, Row(Button(inviteLinkTypeLabel, "toggle_param_mode:invite_link_type")))
rows = append(rows, Row(Button("← Назад", "back_to_optional")))
b.Render(text, Keyboard(rows...), mode)
@@ -789,6 +860,9 @@ func (s *PurchaseOptionalDetails) renderParamScreens(b *bot.Bot, mode bot.Render
case "format_channels":
s.renderParamChannels(b, mode, "format")
return true
case "invite_link_type_channels":
s.renderParamChannels(b, mode, "invite_link_type")
return true
default:
return false
}
@@ -928,7 +1002,7 @@ func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string
// Паддинг вычисляем так, чтобы все значения начинались с одной позиции
const (
marker = "· " // Маркер пункта
valueIndent = 2 // Отступ после самого длинного названия
valueIndent = 6 // Отступ после самого длинного названия (увеличен для лучшей читаемости)
)
var result []string
@@ -936,7 +1010,7 @@ func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string
labelLen := len([]rune(line.label))
// Паддинг = (макс.длина - тек.длина) + отступ после названия
paddingLen := maxLabelLen - labelLen + valueIndent
padding := strings.Repeat(" ", paddingLen)
padding := strings.Repeat("\u00A0", paddingLen)
formattedLine := fmt.Sprintf("%s%s%s%s", marker, line.label, padding, line.value)
result = append(result, formattedLine)
@@ -953,6 +1027,9 @@ func (s *PurchaseOptionalDetails) renderParamChannelSummary(param string) string
finalResult := strings.Join(result, "\n")
// Оборачиваем в <pre> для моноширинного шрифта (выравнивание работает только в monospace)
finalResult = fmt.Sprintf("<pre>%s</pre>", finalResult)
// Логируем итоговый результат
log.Info().
Str("param", param).
@@ -1100,6 +1177,11 @@ func (s *PurchaseOptionalDetails) openCommonEditor(b *bot.Bot, param string) {
s.ReturnMode = ""
s.CurrentChannel = ""
s.Enter(b, bot.EditMessage)
case "invite_link_type":
s.InputMode = "invite_link_type"
s.ReturnMode = ""
s.CurrentChannel = ""
s.Enter(b, bot.EditMessage)
default:
s.InputMode = ""
s.Enter(b, bot.EditMessage)
@@ -1151,6 +1233,11 @@ func (s *PurchaseOptionalDetails) openChannelEditor(b *bot.Bot, param, channelKe
s.ReturnMode = "format_channels"
s.CurrentChannel = channelKey
s.Enter(b, bot.EditMessage)
case "invite_link_type":
s.InputMode = "invite_link_type"
s.ReturnMode = "invite_link_type_channels"
s.CurrentChannel = channelKey
s.Enter(b, bot.EditMessage)
default:
s.InputMode = ""
s.Enter(b, bot.EditMessage)
@@ -1204,6 +1291,13 @@ func (s *PurchaseOptionalDetails) copyParamValue(param, channelKey string) {
} else {
s.FormatCopy = nil
}
case "invite_link_type":
if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok {
copied := value
s.InviteLinkTypeCopy = &copied
} else {
s.InviteLinkTypeCopy = nil
}
}
}
@@ -1275,6 +1369,14 @@ func (s *PurchaseOptionalDetails) copyCommonValueToChannels(param string) {
key := channelKey(ch)
s.FormatByChannel[key] = s.Format
}
case "invite_link_type":
if s.InviteLinkType == "" {
return
}
for _, ch := range s.Channels {
key := channelKey(ch)
s.InviteLinkTypeByChannel[key] = s.InviteLinkType
}
}
}
@@ -1334,6 +1436,12 @@ func (s *PurchaseOptionalDetails) pasteParamValue(param, channelKey string) {
return
}
s.FormatByChannel[channelKey] = *s.FormatCopy
case "invite_link_type":
if s.InviteLinkTypeCopy == nil {
delete(s.InviteLinkTypeByChannel, channelKey)
return
}
s.InviteLinkTypeByChannel[channelKey] = *s.InviteLinkTypeCopy
}
}
@@ -1367,6 +1475,10 @@ func (s *PurchaseOptionalDetails) applyParamToAll(param string) {
for _, ch := range s.Channels {
s.pasteParamValue(param, channelKey(ch))
}
case "invite_link_type":
for _, ch := range s.Channels {
s.pasteParamValue(param, channelKey(ch))
}
}
}
@@ -1386,6 +1498,8 @@ func (s *PurchaseOptionalDetails) clearParamValue(param, channelKey string) {
delete(s.CommentByChannel, channelKey)
case "format":
delete(s.FormatByChannel, channelKey)
case "invite_link_type":
delete(s.InviteLinkTypeByChannel, channelKey)
}
}
@@ -1405,6 +1519,8 @@ func (s *PurchaseOptionalDetails) paramTitle(param string) string {
return "Комментарий"
case "format":
return "Формат"
case "invite_link_type":
return "Тип ссылки"
default:
return ""
}
@@ -1447,6 +1563,11 @@ func (s *PurchaseOptionalDetails) paramMode(param string) string {
return "common"
}
return s.FormatMode
case "invite_link_type":
if s.InviteLinkTypeMode == "" {
return "common"
}
return s.InviteLinkTypeMode
default:
return "common"
}
@@ -1468,6 +1589,8 @@ func (s *PurchaseOptionalDetails) setParamMode(param, mode string) {
s.CommentMode = mode
case "format":
s.FormatMode = mode
case "invite_link_type":
s.InviteLinkTypeMode = mode
}
}
@@ -1493,6 +1616,9 @@ func (s *PurchaseOptionalDetails) ensureDefaults() {
if s.FormatMode == "" {
s.FormatMode = "common"
}
if s.InviteLinkTypeMode == "" {
s.InviteLinkTypeMode = "common"
}
if s.PlacementByChannel == nil {
s.PlacementByChannel = make(map[string]*time.Time)
}
@@ -1514,6 +1640,9 @@ func (s *PurchaseOptionalDetails) ensureDefaults() {
if s.FormatByChannel == nil {
s.FormatByChannel = make(map[string]string)
}
if s.InviteLinkTypeByChannel == nil {
s.InviteLinkTypeByChannel = make(map[string]string)
}
s.syncChannelMaps()
}
@@ -1560,6 +1689,11 @@ func (s *PurchaseOptionalDetails) syncChannelMaps() {
delete(s.FormatByChannel, key)
}
}
for key := range s.InviteLinkTypeByChannel {
if _, ok := valid[key]; !ok {
delete(s.InviteLinkTypeByChannel, key)
}
}
}
func (s *PurchaseOptionalDetails) formatPlacementSummary() string {
@@ -1573,7 +1707,7 @@ func (s *PurchaseOptionalDetails) formatPlacementDetails() string {
if s.PlacementMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return "\n" + s.renderParamChannelSummary("placement")
return s.renderParamChannelSummary("placement")
}
func (s *PurchaseOptionalDetails) formatPaymentDateSummary() string {
@@ -1587,7 +1721,7 @@ func (s *PurchaseOptionalDetails) formatPaymentDateDetails() string {
if s.PaymentDateMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return "\n" + s.renderParamChannelSummary("payment_date")
return s.renderParamChannelSummary("payment_date")
}
func (s *PurchaseOptionalDetails) formatCostSummary() string {
@@ -1601,7 +1735,7 @@ func (s *PurchaseOptionalDetails) formatCostDetails() string {
if s.CostMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return "\n" + s.renderParamChannelSummary("cost")
return s.renderParamChannelSummary("cost")
}
func (s *PurchaseOptionalDetails) formatCostBeforeSummary() string {
@@ -1615,7 +1749,7 @@ func (s *PurchaseOptionalDetails) formatCostBeforeDetails() string {
if s.CostBeforeMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return "\n" + s.renderParamChannelSummary("cost_before")
return s.renderParamChannelSummary("cost_before")
}
func (s *PurchaseOptionalDetails) formatPurchaseTypeSummary() string {
@@ -1629,7 +1763,7 @@ func (s *PurchaseOptionalDetails) formatPurchaseTypeDetails() string {
if s.PurchaseTypeMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return "\n" + s.renderParamChannelSummary("purchase_type")
return s.renderParamChannelSummary("purchase_type")
}
func (s *PurchaseOptionalDetails) formatCommentSummary() string {
@@ -1643,7 +1777,7 @@ func (s *PurchaseOptionalDetails) formatCommentDetails() string {
if s.CommentMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return "\n" + s.renderParamChannelSummary("comment")
return s.renderParamChannelSummary("comment")
}
func (s *PurchaseOptionalDetails) formatFormatSummary() string {
@@ -1657,7 +1791,21 @@ func (s *PurchaseOptionalDetails) formatFormatDetails() string {
if s.FormatMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return "\n" + s.renderParamChannelSummary("format")
return s.renderParamChannelSummary("format")
}
func (s *PurchaseOptionalDetails) formatInviteLinkTypeSummary() string {
if s.InviteLinkTypeMode == "per_channel" && len(s.Channels) > 1 {
return "👥"
}
return s.inviteLinkTypeLabel(s.InviteLinkType)
}
func (s *PurchaseOptionalDetails) formatInviteLinkTypeDetails() string {
if s.InviteLinkTypeMode != "per_channel" || len(s.Channels) <= 1 {
return ""
}
return s.renderParamChannelSummary("invite_link_type")
}
func (s *PurchaseOptionalDetails) formatParamValue(param, channelKey string) string {
@@ -1674,17 +1822,22 @@ func (s *PurchaseOptionalDetails) formatParamValue(param, channelKey string) str
return s.formatCostBeforeForChannel(channelKey)
case "purchase_type":
if value, ok := s.PurchaseTypeByChannel[channelKey]; ok && value != "" {
return fmt.Sprintf("<b>%s</b>", value)
return value
}
return "—"
case "comment":
if value, ok := s.CommentByChannel[channelKey]; ok && value != "" {
return fmt.Sprintf("<b>%s</b>", value)
return value
}
return "—"
case "format":
if value, ok := s.FormatByChannel[channelKey]; ok && value != "" {
return fmt.Sprintf("<b>%s</b>", value)
return value
}
return "—"
case "invite_link_type":
if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok && value != "" {
return s.inviteLinkTypeLabel(value)
}
return "—"
default:
@@ -1698,8 +1851,7 @@ func (s *PurchaseOptionalDetails) formatCostForChannel(channelKey string) string
return "—"
}
label := s.costTypeLabelForEntry(entry)
text := fmt.Sprintf("%s %.0f₽", label, *entry.Value)
return fmt.Sprintf("<b>%s</b>", text)
return fmt.Sprintf("%s %.0f₽", label, *entry.Value)
}
func (s *PurchaseOptionalDetails) formatCostBeforeForChannel(channelKey string) string {
@@ -1708,8 +1860,7 @@ func (s *PurchaseOptionalDetails) formatCostBeforeForChannel(channelKey string)
return "—"
}
label := s.costBeforeTypeLabelForEntry(entry)
text := fmt.Sprintf("%s %.0f₽", label, *entry.Value)
return fmt.Sprintf("<b>%s</b>", text)
return fmt.Sprintf("%s %.0f₽", label, *entry.Value)
}
func (s *PurchaseOptionalDetails) hasCopyBuffer(param string) bool {
@@ -1728,6 +1879,8 @@ func (s *PurchaseOptionalDetails) hasCopyBuffer(param string) bool {
return s.CommentCopy != nil
case "format":
return s.FormatCopy != nil
case "invite_link_type":
return s.InviteLinkTypeCopy != nil
default:
return false
}
@@ -1756,6 +1909,9 @@ func (s *PurchaseOptionalDetails) hasChannelValue(param, channelKey string) bool
case "format":
value, ok := s.FormatByChannel[channelKey]
return ok && value != ""
case "invite_link_type":
value, ok := s.InviteLinkTypeByChannel[channelKey]
return ok && value != ""
default:
return false
}
@@ -1845,6 +2001,28 @@ func (s *PurchaseOptionalDetails) hasCommentValue() bool {
return s.Comment != ""
}
func (s *PurchaseOptionalDetails) hasInviteLinkTypeValue() bool {
if s.InviteLinkTypeMode == "per_channel" && len(s.Channels) > 1 {
for _, value := range s.InviteLinkTypeByChannel {
if value != "" {
return true
}
}
return false
}
return s.InviteLinkType != ""
}
func (s *PurchaseOptionalDetails) inviteLinkTypeLabel(linkType string) string {
if linkType == "public" {
return "Открытая"
}
if linkType == "approval" {
return "С заявками"
}
return "—"
}
func (s *PurchaseOptionalDetails) costTypeLabelForEntry(entry CostEntry) string {
if entry.Type == "cpm" {
return "СРМ"
@@ -1966,6 +2144,14 @@ func (s *PurchaseOptionalDetails) setFormatValue(value string) {
s.FormatByChannel[s.CurrentChannel] = value
}
func (s *PurchaseOptionalDetails) setInviteLinkTypeValue(value string) {
if s.CurrentChannel == "" {
s.InviteLinkType = value
return
}
s.InviteLinkTypeByChannel[s.CurrentChannel] = value
}
func (s *PurchaseOptionalDetails) backAction() string {
if s.ReturnMode != "" {
return "back_to_return"
@@ -2081,6 +2267,11 @@ func (s *PurchaseOptionalDetails) buildPlacementDetails(placementType *string) *
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
}
@@ -2130,6 +2321,11 @@ func (s *PurchaseOptionalDetails) buildChannelDetails(channelKey string) *backen
details.Format = &value
}
}
if s.InviteLinkTypeMode == "per_channel" {
if value, ok := s.InviteLinkTypeByChannel[channelKey]; ok && value != "" {
details.InviteLinkType = &value
}
}
return details
}