Ctrl K
ComunidadGuías…

internal/store/postgres/bot_invoice_payments.go

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
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
	})
}