feat: sync implement Stars friend gift packages
This commit is contained in:
parent
3a123f38db
commit
c854fc7b94
15 changed files with 914 additions and 1 deletions
|
|
@ -244,6 +244,21 @@ func tgMessageServiceAction(msg domain.Message) tg.MessageActionClass {
|
|||
}
|
||||
case domain.MessageServiceActionStarGift:
|
||||
return tgMessageActionStarGiftForViewer(m.ServiceAction.StarGift, msg.OwnerUserID)
|
||||
case domain.MessageServiceActionGiftStars:
|
||||
action := m.ServiceAction.GiftStars
|
||||
if action == nil || action.Currency == "" || action.Amount <= 0 || action.Stars <= 0 {
|
||||
return &tg.MessageActionEmpty{}
|
||||
}
|
||||
out := &tg.MessageActionGiftStars{
|
||||
Currency: action.Currency,
|
||||
Amount: action.Amount,
|
||||
Stars: action.Stars,
|
||||
}
|
||||
// Telegram only exposes the provider transaction id to the receiver.
|
||||
if !msg.Out && action.TransactionID != "" {
|
||||
out.SetTransactionID(action.TransactionID)
|
||||
}
|
||||
return out
|
||||
case domain.MessageServiceActionStarGiftUnique:
|
||||
return tgMessageActionStarGiftUnique(m.ServiceAction.StarGiftUnique)
|
||||
case domain.MessageServiceActionStarGiftOffer:
|
||||
|
|
|
|||
|
|
@ -27,6 +27,9 @@ func tgUpdatesDifference(viewerUserID int64, diff domain.UpdateDifference) tg.Up
|
|||
out.NewMessages = append(out.NewMessages, msg)
|
||||
addMessageUsers(out, seenUsers, event.Message)
|
||||
}
|
||||
if balance := tgGiftStarsBalanceUpdate(event.Message); balance != nil {
|
||||
out.OtherUpdates = append(out.OtherUpdates, balance)
|
||||
}
|
||||
case domain.UpdateEventReadHistoryInbox:
|
||||
if update := tgReadHistoryInboxUpdate(event); update != nil {
|
||||
out.OtherUpdates = append(out.OtherUpdates, update)
|
||||
|
|
|
|||
|
|
@ -507,6 +507,9 @@ func tgPrivateMessageUpdates(event domain.UpdateEvent, msg domain.Message, rando
|
|||
Pts: event.Pts,
|
||||
PtsCount: event.PtsCount,
|
||||
})
|
||||
if balance := tgGiftStarsBalanceUpdate(msg); balance != nil {
|
||||
updates = append(updates, balance)
|
||||
}
|
||||
date := event.Date
|
||||
if date == 0 {
|
||||
date = msg.Date
|
||||
|
|
@ -520,6 +523,18 @@ func tgPrivateMessageUpdates(event domain.UpdateEvent, msg domain.Message, rando
|
|||
}
|
||||
}
|
||||
|
||||
func tgGiftStarsBalanceUpdate(msg domain.Message) tg.UpdateClass {
|
||||
if msg.Out || msg.Media == nil || msg.Media.ServiceAction == nil ||
|
||||
msg.Media.ServiceAction.Kind != domain.MessageServiceActionGiftStars {
|
||||
return nil
|
||||
}
|
||||
action := msg.Media.ServiceAction.GiftStars
|
||||
if action == nil || action.BalanceAfter < 0 {
|
||||
return nil
|
||||
}
|
||||
return &tg.UpdateStarsBalance{Balance: &tg.StarsAmount{Amount: action.BalanceAfter}}
|
||||
}
|
||||
|
||||
// tgPrivateSendResultUpdates returns a complete send acknowledgement for exact
|
||||
// random_id replays. DrKLO requires UpdateNewMessage in an Updates response to
|
||||
// transition its local pending message to SENT. Visible edited messages use the
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ import (
|
|||
// registerPayments 注册 payments.* RPC:Stars 本地账本(余额/流水真实化)+ 其余
|
||||
// gift/auction/revenue 第一阶段兼容桩。
|
||||
func (r *Router) registerPayments(d *tlprofile.Dispatcher) {
|
||||
registerRPC[*tg.PaymentsGetStarsGiftOptionsRequest](d, tlprofile.SemanticMethodPaymentsGetStarsGiftOptions, func(ctx context.Context, req *tg.PaymentsGetStarsGiftOptionsRequest) (any, error) {
|
||||
return r.onPaymentsGetStarsGiftOptions(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.PaymentsGetStarsTopupOptionsRequest](d, tlprofile.SemanticMethodPaymentsGetStarsTopupOptions, func(ctx context.Context, layerRequest *tg.PaymentsGetStarsTopupOptionsRequest) (any,
|
||||
|
||||
// premium 订阅赠送 telesrv 不实现(无支付流),返回空选项。关键作用:TDesktop 送礼框
|
||||
|
|
@ -66,6 +69,9 @@ func (r *Router) registerPayments(d *tlprofile.Dispatcher) {
|
|||
registerRPC[*tg.PaymentsSendStarsFormRequest](d, tlprofile.SemanticMethodPaymentsSendStarsForm, func(ctx context.Context, layerRequest *tg.PaymentsSendStarsFormRequest) (any, error) {
|
||||
return r.onPaymentsSendStarsForm(ctx, layerRequest)
|
||||
})
|
||||
registerRPC[*tg.PaymentsSendPaymentFormRequest](d, tlprofile.SemanticMethodPaymentsSendPaymentForm, func(ctx context.Context, req *tg.PaymentsSendPaymentFormRequest) (any, error) {
|
||||
return r.onPaymentsSendPaymentForm(ctx, req)
|
||||
})
|
||||
registerRPC[*tg.PaymentsGetSavedStarGiftsRequest](d, tlprofile.SemanticMethodPaymentsGetSavedStarGifts, func(ctx context.Context, layerRequest *tg.PaymentsGetSavedStarGiftsRequest) (any, error) {
|
||||
return r.onPaymentsGetSavedStarGifts(ctx, layerRequest)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -30,6 +30,82 @@ func devStarsTopupOptions() []tg.StarsTopupOption {
|
|||
}
|
||||
}
|
||||
|
||||
func devStarsGiftOptions() []tg.StarsGiftOption {
|
||||
// No store_product is advertised: telesrv has no Google/Apple product and
|
||||
// sideloaded Android clients must use the invoice checkout path.
|
||||
return []tg.StarsGiftOption{
|
||||
{Stars: 1000, Currency: "USD", Amount: 99},
|
||||
{Stars: 2500, Currency: "USD", Amount: 199},
|
||||
{Stars: 5000, Currency: "USD", Amount: 399},
|
||||
}
|
||||
}
|
||||
|
||||
type starsGiftPurchaseService interface {
|
||||
IssueGiftPurchaseForm(context.Context, domain.StarsGiftPurchaseForm) (domain.StarsGiftPurchaseForm, error)
|
||||
PurchaseGift(context.Context, domain.StarsGiftPurchaseRequest) (domain.StarsGiftPurchaseResult, error)
|
||||
}
|
||||
|
||||
func userGiftUnavailableErr() error { return tgerr.New(400, "USER_GIFT_UNAVAILABLE") }
|
||||
|
||||
func (r *Router) onPaymentsGetStarsGiftOptions(ctx context.Context, req *tg.PaymentsGetStarsGiftOptionsRequest) ([]tg.StarsGiftOption, error) {
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
if req != nil {
|
||||
if input, ok := req.GetUserID(); ok {
|
||||
if _, err := r.starsGiftRecipient(ctx, userID, input); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
return devStarsGiftOptions(), nil
|
||||
}
|
||||
|
||||
func (r *Router) starsGiftRecipient(ctx context.Context, buyerUserID int64, input tg.InputUserClass) (domain.User, error) {
|
||||
if inputUserIsEmpty(input) || r.deps.Users == nil {
|
||||
return domain.User{}, userIDInvalidErr()
|
||||
}
|
||||
recipient, found, err := r.userFromInput(ctx, buyerUserID, input)
|
||||
if err != nil {
|
||||
return domain.User{}, internalErr()
|
||||
}
|
||||
if !found || recipient.ID <= 0 || recipient.Deleted {
|
||||
return domain.User{}, userIDInvalidErr()
|
||||
}
|
||||
if recipient.ID == buyerUserID {
|
||||
return domain.User{}, userGiftUnavailableErr()
|
||||
}
|
||||
blocked, err := r.peerBlocksUser(ctx, buyerUserID, recipient.ID)
|
||||
if err != nil {
|
||||
return domain.User{}, err
|
||||
}
|
||||
if blocked {
|
||||
return domain.User{}, userGiftUnavailableErr()
|
||||
}
|
||||
return recipient, nil
|
||||
}
|
||||
|
||||
func validateStarsGiftOption(purpose *tg.InputStorePaymentStarsGift) (tg.StarsGiftOption, error) {
|
||||
if purpose == nil || purpose.UserID == nil || purpose.Stars <= 0 || purpose.Amount <= 0 || purpose.Currency == "" {
|
||||
return tg.StarsGiftOption{}, starsAmountInvalidErr()
|
||||
}
|
||||
for _, option := range devStarsGiftOptions() {
|
||||
if option.Stars == purpose.Stars && option.Currency == purpose.Currency && option.Amount == purpose.Amount {
|
||||
return option, nil
|
||||
}
|
||||
}
|
||||
return tg.StarsGiftOption{}, starsFormAmountMismatchErr()
|
||||
}
|
||||
|
||||
func starsGiftPurpose(inv *tg.InputInvoiceStars) (*tg.InputStorePaymentStarsGift, bool) {
|
||||
if inv == nil {
|
||||
return nil, false
|
||||
}
|
||||
purpose, ok := inv.Purpose.(*tg.InputStorePaymentStarsGift)
|
||||
return purpose, ok && purpose != nil
|
||||
}
|
||||
|
||||
// onPaymentsGetStarGifts 返回可购买礼物目录(hash 命中返回 NotModified)。
|
||||
func (r *Router) onPaymentsGetStarGifts(ctx context.Context, hash int) (tg.PaymentsStarGiftsClass, error) {
|
||||
if r.deps.Gifts == nil {
|
||||
|
|
@ -71,6 +147,9 @@ func (r *Router) onPaymentsGetPaymentForm(ctx context.Context, req *tg.PaymentsG
|
|||
}
|
||||
|
||||
if inv, ok := req.Invoice.(*tg.InputInvoiceStars); ok {
|
||||
if purpose, gift := starsGiftPurpose(inv); gift {
|
||||
return r.starsGiftPaymentForm(ctx, userID, purpose)
|
||||
}
|
||||
purpose, ok := starsTopupPurpose(inv)
|
||||
if !ok {
|
||||
return nil, notImplementedErr()
|
||||
|
|
@ -149,6 +228,36 @@ func (r *Router) onPaymentsGetPaymentForm(ctx context.Context, req *tg.PaymentsG
|
|||
}, nil
|
||||
}
|
||||
|
||||
func (r *Router) starsGiftPaymentForm(ctx context.Context, buyerUserID int64, purpose *tg.InputStorePaymentStarsGift) (tg.PaymentsPaymentFormClass, error) {
|
||||
if _, err := validateStarsGiftOption(purpose); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recipient, err := r.starsGiftRecipient(ctx, buyerUserID, purpose.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service, ok := r.deps.Stars.(starsGiftPurchaseService)
|
||||
if !ok {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
now := int(r.clock.Now().Unix())
|
||||
form, err := service.IssueGiftPurchaseForm(ctx, domain.StarsGiftPurchaseForm{
|
||||
BuyerUserID: buyerUserID, RecipientUserID: recipient.ID,
|
||||
Stars: purpose.Stars, Currency: purpose.Currency, Amount: purpose.Amount,
|
||||
IssuedAt: now, ExpiresAt: now + 600,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, starsGiftPurchaseErr(err)
|
||||
}
|
||||
users := []domain.User{domain.OfficialSystemUser(), recipient}
|
||||
return &tg.PaymentsPaymentFormStars{
|
||||
FormID: form.FormID, BotID: domain.OfficialSystemUserID,
|
||||
Title: "Gift Stars", Description: "Gift Stars to a friend",
|
||||
Invoice: tg.Invoice{Currency: "XTR", Prices: []tg.LabeledPrice{{Label: branding.StarsName, Amount: purpose.Stars}}},
|
||||
Users: tgUsersForViewer(buyerUserID, users),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// onPaymentsSendStarsForm 处理 star gift 与 Stars topup:
|
||||
// - star gift: Debit→投递/记账,失败补偿退款。
|
||||
// - topup: 校验测试包白名单→Credit 本地账本。
|
||||
|
|
@ -165,6 +274,9 @@ func (r *Router) onPaymentsSendStarsForm(ctx context.Context, req *tg.PaymentsSe
|
|||
}
|
||||
|
||||
if inv, ok := req.Invoice.(*tg.InputInvoiceStars); ok {
|
||||
if purpose, gift := starsGiftPurpose(inv); gift {
|
||||
return r.sendStarsGiftPurchase(ctx, userID, req.FormID, purpose)
|
||||
}
|
||||
return r.sendStarsTopupForm(ctx, userID, req.FormID, inv)
|
||||
}
|
||||
if inv, ok := req.Invoice.(*tg.InputInvoiceStarGiftUpgrade); ok {
|
||||
|
|
@ -266,6 +378,78 @@ func (r *Router) onPaymentsSendStarsForm(ctx context.Context, req *tg.PaymentsSe
|
|||
return &tg.PaymentsPaymentResult{Updates: updates}, nil
|
||||
}
|
||||
|
||||
// onPaymentsSendPaymentForm is the Android invoice-billing completion path.
|
||||
// The local development provider does not charge an external card, but it
|
||||
// accepts only a persisted Stars-gift form and runs the same atomic settlement
|
||||
// as TDesktop's paymentFormStars/sendStarsForm compatibility path.
|
||||
func (r *Router) onPaymentsSendPaymentForm(ctx context.Context, req *tg.PaymentsSendPaymentFormRequest) (tg.PaymentsPaymentResultClass, error) {
|
||||
if req == nil {
|
||||
return nil, inputRequestInvalidErr()
|
||||
}
|
||||
userID, _, err := r.currentUserID(ctx)
|
||||
if err != nil {
|
||||
return nil, internalErr()
|
||||
}
|
||||
inv, ok := req.Invoice.(*tg.InputInvoiceStars)
|
||||
if !ok {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
purpose, ok := starsGiftPurpose(inv)
|
||||
if !ok {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
if req.Credentials == nil {
|
||||
return nil, tgerr.New(400, "PAYMENT_CREDENTIALS_INVALID")
|
||||
}
|
||||
if req.TipAmount != 0 {
|
||||
return nil, tgerr.New(400, "TIP_AMOUNT_INVALID")
|
||||
}
|
||||
return r.sendStarsGiftPurchase(ctx, userID, req.FormID, purpose)
|
||||
}
|
||||
|
||||
func (r *Router) sendStarsGiftPurchase(ctx context.Context, buyerUserID, formID int64, purpose *tg.InputStorePaymentStarsGift) (tg.PaymentsPaymentResultClass, error) {
|
||||
if formID == 0 {
|
||||
return nil, formIDEmptyErr()
|
||||
}
|
||||
if _, err := validateStarsGiftOption(purpose); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
recipient, err := r.starsGiftRecipient(ctx, buyerUserID, purpose.UserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
service, ok := r.deps.Stars.(starsGiftPurchaseService)
|
||||
if !ok {
|
||||
return nil, notImplementedErr()
|
||||
}
|
||||
result, err := service.PurchaseGift(ctx, domain.StarsGiftPurchaseRequest{
|
||||
StarsGiftPurchaseForm: domain.StarsGiftPurchaseForm{
|
||||
FormID: formID, BuyerUserID: buyerUserID, RecipientUserID: recipient.ID,
|
||||
Stars: purpose.Stars, Currency: purpose.Currency, Amount: purpose.Amount,
|
||||
},
|
||||
Date: int(r.clock.Now().Unix()), OriginAuthKeyID: rawAuthKeyIDForOrigin(ctx),
|
||||
OriginSessionID: sessionIDOrZero(ctx),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, starsGiftPurchaseErr(err)
|
||||
}
|
||||
updates := r.starGiftSendUpdates(ctx, buyerUserID, result.Send)
|
||||
return &tg.PaymentsPaymentResult{Updates: updates}, nil
|
||||
}
|
||||
|
||||
func starsGiftPurchaseErr(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, domain.ErrStarsGiftFormExpired):
|
||||
return tgerr.New(400, "FORM_EXPIRED")
|
||||
case errors.Is(err, domain.ErrStarsGiftFormInvalid):
|
||||
return starsFormAmountMismatchErr()
|
||||
case errors.Is(err, domain.ErrStarsGiftUnavailable):
|
||||
return userGiftUnavailableErr()
|
||||
default:
|
||||
return internalErr()
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Router) sendStarGiftMemoryPurchase(ctx context.Context, userID int64, peer domain.Peer, gift domain.StarGift,
|
||||
inv *tg.InputInvoiceStarGift, giftMessage string, upgradeStars int64) (tg.PaymentsPaymentResultClass, error) {
|
||||
purchaseStars := gift.Stars + upgradeStars
|
||||
|
|
|
|||
192
internal/rpc/payments_stars_friend_gift_test.go
Normal file
192
internal/rpc/payments_stars_friend_gift_test.go
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
package rpc
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/iamxvbaba/td/clock"
|
||||
"github.com/iamxvbaba/td/tg"
|
||||
"github.com/iamxvbaba/td/tgerr"
|
||||
"go.uber.org/zap/zaptest"
|
||||
|
||||
appstars "telesrv/internal/app/stars"
|
||||
appusers "telesrv/internal/app/users"
|
||||
"telesrv/internal/domain"
|
||||
"telesrv/internal/store/memory"
|
||||
)
|
||||
|
||||
type starsFriendGiftRPCStore struct {
|
||||
*memory.StarsStore
|
||||
issued domain.StarsGiftPurchaseForm
|
||||
purchased domain.StarsGiftPurchaseRequest
|
||||
purchases int
|
||||
}
|
||||
|
||||
func (s *starsFriendGiftRPCStore) IssueStarsGiftPurchaseForm(_ context.Context, form domain.StarsGiftPurchaseForm) (domain.StarsGiftPurchaseForm, error) {
|
||||
form.FormID = 70001
|
||||
s.issued = form
|
||||
return form, nil
|
||||
}
|
||||
|
||||
func (s *starsFriendGiftRPCStore) PurchaseStarsGift(_ context.Context, req domain.StarsGiftPurchaseRequest) (domain.StarsGiftPurchaseResult, error) {
|
||||
s.purchased = req
|
||||
s.purchases++
|
||||
action := &domain.MessageServiceAction{Kind: domain.MessageServiceActionGiftStars, GiftStars: &domain.MessageGiftStarsAction{
|
||||
Currency: req.Currency, Amount: req.Amount, Stars: req.Stars,
|
||||
TransactionID: "stars-gift-test", BalanceAfter: 4321,
|
||||
}}
|
||||
sender := domain.Message{ID: 11, OwnerUserID: req.BuyerUserID, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: req.RecipientUserID},
|
||||
From: domain.Peer{Type: domain.PeerTypeUser, ID: req.BuyerUserID}, Out: true, Date: req.Date,
|
||||
Media: &domain.MessageMedia{Kind: domain.MessageMediaKindService, ServiceAction: action}}
|
||||
recipient := sender
|
||||
recipient.ID, recipient.OwnerUserID, recipient.Peer, recipient.Out = 12, req.RecipientUserID,
|
||||
domain.Peer{Type: domain.PeerTypeUser, ID: req.BuyerUserID}, false
|
||||
return domain.StarsGiftPurchaseResult{
|
||||
RecipientBalance: domain.StarsBalance{UserID: req.RecipientUserID, Balance: 4321},
|
||||
TransactionID: "stars-gift-test",
|
||||
Send: domain.SendPrivateTextResult{
|
||||
SenderMessage: sender, RecipientMessage: recipient,
|
||||
SenderEvent: domain.UpdateEvent{UserID: req.BuyerUserID, Type: domain.UpdateEventNewMessage, Pts: 5, PtsCount: 1, Date: req.Date, Message: sender},
|
||||
RecipientEvent: domain.UpdateEvent{UserID: req.RecipientUserID, Type: domain.UpdateEventNewMessage, Pts: 9, PtsCount: 1, Date: req.Date, Message: recipient},
|
||||
},
|
||||
}, nil
|
||||
}
|
||||
|
||||
func starsFriendGiftTestRouter(t *testing.T) (*Router, *starsFriendGiftRPCStore, domain.User, domain.User) {
|
||||
t.Helper()
|
||||
ctx := context.Background()
|
||||
users := memory.NewUserStore()
|
||||
buyer, err := users.Create(ctx, domain.User{AccessHash: 8101, Phone: "+15558101", FirstName: "Buyer"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
recipient, err := users.Create(ctx, domain.User{AccessHash: 8102, Phone: "+15558102", FirstName: "Recipient"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
st := &starsFriendGiftRPCStore{StarsStore: memory.NewStarsStore()}
|
||||
r := New(Config{DC: 2}, Deps{
|
||||
Users: appusers.NewService(users),
|
||||
Stars: appstars.NewService(st, appstars.WithStartingGrant(0), appstars.WithGiftPurchaseStore(st)),
|
||||
}, zaptest.NewLogger(t), clock.System)
|
||||
return r, st, buyer, recipient
|
||||
}
|
||||
|
||||
func TestStarsFriendGiftOptionsFormAndBothSettlementMethods(t *testing.T) {
|
||||
r, st, buyer, recipient := starsFriendGiftTestRouter(t)
|
||||
ctx := WithUserID(context.Background(), buyer.ID)
|
||||
|
||||
generic, err := r.onPaymentsGetStarsGiftOptions(ctx, &tg.PaymentsGetStarsGiftOptionsRequest{})
|
||||
if err != nil || len(generic) != 3 || generic[0].Stars != 1000 || generic[0].Currency != "USD" || generic[0].Amount != 99 {
|
||||
t.Fatalf("generic gift options = %+v err=%v", generic, err)
|
||||
}
|
||||
input := &tg.InputUser{UserID: recipient.ID, AccessHash: recipient.AccessHash}
|
||||
personalReq := &tg.PaymentsGetStarsGiftOptionsRequest{}
|
||||
personalReq.SetUserID(input)
|
||||
personal, err := r.onPaymentsGetStarsGiftOptions(ctx, personalReq)
|
||||
if err != nil || len(personal) != len(generic) {
|
||||
t.Fatalf("personal gift options = %+v err=%v", personal, err)
|
||||
}
|
||||
|
||||
purpose := &tg.InputStorePaymentStarsGift{UserID: input, Stars: 2500, Currency: "USD", Amount: 199}
|
||||
invoice := &tg.InputInvoiceStars{Purpose: purpose}
|
||||
formClass, err := r.onPaymentsGetPaymentForm(ctx, &tg.PaymentsGetPaymentFormRequest{Invoice: invoice})
|
||||
if err != nil {
|
||||
t.Fatalf("get gift payment form: %v", err)
|
||||
}
|
||||
form, ok := formClass.(*tg.PaymentsPaymentFormStars)
|
||||
if !ok || form.FormID != 70001 || form.Invoice.Currency != "XTR" || len(form.Invoice.Prices) != 1 || form.Invoice.Prices[0].Amount != 2500 {
|
||||
t.Fatalf("gift payment form = %T %+v", formClass, formClass)
|
||||
}
|
||||
if st.issued.BuyerUserID != buyer.ID || st.issued.RecipientUserID != recipient.ID || st.issued.Stars != 2500 || st.issued.ExpiresAt != st.issued.IssuedAt+600 {
|
||||
t.Fatalf("issued form = %+v", st.issued)
|
||||
}
|
||||
|
||||
resultClass, err := r.onPaymentsSendStarsForm(ctx, &tg.PaymentsSendStarsFormRequest{FormID: form.FormID, Invoice: invoice})
|
||||
if err != nil {
|
||||
t.Fatalf("sendStarsForm gift: %v", err)
|
||||
}
|
||||
result, ok := resultClass.(*tg.PaymentsPaymentResult)
|
||||
if !ok {
|
||||
t.Fatalf("sendStarsForm result = %T", resultClass)
|
||||
}
|
||||
updates, ok := result.Updates.(*tg.Updates)
|
||||
if !ok || len(updates.Updates) != 1 {
|
||||
t.Fatalf("sender updates = %T %+v", result.Updates, result.Updates)
|
||||
}
|
||||
newMessage, ok := updates.Updates[0].(*tg.UpdateNewMessage)
|
||||
if !ok || newMessage.Pts != 5 || newMessage.PtsCount != 1 {
|
||||
t.Fatalf("sender new message = %T %+v", updates.Updates[0], updates.Updates[0])
|
||||
}
|
||||
serviceMessage, ok := newMessage.Message.(*tg.MessageService)
|
||||
if !ok {
|
||||
t.Fatalf("gift message = %T", newMessage.Message)
|
||||
}
|
||||
action, ok := serviceMessage.Action.(*tg.MessageActionGiftStars)
|
||||
if !ok || action.Stars != 2500 || action.Currency != "USD" || action.Amount != 199 || action.TransactionID != "" {
|
||||
t.Fatalf("sender gift action = %T %+v", serviceMessage.Action, serviceMessage.Action)
|
||||
}
|
||||
if st.purchased.FormID != form.FormID || st.purchased.RecipientUserID != recipient.ID || st.purchases != 1 {
|
||||
t.Fatalf("purchase request = %+v count=%d", st.purchased, st.purchases)
|
||||
}
|
||||
|
||||
credentials := &tg.InputPaymentCredentials{Data: tg.DataJSON{Data: "{}"}}
|
||||
if _, err := r.onPaymentsSendPaymentForm(ctx, &tg.PaymentsSendPaymentFormRequest{
|
||||
FormID: form.FormID, Invoice: invoice, Credentials: credentials,
|
||||
}); err != nil {
|
||||
t.Fatalf("sendPaymentForm gift: %v", err)
|
||||
}
|
||||
if st.purchases != 2 {
|
||||
t.Fatalf("settlement method count = %d, want 2 fake invocations", st.purchases)
|
||||
}
|
||||
}
|
||||
|
||||
func TestStarsFriendGiftRejectsInvalidRecipientAndPackageBeforeStore(t *testing.T) {
|
||||
r, st, buyer, recipient := starsFriendGiftTestRouter(t)
|
||||
ctx := WithUserID(context.Background(), buyer.ID)
|
||||
|
||||
badRecipientReq := &tg.PaymentsGetStarsGiftOptionsRequest{}
|
||||
badRecipientReq.SetUserID(&tg.InputUser{UserID: recipient.ID + 99999})
|
||||
if _, err := r.onPaymentsGetStarsGiftOptions(ctx, badRecipientReq); !tgerr.Is(err, "USER_ID_INVALID") {
|
||||
t.Fatalf("bad recipient err = %v", err)
|
||||
}
|
||||
bad := &tg.InputInvoiceStars{Purpose: &tg.InputStorePaymentStarsGift{
|
||||
UserID: &tg.InputUser{UserID: recipient.ID, AccessHash: recipient.AccessHash},
|
||||
Stars: 2500, Currency: "USD", Amount: 200,
|
||||
}}
|
||||
if _, err := r.onPaymentsGetPaymentForm(ctx, &tg.PaymentsGetPaymentFormRequest{Invoice: bad}); !tgerr.Is(err, "STARS_FORM_AMOUNT_MISMATCH") {
|
||||
t.Fatalf("tampered package form err = %v", err)
|
||||
}
|
||||
if _, err := r.onPaymentsSendStarsForm(ctx, &tg.PaymentsSendStarsFormRequest{FormID: 70001, Invoice: bad}); !tgerr.Is(err, "STARS_FORM_AMOUNT_MISMATCH") {
|
||||
t.Fatalf("tampered package settle err = %v", err)
|
||||
}
|
||||
if st.issued.FormID != 0 || st.purchases != 0 {
|
||||
t.Fatalf("invalid request reached store: issued=%+v purchases=%d", st.issued, st.purchases)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGiftStarsRecipientProjectionCarriesBalanceOnlineAndDifference(t *testing.T) {
|
||||
action := &domain.MessageServiceAction{Kind: domain.MessageServiceActionGiftStars, GiftStars: &domain.MessageGiftStarsAction{
|
||||
Currency: "USD", Amount: 99, Stars: 1000, TransactionID: "txn-1", BalanceAfter: 3100,
|
||||
}}
|
||||
msg := domain.Message{ID: 4, OwnerUserID: 2, Peer: domain.Peer{Type: domain.PeerTypeUser, ID: 1},
|
||||
From: domain.Peer{Type: domain.PeerTypeUser, ID: 1}, Date: 1700000000,
|
||||
Media: &domain.MessageMedia{Kind: domain.MessageMediaKindService, ServiceAction: action}}
|
||||
event := domain.UpdateEvent{UserID: 2, Type: domain.UpdateEventNewMessage, Pts: 8, PtsCount: 1, Date: msg.Date, Message: msg}
|
||||
online := tgPrivateMessageUpdates(event, msg, 0, false, nil, nil)
|
||||
if len(online.Updates) != 2 {
|
||||
t.Fatalf("online updates = %+v", online.Updates)
|
||||
}
|
||||
balance, ok := online.Updates[1].(*tg.UpdateStarsBalance)
|
||||
if !ok || balance.Balance.(*tg.StarsAmount).Amount != 3100 {
|
||||
t.Fatalf("online balance = %T %+v", online.Updates[1], online.Updates[1])
|
||||
}
|
||||
diff := tgUpdatesDifference(2, domain.UpdateDifference{Events: []domain.UpdateEvent{event}, State: domain.UpdateState{Pts: 8}})
|
||||
full, ok := diff.(*tg.UpdatesDifference)
|
||||
if !ok || len(full.NewMessages) != 1 || len(full.OtherUpdates) != 1 {
|
||||
t.Fatalf("difference = %T %+v", diff, diff)
|
||||
}
|
||||
if _, ok := full.OtherUpdates[0].(*tg.UpdateStarsBalance); !ok {
|
||||
t.Fatalf("difference balance = %T", full.OtherUpdates[0])
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue