512 lines
17 KiB
Go
512 lines
17 KiB
Go
package main
|
||
|
||
// bot_flows.go — customer + reseller conversation flows and admin commands.
|
||
|
||
import (
|
||
"encoding/base64"
|
||
"fmt"
|
||
"log"
|
||
"strconv"
|
||
"strings"
|
||
"time"
|
||
)
|
||
|
||
// ---------- Main menu ----------
|
||
|
||
func (b *Bot) showMainMenu(chatID int64, from *tgUser, msgID int64) {
|
||
bu := b.botUser(from.ID)
|
||
if bu.Role == "blocked" {
|
||
b.sendOrEdit(chatID, msgID, "🚫 Seu acesso foi bloqueado.", nil)
|
||
return
|
||
}
|
||
welcome := b.store.GetSetting(b.ctx, "welcome_text", "")
|
||
var text string
|
||
if welcome != "" {
|
||
text = strings.ReplaceAll(welcome, "{name}", htmlEscape(from.FirstName))
|
||
} else {
|
||
text = fmt.Sprintf("😉 Olá <b>%s</b>, seja bem-vindo!\n\n🚀 Aqui você encontra os melhores planos <b>SSH</b> e <b>Xray</b> Premium.\nSelecione uma das opções abaixo:", htmlEscape(from.FirstName))
|
||
}
|
||
|
||
var rows [][]tgInlineButton
|
||
if b.cfg.TrialEnabled {
|
||
rows = append(rows, []tgInlineButton{btn("⏳ Teste Grátis", "trial"), btn("🛍️ Minhas Compras", "purchases")})
|
||
} else {
|
||
rows = append(rows, []tgInlineButton{btn("🛍️ Minhas Compras", "purchases")})
|
||
}
|
||
rows = append(rows, []tgInlineButton{btn("💎 Comprar Premium", "buy")})
|
||
rows = append(rows, []tgInlineButton{btn("🔄 Renovar", "renew")})
|
||
|
||
appURL := b.store.GetSetting(b.ctx, "app_url", "")
|
||
appRow := []tgInlineButton{}
|
||
if appURL != "" {
|
||
appRow = append(appRow, urlBtn("📥 Baixar APP", appURL))
|
||
} else {
|
||
appRow = append(appRow, btn("📥 Baixar APP", "app"))
|
||
}
|
||
appRow = append(appRow, btn("👤 Contato", "contact"))
|
||
rows = append(rows, appRow)
|
||
|
||
if bu.Role == "reseller" {
|
||
rows = append(rows, []tgInlineButton{btn("👑 Área do Revendedor", "res:menu")})
|
||
}
|
||
b.sendOrEdit(chatID, msgID, text, kb(rows...))
|
||
}
|
||
|
||
// ---------- Plan list (buy / reseller create) ----------
|
||
|
||
func (b *Bot) showPlanList(chatID, msgID int64, _ string, action string) {
|
||
plans, err := b.store.ListPlans(b.ctx, true)
|
||
if err != nil || len(plans) == 0 {
|
||
b.sendOrEdit(chatID, msgID, "Nenhum plano disponível no momento.", backKeyboard())
|
||
return
|
||
}
|
||
isReseller := action == "res:create"
|
||
var rows [][]tgInlineButton
|
||
for _, p := range plans {
|
||
icon := "🔒"
|
||
if p.Kind == "xray" {
|
||
icon = "⚡"
|
||
}
|
||
var label string
|
||
if isReseller {
|
||
label = fmt.Sprintf("%s %s — %d créd.", icon, p.Name, p.CreditCost)
|
||
} else {
|
||
label = fmt.Sprintf("%s %s — %s", icon, p.Name, centsToBRL(p.PriceCents))
|
||
}
|
||
rows = append(rows, []tgInlineButton{btn(label, action+":"+botItoa(p.ID))})
|
||
}
|
||
rows = append(rows, []tgInlineButton{btn("⬅️ Voltar", backTarget(action))})
|
||
title := "💎 Escolha um plano:"
|
||
if isReseller {
|
||
title = "➕ Escolha um plano para criar (custo em créditos):"
|
||
}
|
||
b.sendOrEdit(chatID, msgID, title, kb(rows...))
|
||
}
|
||
|
||
func backTarget(action string) string {
|
||
if strings.HasPrefix(action, "res:") {
|
||
return "res:menu"
|
||
}
|
||
return "menu:main"
|
||
}
|
||
|
||
// ---------- Buy ----------
|
||
|
||
func (b *Bot) startPlanPurchase(chatID int64, from *tgUser, planIDStr string, _ bool) {
|
||
id, _ := strconv.Atoi(planIDStr)
|
||
p, err := b.store.GetPlan(b.ctx, id)
|
||
if err != nil {
|
||
b.send(chatID, "Plano não encontrado.", backKeyboard())
|
||
return
|
||
}
|
||
pid := p.ID
|
||
b.createAndSendPix(chatID, from, "plan_purchase", "Compra: "+p.Name, p.PriceCents, &pid, nil, 0, "")
|
||
}
|
||
|
||
// ---------- Renew ----------
|
||
|
||
func (b *Bot) showRenewList(chatID int64, from *tgUser, msgID int64) {
|
||
txns, _ := b.store.ListUserTransactions(b.ctx, from.ID, 100)
|
||
seen := map[string]bool{}
|
||
var rows [][]tgInlineButton
|
||
for _, t := range txns {
|
||
if t.Status != "approved" || t.TargetUsername == "" || t.PlanID == nil {
|
||
continue
|
||
}
|
||
p, err := b.store.GetPlan(b.ctx, *t.PlanID)
|
||
if err != nil {
|
||
continue
|
||
}
|
||
key := p.Kind + ":" + t.TargetUsername
|
||
if seen[key] {
|
||
continue
|
||
}
|
||
seen[key] = true
|
||
short := "s"
|
||
if p.Kind == "xray" {
|
||
short = "x"
|
||
}
|
||
disp := t.TargetUsername
|
||
if len(disp) > 16 {
|
||
disp = disp[:8] + "…"
|
||
}
|
||
rows = append(rows, []tgInlineButton{btn("🔄 "+disp+" ("+p.Kind+")", "renew:"+short+":"+t.TargetUsername)})
|
||
}
|
||
if len(rows) == 0 {
|
||
b.sendOrEdit(chatID, msgID, "Você não tem contas para renovar.", backKeyboard())
|
||
return
|
||
}
|
||
rows = append(rows, []tgInlineButton{btn("⬅️ Voltar", "menu:main")})
|
||
b.sendOrEdit(chatID, msgID, "🔄 Selecione a conta para renovar:", kb(rows...))
|
||
}
|
||
|
||
func (b *Bot) startRenew(chatID int64, from *tgUser, msgID int64, target string) {
|
||
kind := "ssh"
|
||
if strings.HasPrefix(target, "x:") {
|
||
kind = "xray"
|
||
}
|
||
plans, _ := b.store.ListPlans(b.ctx, true)
|
||
var rows [][]tgInlineButton
|
||
for _, p := range plans {
|
||
if p.Kind != kind {
|
||
continue
|
||
}
|
||
label := fmt.Sprintf("%s — %s", p.Name, centsToBRL(p.PriceCents))
|
||
rows = append(rows, []tgInlineButton{btn(label, "rnw:"+botItoa(p.ID)+":"+target)})
|
||
}
|
||
if len(rows) == 0 {
|
||
b.sendOrEdit(chatID, msgID, "Nenhum plano de renovação disponível.", backKeyboard())
|
||
return
|
||
}
|
||
rows = append(rows, []tgInlineButton{btn("⬅️ Voltar", "renew")})
|
||
b.sendOrEdit(chatID, msgID, "🔄 Escolha a duração da renovação:", kb(rows...))
|
||
}
|
||
|
||
func (b *Bot) startRenewPayment(chatID int64, from *tgUser, payload string) {
|
||
parts := strings.SplitN(payload, ":", 3)
|
||
if len(parts) < 3 {
|
||
b.send(chatID, "Renovação inválida.", backKeyboard())
|
||
return
|
||
}
|
||
id, _ := strconv.Atoi(parts[0])
|
||
renewTarget := parts[1] + ":" + parts[2] // "s:username" or "x:uuid"
|
||
p, err := b.store.GetPlan(b.ctx, id)
|
||
if err != nil {
|
||
b.send(chatID, "Plano não encontrado.", backKeyboard())
|
||
return
|
||
}
|
||
pid := p.ID
|
||
b.createAndSendPix(chatID, from, "plan_renewal", "Renovação: "+p.Name, p.PriceCents, &pid, nil, 0, renewTarget)
|
||
}
|
||
|
||
// ---------- Trial ----------
|
||
|
||
func (b *Bot) handleTrial(chatID int64, from *tgUser) {
|
||
if !b.cfg.TrialEnabled {
|
||
b.send(chatID, "Teste grátis indisponível.", backKeyboard())
|
||
return
|
||
}
|
||
bu := b.botUser(from.ID)
|
||
if bu.TrialUsed {
|
||
b.send(chatID, "⚠️ Você já utilizou seu teste grátis.", backKeyboard())
|
||
return
|
||
}
|
||
exp := time.Now().Add(time.Duration(b.cfg.TrialHours) * time.Hour)
|
||
if b.cfg.TrialKind == "xray" {
|
||
uuid, link, err := createXrayClient(b.ctx, b.store, b.cfg.TrialInboundTag, "", exp, b.cfg.TrialMaxConnections, "", b.cfg.XrayPublicHost)
|
||
if err != nil {
|
||
log.Printf("[bot] trial xray: %v", err)
|
||
b.send(chatID, "❌ Falha ao criar teste. Tente mais tarde.", backKeyboard())
|
||
return
|
||
}
|
||
_ = b.store.SetBotUserTrialUsed(b.ctx, from.ID)
|
||
b.send(chatID, b.formatXrayDelivery("Teste Grátis", uuid, link, exp), backKeyboard())
|
||
return
|
||
}
|
||
user := genUsername("test")
|
||
pass := genPassword()
|
||
if err := createSSHUser(b.ctx, b.store, user, pass, exp, b.cfg.TrialMaxConnections, 0, 0, ""); err != nil {
|
||
log.Printf("[bot] trial ssh: %v", err)
|
||
b.send(chatID, "❌ Falha ao criar teste. Tente mais tarde.", backKeyboard())
|
||
return
|
||
}
|
||
_ = b.store.SetBotUserTrialUsed(b.ctx, from.ID)
|
||
b.send(chatID, b.formatSSHDelivery("Teste Grátis", user, pass, exp), backKeyboard())
|
||
}
|
||
|
||
// ---------- Purchases ----------
|
||
|
||
func (b *Bot) showPurchases(chatID int64, from *tgUser, msgID int64) {
|
||
txns, _ := b.store.ListUserTransactions(b.ctx, from.ID, 20)
|
||
var sb strings.Builder
|
||
sb.WriteString("🛍️ <b>Suas Compras</b>\n\n")
|
||
count := 0
|
||
for _, t := range txns {
|
||
if t.Status == "pending" {
|
||
continue
|
||
}
|
||
count++
|
||
sb.WriteString(fmt.Sprintf("• #%d %s — %s — %s\n", t.ID, txnTypeLabel(t.Type), centsToBRL(t.AmountCents), statusLabel(t.Status)))
|
||
if t.TargetUsername != "" && t.Status == "approved" {
|
||
sb.WriteString(" conta: <code>" + htmlEscape(t.TargetUsername) + "</code>\n")
|
||
}
|
||
}
|
||
if count == 0 {
|
||
sb.WriteString("Nenhuma compra ainda.")
|
||
}
|
||
b.sendOrEdit(chatID, msgID, sb.String(), backKeyboard())
|
||
}
|
||
|
||
// ---------- Reseller ----------
|
||
|
||
func (b *Bot) showResellerMenu(chatID int64, from *tgUser, msgID int64) {
|
||
bu := b.botUser(from.ID)
|
||
if bu.Role != "reseller" {
|
||
b.sendOrEdit(chatID, msgID, "Você não é um revendedor.", backKeyboard())
|
||
return
|
||
}
|
||
text := fmt.Sprintf("👑 <b>Área do Revendedor</b>\n\n💳 Saldo: <b>%d créditos</b>", bu.CreditBalance)
|
||
rows := [][]tgInlineButton{
|
||
{btn("💳 Recarregar Créditos", "res:topup")},
|
||
{btn("➕ Criar Conta", "res:create")},
|
||
{btn("👥 Meus Clientes", "res:clients")},
|
||
{btn("⬅️ Voltar", "menu:main")},
|
||
}
|
||
b.sendOrEdit(chatID, msgID, text, kb(rows...))
|
||
}
|
||
|
||
func (b *Bot) showCreditPackages(chatID, msgID int64) {
|
||
pkgs, _ := b.store.ListCreditPackages(b.ctx, true)
|
||
if len(pkgs) == 0 {
|
||
b.sendOrEdit(chatID, msgID, "Nenhum pacote de créditos disponível.", kb([]tgInlineButton{btn("⬅️ Voltar", "res:menu")}))
|
||
return
|
||
}
|
||
var rows [][]tgInlineButton
|
||
for _, p := range pkgs {
|
||
rows = append(rows, []tgInlineButton{btn(fmt.Sprintf("%s — %d créd. — %s", p.Name, p.Credits, centsToBRL(p.PriceCents)), "res:topup:"+botItoa(p.ID))})
|
||
}
|
||
rows = append(rows, []tgInlineButton{btn("⬅️ Voltar", "res:menu")})
|
||
b.sendOrEdit(chatID, msgID, "💳 Escolha um pacote de créditos:", kb(rows...))
|
||
}
|
||
|
||
func (b *Bot) startTopup(chatID int64, from *tgUser, pkgIDStr string) {
|
||
id, _ := strconv.Atoi(pkgIDStr)
|
||
p, err := b.store.GetCreditPackage(b.ctx, id)
|
||
if err != nil {
|
||
b.send(chatID, "Pacote não encontrado.", backKeyboard())
|
||
return
|
||
}
|
||
pid := p.ID
|
||
b.createAndSendPix(chatID, from, "credit_topup", "Recarga: "+p.Name, p.PriceCents, nil, &pid, p.Credits, "")
|
||
}
|
||
|
||
func (b *Bot) resellerCreateAccount(chatID int64, from *tgUser, planIDStr string) {
|
||
bu := b.botUser(from.ID)
|
||
if bu.Role != "reseller" || bu.LinkedAdminUsername == "" {
|
||
b.send(chatID, "Conta de revendedor não configurada.", backKeyboard())
|
||
return
|
||
}
|
||
id, _ := strconv.Atoi(planIDStr)
|
||
p, err := b.store.GetPlan(b.ctx, id)
|
||
if err != nil {
|
||
b.send(chatID, "Plano não encontrado.", backKeyboard())
|
||
return
|
||
}
|
||
if bu.CreditBalance < p.CreditCost {
|
||
b.send(chatID, fmt.Sprintf("❌ Saldo insuficiente. Necessário %d créditos, você tem %d.", p.CreditCost, bu.CreditBalance),
|
||
kb([]tgInlineButton{btn("💳 Recarregar", "res:topup"), btn("⬅️ Voltar", "res:menu")}))
|
||
return
|
||
}
|
||
if owner, ok := adminUsers.get(bu.LinkedAdminUsername); ok && owner.MaxUsers > 0 &&
|
||
countOwnedQuota(b.ctx, b.store, bu.LinkedAdminUsername) >= owner.MaxUsers {
|
||
b.send(chatID, fmt.Sprintf("❌ Limite de contas atingido (%d).", owner.MaxUsers), backKeyboard())
|
||
return
|
||
}
|
||
// Debit first; refund if provisioning fails.
|
||
if _, err := b.store.AdjustCredits(b.ctx, from.ID, -p.CreditCost, "account_create", nil); err != nil {
|
||
b.send(chatID, "❌ Não foi possível debitar créditos.", backKeyboard())
|
||
return
|
||
}
|
||
exp := time.Now().Add(time.Duration(p.Days) * 24 * time.Hour)
|
||
var deliver string
|
||
if p.Kind == "xray" {
|
||
uuid, link, err := createXrayClient(b.ctx, b.store, p.XrayInboundTag, p.XrayProtocol, exp, p.MaxConnections, bu.LinkedAdminUsername, b.cfg.XrayPublicHost)
|
||
if err != nil {
|
||
_, _ = b.store.AdjustCredits(b.ctx, from.ID, p.CreditCost, "refund", nil)
|
||
b.send(chatID, "❌ Falha ao criar conta Xray. Créditos devolvidos.", backKeyboard())
|
||
return
|
||
}
|
||
deliver = b.formatXrayDelivery(p.Name, uuid, link, exp)
|
||
} else {
|
||
user := genUsername("r")
|
||
pass := genPassword()
|
||
if err := createSSHUser(b.ctx, b.store, user, pass, exp, p.MaxConnections, p.LimitMbpsUp, p.LimitMbpsDown, bu.LinkedAdminUsername); err != nil {
|
||
_, _ = b.store.AdjustCredits(b.ctx, from.ID, p.CreditCost, "refund", nil)
|
||
b.send(chatID, "❌ Falha ao criar conta SSH. Créditos devolvidos.", backKeyboard())
|
||
return
|
||
}
|
||
deliver = b.formatSSHDelivery(p.Name, user, pass, exp)
|
||
}
|
||
b.send(chatID, deliver+fmt.Sprintf("\n\n💳 Saldo restante: %d créditos", bu.CreditBalance-p.CreditCost),
|
||
kb([]tgInlineButton{btn("➕ Criar outra", "res:create"), btn("⬅️ Voltar", "res:menu")}))
|
||
}
|
||
|
||
func (b *Bot) showResellerClients(chatID int64, from *tgUser, msgID int64) {
|
||
bu := b.botUser(from.ID)
|
||
if bu.Role != "reseller" || bu.LinkedAdminUsername == "" {
|
||
b.sendOrEdit(chatID, msgID, "Conta de revendedor não configurada.", backKeyboard())
|
||
return
|
||
}
|
||
var sb strings.Builder
|
||
sb.WriteString("👥 <b>Seus Clientes</b>\n\n")
|
||
n := 0
|
||
for _, u := range userMgr.List() {
|
||
if u.Cfg.OwnerUsername == bu.LinkedAdminUsername {
|
||
n++
|
||
exp := "sem validade"
|
||
if u.ExpiresAt != nil {
|
||
exp = u.ExpiresAt.Format("02/01/2006")
|
||
}
|
||
sb.WriteString(fmt.Sprintf("• SSH <code>%s</code> — %s\n", htmlEscape(u.Cfg.Username), exp))
|
||
if n >= 40 {
|
||
break
|
||
}
|
||
}
|
||
}
|
||
xs, _ := b.store.ListXrayClientsByOwner(b.ctx, bu.LinkedAdminUsername)
|
||
for _, x := range xs {
|
||
n++
|
||
exp := "sem validade"
|
||
if x.ExpiresAt != nil {
|
||
exp = x.ExpiresAt.Format("02/01/2006")
|
||
}
|
||
sb.WriteString(fmt.Sprintf("• Xray <code>%s</code> — %s\n", htmlEscape(x.UUID), exp))
|
||
if n >= 80 {
|
||
break
|
||
}
|
||
}
|
||
if n == 0 {
|
||
sb.WriteString("Nenhum cliente ainda.")
|
||
}
|
||
b.sendOrEdit(chatID, msgID, sb.String(), kb([]tgInlineButton{btn("⬅️ Voltar", "res:menu")}))
|
||
}
|
||
|
||
// ---------- PIX charge creation + delivery formatting ----------
|
||
|
||
func (b *Bot) createAndSendPix(chatID int64, from *tgUser, ttype, description string, amountCents int, planID, pkgID *int, credits int, renewTarget string) {
|
||
if amountCents <= 0 {
|
||
b.send(chatID, "❌ Este item não tem preço configurado. Fale com o suporte.", backKeyboard())
|
||
return
|
||
}
|
||
if b.mp == nil {
|
||
b.send(chatID, "❌ Pagamento não configurado no momento. Fale com o suporte.", backKeyboard())
|
||
return
|
||
}
|
||
exp := time.Now().Add(time.Duration(b.cfg.PixExpirationMinutes) * time.Minute)
|
||
pix, err := b.mp.CreatePixPayment(b.ctx, amountCents, description, "", strconv.FormatInt(from.ID, 10), exp, uuidV4())
|
||
if err != nil {
|
||
log.Printf("[bot] create pix: %v", err)
|
||
b.send(chatID, "❌ Falha ao gerar o pagamento PIX. Tente novamente em instantes.", backKeyboard())
|
||
return
|
||
}
|
||
txn := &BotTransaction{
|
||
TelegramID: from.ID,
|
||
Type: ttype,
|
||
PlanID: planID,
|
||
PackageID: pkgID,
|
||
Credits: credits,
|
||
AmountCents: amountCents,
|
||
MPPaymentID: pix.PaymentID,
|
||
MPQRCode: pix.QRCode,
|
||
MPQRBase64: pix.QRBase64,
|
||
Status: "pending",
|
||
RenewTarget: renewTarget,
|
||
ExpiresAt: &exp,
|
||
}
|
||
if err := b.store.CreateTransaction(b.ctx, txn); err != nil {
|
||
log.Printf("[bot] create txn: %v", err)
|
||
b.send(chatID, "❌ Erro interno ao registrar o pagamento.", backKeyboard())
|
||
return
|
||
}
|
||
b.sendPixMessage(chatID, txn, description)
|
||
}
|
||
|
||
func (b *Bot) sendPixMessage(chatID int64, txn *BotTransaction, description string) {
|
||
caption := fmt.Sprintf("💳 <b>Pagamento PIX</b>\n%s\nValor: <b>%s</b>\n⏱ Validade: %d min\n\nEscaneie o QR acima ou use o código copia-e-cola abaixo. A liberação é automática após o pagamento.",
|
||
htmlEscape(description), centsToBRL(txn.AmountCents), b.cfg.PixExpirationMinutes)
|
||
kbd := kb(
|
||
[]tgInlineButton{btn("✅ Já paguei / Verificar", "pay:check:"+botItoa(txn.ID))},
|
||
[]tgInlineButton{btn("⬅️ Voltar", "menu:main")},
|
||
)
|
||
sent := false
|
||
if txn.MPQRBase64 != "" {
|
||
if raw, err := base64.StdEncoding.DecodeString(txn.MPQRBase64); err == nil {
|
||
if _, err := b.tg.sendPhotoBytes(b.ctx, chatID, raw, "pix.png", caption, kbd); err == nil {
|
||
sent = true
|
||
}
|
||
}
|
||
}
|
||
if !sent {
|
||
b.send(chatID, caption, kbd)
|
||
}
|
||
if txn.MPQRCode != "" {
|
||
b.send(chatID, "📋 <b>PIX Copia e Cola:</b>\n<code>"+htmlEscape(txn.MPQRCode)+"</code>", nil)
|
||
}
|
||
}
|
||
|
||
func (b *Bot) formatSSHDelivery(planName, user, pass string, exp time.Time) string {
|
||
host := b.cfg.PublicHost
|
||
if host == "" {
|
||
host = "(configure o host no painel)"
|
||
}
|
||
return fmt.Sprintf("✅ <b>%s</b>\n\n🔒 <b>Conta SSH</b>\nHost: <code>%s</code>\nUsuário: <code>%s</code>\nSenha: <code>%s</code>\nValidade: %s",
|
||
htmlEscape(planName), htmlEscape(host), htmlEscape(user), htmlEscape(pass), exp.Format("02/01/2006 15:04"))
|
||
}
|
||
|
||
func (b *Bot) formatXrayDelivery(planName, uuid, link string, exp time.Time) string {
|
||
return fmt.Sprintf("✅ <b>%s</b>\n\n⚡ <b>Conta Xray</b>\nUUID: <code>%s</code>\nValidade: %s\n\n🔗 Link de conexão:\n<code>%s</code>",
|
||
htmlEscape(planName), htmlEscape(uuid), exp.Format("02/01/2006 15:04"), htmlEscape(link))
|
||
}
|
||
|
||
// ---------- Admin commands (in-chat convenience) ----------
|
||
|
||
func (b *Bot) cmdAdminStats(chatID int64) {
|
||
users, _ := b.store.ListBotUsers(b.ctx)
|
||
pend, _ := b.store.ListPendingTransactions(b.ctx)
|
||
b.send(chatID, fmt.Sprintf("📊 <b>Estatísticas</b>\nUsuários do bot: %d\nPagamentos pendentes: %d\nContas SSH ativas: %d",
|
||
len(users), len(pend), len(userMgr.List())), nil)
|
||
}
|
||
|
||
func (b *Bot) cmdAdminAddCredit(chatID int64, text string) {
|
||
f := strings.Fields(text)
|
||
if len(f) < 3 {
|
||
b.send(chatID, "Uso: /addcredit <telegram_id> <quantidade>", nil)
|
||
return
|
||
}
|
||
tid, _ := strconv.ParseInt(f[1], 10, 64)
|
||
amt, _ := strconv.Atoi(f[2])
|
||
bal, err := b.store.AdjustCredits(b.ctx, tid, amt, "admin_adjust", nil)
|
||
if err != nil {
|
||
b.send(chatID, "Erro: "+err.Error(), nil)
|
||
return
|
||
}
|
||
b.send(chatID, fmt.Sprintf("✅ Ajuste aplicado. Novo saldo de %d: %d créditos", tid, bal), nil)
|
||
b.notify(tid, fmt.Sprintf("💳 Seu saldo foi ajustado em %+d créditos. Saldo atual: %d", amt, bal))
|
||
}
|
||
|
||
func (b *Bot) notify(telegramID int64, text string) {
|
||
if _, err := b.tg.sendMessage(b.ctx, telegramID, text, nil); err != nil {
|
||
log.Printf("[bot] notify %d: %v", telegramID, err)
|
||
}
|
||
}
|
||
|
||
// ---------- labels ----------
|
||
|
||
func txnTypeLabel(t string) string {
|
||
switch t {
|
||
case "plan_purchase":
|
||
return "Compra"
|
||
case "plan_renewal":
|
||
return "Renovação"
|
||
case "credit_topup":
|
||
return "Recarga"
|
||
}
|
||
return t
|
||
}
|
||
|
||
func statusLabel(s string) string {
|
||
switch s {
|
||
case "approved":
|
||
return "✅ pago"
|
||
case "pending":
|
||
return "⏳ pendente"
|
||
case "expired":
|
||
return "⌛ expirado"
|
||
case "refunded":
|
||
return "↩️ estornado"
|
||
case "error":
|
||
return "❌ erro"
|
||
}
|
||
return s
|
||
}
|