internal/store/postgres/bot_invoice_payments.go
Rama main · réplica de lectura del repositorio original.
package postgres
import (
"context"
"crypto/sha256"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
"telesrv/internal/domain"
"telesrv/internal/store"
)
var _ store.BotInvoicePayments = (*MessageStore)(nil)
func (s *MessageStore) CreateBotInvoiceMessage(ctx context.Context, invoice domain.BotInvoice, req domain.SendPrivateTextRequest) (domain.SendPrivateTextResult, error) {
if req.SenderUserID != invoice.BotUserID || req.RecipientUserID != invoice.ChatID || invoice.ChatID <= 0 || req.RecipientBlocked {
return domain.SendPrivateTextResult{}, domain.ErrBotInvoiceInvalid
}
return s.sendPrivateTextWithHooks(ctx, req, privateSendTxHooks{after: func(ctx context.Context, tx pgx.Tx, sent domain.SendPrivateTextResult) error {
invoice.MessageID = sent.RecipientMessage.ID
_, err := NewBotStore(tx).CreateBotInvoice(ctx, invoice)
return err
}})
}
func (s *MessageStore) PayBotInvoice(ctx context.Context, invoiceID, payerID int64, date int) (domain.SendPrivateTextResult, error) {
if invoiceID <= 0 || payerID <= 0 || date <= 0 {
return domain.SendPrivateTextResult{}, domain.ErrBotInvoiceInvalid
}
invoice, err := scanBotInvoice(s.db.QueryRow(ctx, `SELECT `+botInvoiceColumns+` FROM bot_invoices WHERE id=$1 AND chat_id=$2`, invoiceID, payerID))
if errors.Is(err, pgx.ErrNoRows) {
return domain.SendPrivateTextResult{}, domain.ErrBotInvoiceNotFound
}
if err != nil {
return domain.SendPrivateTextResult{}, err
}
if invoice.Refunded {
return domain.SendPrivateTextResult{}, domain.ErrBotInvoiceRefunded
}
if invoice.Paid && invoice.PayerID != payerID {
return domain.SendPrivateTextResult{}, domain.ErrBotInvoiceInvalid
}
chargeID := fmt.Sprintf("botinv-%d", invoice.ID)
fingerprint := sha256.Sum256([]byte(fmt.Sprintf("bot-invoice-payment:%d:%d", invoice.ID, payerID)))
req := domain.SendPrivateTextRequest{
SenderUserID: payerID, RecipientUserID: invoice.BotUserID, RandomID: lifecycleCommandRandomID("bot-invoice-payment", payerID, chargeID), Date: date,
IdempotencyFingerprint: fingerprint[:],
Media: &domain.MessageMedia{Kind: domain.MessageMediaKindService, ServiceAction: &domain.MessageServiceAction{
Kind: domain.MessageServiceActionPayment, Payment: &domain.MessagePaymentAction{Currency: invoice.Currency, TotalAmount: invoice.Amount, Payload: invoice.Payload, ChargeID: chargeID, ProviderChargeID: chargeID, Title: invoice.Title, Description: invoice.Description}}},
}
return s.sendPrivateTextWithHooks(ctx, req, privateSendTxHooks{
before: func(ctx context.Context, tx pgx.Tx, _ *domain.SendPrivateTextRequest) error {
locked, err := scanBotInvoice(tx.QueryRow(ctx, `SELECT `+botInvoiceColumns+` FROM bot_invoices WHERE id=$1 FOR UPDATE`, invoiceID))
if err != nil {
return err
}
if locked.Paid || locked.Refunded || locked.ChatID != payerID {
return domain.ErrBotInvoiceSettled
}
peer := domain.Peer{Type: domain.PeerTypeUser, ID: invoice.BotUserID}
if _, err := NewStarsStore(tx).Debit(ctx, payerID, invoice.Amount, domain.StarsReasonBotInvoice, peer, date, invoice.Title, ""); err != nil {
return err
}
lifecycle := &StarGiftLifecycleStore{db: tx}
if _, err := lifecycle.applyBotStarsWalletTx(ctx, tx, domain.BotStarsCredit{BotUserID: invoice.BotUserID, PayerUserID: payerID, Amount: invoice.Amount, Reason: domain.StarsReasonBotInvoice, InvoiceKey: chargeID, Date: date}); err != nil {
return err
}
_, err = tx.Exec(ctx, `UPDATE bot_invoices SET paid=true,charge_id=$2,payer_user_id=$3,paid_at=$4 WHERE id=$1`, invoiceID, chargeID, payerID, date)
return err
},
after: func(ctx context.Context, tx pgx.Tx, sent domain.SendPrivateTextResult) error {
_, _, err := NewBotAPIUpdateStore(tx).EnqueueBotAPIUpdate(ctx, domain.EnqueueBotAPIUpdateRequest{
BotUserID: invoice.BotUserID, Kind: domain.BotAPIUpdateMessage, Peer: sent.RecipientMessage.Peer, MessageID: sent.RecipientMessage.ID, SourcePts: sent.RecipientEvent.Pts, Date: date,
})
return err
},
})
}
func (s *MessageStore) RefundBotInvoice(ctx context.Context, botID, payerID int64, chargeID string, date int) error {
if botID <= 0 || payerID <= 0 || chargeID == "" || date <= 0 {
return domain.ErrBotInvoiceInvalid
}
return withTx(ctx, s.db, "refund bot invoice", func(tx pgx.Tx) error {
if err := lockUsersForUpdate(ctx, tx, botID, payerID); err != nil {
return err
}
invoice, err := scanBotInvoice(tx.QueryRow(ctx, `SELECT `+botInvoiceColumns+` FROM bot_invoices WHERE charge_id=$1 AND bot_user_id=$2 AND payer_user_id=$3 FOR UPDATE`, chargeID, botID, payerID))
if errors.Is(err, pgx.ErrNoRows) {
return domain.ErrBotInvoiceNotFound
}
if err != nil {
return err
}
if !invoice.Paid {
return domain.ErrBotInvoiceInvalid
}
if invoice.Refunded {
return nil
}
lifecycle := &StarGiftLifecycleStore{db: tx}
if _, err := lifecycle.applyBotStarsWalletTx(ctx, tx, domain.BotStarsCredit{BotUserID: botID, PayerUserID: payerID, Amount: -invoice.Amount, Reason: domain.StarsReasonBotRefund, InvoiceKey: chargeID + "-refund", Date: date}); err != nil {
return err
}
if _, err := NewStarsStore(tx).Credit(ctx, payerID, invoice.Amount, domain.StarsReasonBotRefund, domain.Peer{Type: domain.PeerTypeUser, ID: botID}, date, invoice.Title, ""); err != nil {
return err
}
_, err = tx.Exec(ctx, `UPDATE bot_invoices SET refunded=true WHERE id=$1`, invoice.ID)
return err
})
}