internal/store/postgres/bot_invoice.go
Rama main · réplica de lectura del repositorio original.
package postgres
import (
"context"
"errors"
"github.com/jackc/pgx/v5"
"telesrv/internal/domain"
)
// Bot XTR invoices.
//
// payments.sendPaymentForm has no purpose field in this layer, so the price is
// resolved from the stored invoice: the client reports only the message id.
// The row is the price authority; bot_invoice_payments.go owns the atomic
// invoice, ledger, message and delivery transaction.
func scanBotInvoice(row interface {
Scan(dest ...any) error
}) (domain.BotInvoice, error) {
var (
invoice domain.BotInvoice
date int32
paidAt int32
)
err := row.Scan(
&invoice.ID, &invoice.BotUserID, &invoice.ChatID, &invoice.MessageID,
&invoice.Title, &invoice.Description, &invoice.Amount, &invoice.Currency,
&invoice.Payload, &invoice.ChargeID, &invoice.PayerID, &invoice.Paid,
&invoice.Refunded, &paidAt, &date,
)
if err != nil {
return domain.BotInvoice{}, err
}
invoice.PaidAt = int(paidAt)
invoice.Date = int(date)
return invoice, nil
}
const botInvoiceColumns = `id,bot_user_id,chat_id,message_id,title,description,
amount,currency,payload,charge_id,payer_user_id,paid,refunded,paid_at,date`
// CreateBotInvoice stores a freshly sent invoice message. The message id is
// unique per bot chat, so a resend after a delete cannot orphan a paid invoice:
// the unique index rejects it rather than silently replacing history.
func (s *BotStore) CreateBotInvoice(ctx context.Context, invoice domain.BotInvoice) (domain.BotInvoice, error) {
if !invoice.Valid() {
return domain.BotInvoice{}, domain.ErrBotInvoiceInvalid
}
row := s.db.QueryRow(ctx, `INSERT INTO bot_invoices
(bot_user_id,chat_id,message_id,title,description,amount,currency,payload,date)
VALUES($1,$2,$3,$4,$5,$6,$7,$8,$9)
RETURNING `+botInvoiceColumns,
invoice.BotUserID, invoice.ChatID, invoice.MessageID, invoice.Title,
invoice.Description, invoice.Amount, invoice.Currency, invoice.Payload, invoice.Date)
stored, err := scanBotInvoice(row)
if err != nil {
return domain.BotInvoice{}, err
}
return stored, nil
}
// BotInvoiceByMessage resolves the invoice a client referenced through
// inputInvoiceMessage.
func (s *BotStore) BotInvoiceByMessage(ctx context.Context, botUserID, chatID int64, messageID int) (domain.BotInvoice, bool, error) {
if botUserID <= 0 || chatID == 0 || messageID <= 0 {
return domain.BotInvoice{}, false, domain.ErrBotInvoiceInvalid
}
row := s.db.QueryRow(ctx, `SELECT `+botInvoiceColumns+`
FROM bot_invoices WHERE bot_user_id=$1 AND chat_id=$2 AND message_id=$3`,
botUserID, chatID, messageID)
invoice, err := scanBotInvoice(row)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.BotInvoice{}, false, nil
}
return domain.BotInvoice{}, false, err
}
return invoice, true, nil
}
// BotInvoiceByCharge resolves an invoice by the charge id the client reported,
// leaving it untouched so the caller can check ownership before flagging it.
func (s *BotStore) BotInvoiceByCharge(ctx context.Context, chargeID string) (domain.BotInvoice, bool, error) {
if chargeID == "" {
return domain.BotInvoice{}, false, domain.ErrBotInvoiceInvalid
}
row := s.db.QueryRow(ctx, `SELECT `+botInvoiceColumns+` FROM bot_invoices WHERE charge_id=$1`, chargeID)
invoice, err := scanBotInvoice(row)
if errors.Is(err, pgx.ErrNoRows) {
return domain.BotInvoice{}, false, domain.ErrBotInvoiceNotFound
}
if err != nil {
return domain.BotInvoice{}, false, err
}
return invoice, true, nil
}
var _ interface {
CreateBotInvoice(context.Context, domain.BotInvoice) (domain.BotInvoice, error)
BotInvoiceByMessage(context.Context, int64, int64, int) (domain.BotInvoice, bool, error)
BotInvoiceByCharge(context.Context, string) (domain.BotInvoice, bool, error)
} = (*BotStore)(nil)