From 2a6dc5a304f9a8a04d2bd585f792755c18d02d0e Mon Sep 17 00:00:00 2001 From: Ilia Denisov Date: Thu, 9 Jul 2026 17:30:39 +0200 Subject: [PATCH] feat(gateway): wire the wallet.order edge call for the direct rail Add the WalletOrderRequest / WalletOrderResponse FlatBuffers messages and the wallet.order Connect op: the gateway decodes the order request, forwards it to the backend POST /wallet/order and returns the created order id plus the provider launch URL the client opens. Regenerate the committed Go and TS FlatBuffers code. --- gateway/internal/backendclient/api.go | 18 +++++ gateway/internal/transcode/encode.go | 13 ++++ gateway/internal/transcode/transcode.go | 13 ++++ pkg/fbs/scrabble.fbs | 12 ++++ pkg/fbs/scrabblefb/WalletOrderRequest.go | 60 ++++++++++++++++ pkg/fbs/scrabblefb/WalletOrderResponse.go | 71 +++++++++++++++++++ ui/src/gen/fbs/scrabblefb.ts | 2 + .../fbs/scrabblefb/wallet-order-request.ts | 48 +++++++++++++ .../fbs/scrabblefb/wallet-order-response.ts | 60 ++++++++++++++++ 9 files changed, 297 insertions(+) create mode 100644 pkg/fbs/scrabblefb/WalletOrderRequest.go create mode 100644 pkg/fbs/scrabblefb/WalletOrderResponse.go create mode 100644 ui/src/gen/fbs/scrabblefb/wallet-order-request.ts create mode 100644 ui/src/gen/fbs/scrabblefb/wallet-order-response.ts diff --git a/gateway/internal/backendclient/api.go b/gateway/internal/backendclient/api.go index 1607b67..09f3352 100644 --- a/gateway/internal/backendclient/api.go +++ b/gateway/internal/backendclient/api.go @@ -396,6 +396,24 @@ func (c *Client) WalletBuy(ctx context.Context, userID, productID string) (Walle return out, err } +// walletOrderBody is the POST body of an order: the chip pack to fund. +type walletOrderBody struct { + ProductID string `json:"product_id"` +} + +// WalletOrderResp is a created order: its id and the provider launch URL the client opens. +type WalletOrderResp struct { + OrderID string `json:"order_id"` + RedirectURL string `json:"redirect_url"` +} + +// WalletOrder opens a pending order to fund a chip pack and returns the provider launch URL. +func (c *Client) WalletOrder(ctx context.Context, userID, productID string) (WalletOrderResp, error) { + var out WalletOrderResp + err := c.do(ctx, http.MethodPost, "/api/v1/user/wallet/order", userID, "", walletOrderBody{ProductID: productID}, &out) + return out, err +} + // CatalogAtomResp is one atom line of a storefront product: the value type it grants and quantity. type CatalogAtomResp struct { AtomType string `json:"atom_type"` diff --git a/gateway/internal/transcode/encode.go b/gateway/internal/transcode/encode.go index b99c973..a836722 100644 --- a/gateway/internal/transcode/encode.go +++ b/gateway/internal/transcode/encode.go @@ -37,6 +37,19 @@ func encodeAck(ok bool) []byte { return b.FinishedBytes() } +// encodeWalletOrder builds a WalletOrderResponse payload: the created order id and the provider +// launch URL the client opens. +func encodeWalletOrder(o backendclient.WalletOrderResp) []byte { + b := flatbuffers.NewBuilder(128) + oid := b.CreateString(o.OrderID) + url := b.CreateString(o.RedirectURL) + fb.WalletOrderResponseStart(b) + fb.WalletOrderResponseAddOrderId(b, oid) + fb.WalletOrderResponseAddRedirectUrl(b, url) + b.Finish(fb.WalletOrderResponseEnd(b)) + return b.FinishedBytes() +} + // encodeDeleteRequestResult builds an AccountDeleteRequestResult payload reporting which // deletion step-up the account uses ("email" | "phrase"). func encodeDeleteRequestResult(method string) []byte { diff --git a/gateway/internal/transcode/transcode.go b/gateway/internal/transcode/transcode.go index 3b5c76f..0b6a399 100644 --- a/gateway/internal/transcode/transcode.go +++ b/gateway/internal/transcode/transcode.go @@ -55,6 +55,7 @@ const ( MsgWalletGet = "wallet.get" MsgWalletCatalog = "wallet.catalog" MsgWalletBuy = "wallet.buy" + MsgWalletOrder = "wallet.order" ) // Request is one decoded Execute call. @@ -111,6 +112,7 @@ func NewRegistry(backend *backendclient.Client, tg TelegramValidator, opts ...Op r.ops[MsgWalletGet] = Op{Handler: walletHandler(backend), Auth: true} r.ops[MsgWalletCatalog] = Op{Handler: walletCatalogHandler(backend), Auth: true} r.ops[MsgWalletBuy] = Op{Handler: walletBuyHandler(backend), Auth: true} + r.ops[MsgWalletOrder] = Op{Handler: walletOrderHandler(backend), Auth: true} r.ops[MsgBlockStatus] = Op{Handler: blockStatusHandler(backend), Auth: true} r.ops[MsgGameSubmitPlay] = Op{Handler: submitPlayHandler(backend), Auth: true} r.ops[MsgGameState] = Op{Handler: gameStateHandler(backend), Auth: true} @@ -331,6 +333,17 @@ func walletBuyHandler(backend *backendclient.Client) Handler { } } +func walletOrderHandler(backend *backendclient.Client) Handler { + return func(ctx context.Context, req Request) ([]byte, error) { + in := fb.GetRootAsWalletOrderRequest(req.Payload, 0) + o, err := backend.WalletOrder(ctx, req.UserID, string(in.ProductId())) + if err != nil { + return nil, err + } + return encodeWalletOrder(o), nil + } +} + func blockStatusHandler(backend *backendclient.Client) Handler { return func(ctx context.Context, req Request) ([]byte, error) { bs, err := backend.BlockStatus(ctx, req.UserID) diff --git a/pkg/fbs/scrabble.fbs b/pkg/fbs/scrabble.fbs index 89e13f5..196d2ce 100644 --- a/pkg/fbs/scrabble.fbs +++ b/pkg/fbs/scrabble.fbs @@ -873,6 +873,18 @@ table WalletBuyRequest { product_id:string; } +// WalletOrderRequest opens a money order to fund a chip pack: the pack to buy. +table WalletOrderRequest { + product_id:string; +} + +// WalletOrderResponse returns the created order id and the provider launch URL the client opens +// (the Robokassa hosted-payment page); chips are credited later, by the verified server callback. +table WalletOrderResponse { + order_id:string; + redirect_url:string; +} + // CatalogAtom is one atom line of a storefront product: the base value type it grants // ("chips"/"hints"/"noads_days"/"tournament") and how many of it the product carries. table CatalogAtom { diff --git a/pkg/fbs/scrabblefb/WalletOrderRequest.go b/pkg/fbs/scrabblefb/WalletOrderRequest.go new file mode 100644 index 0000000..a19177d --- /dev/null +++ b/pkg/fbs/scrabblefb/WalletOrderRequest.go @@ -0,0 +1,60 @@ +// Code generated by the FlatBuffers compiler. DO NOT EDIT. + +package scrabblefb + +import ( + flatbuffers "github.com/google/flatbuffers/go" +) + +type WalletOrderRequest struct { + _tab flatbuffers.Table +} + +func GetRootAsWalletOrderRequest(buf []byte, offset flatbuffers.UOffsetT) *WalletOrderRequest { + n := flatbuffers.GetUOffsetT(buf[offset:]) + x := &WalletOrderRequest{} + x.Init(buf, n+offset) + return x +} + +func FinishWalletOrderRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.Finish(offset) +} + +func GetSizePrefixedRootAsWalletOrderRequest(buf []byte, offset flatbuffers.UOffsetT) *WalletOrderRequest { + n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) + x := &WalletOrderRequest{} + x.Init(buf, n+offset+flatbuffers.SizeUint32) + return x +} + +func FinishSizePrefixedWalletOrderRequestBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.FinishSizePrefixed(offset) +} + +func (rcv *WalletOrderRequest) Init(buf []byte, i flatbuffers.UOffsetT) { + rcv._tab.Bytes = buf + rcv._tab.Pos = i +} + +func (rcv *WalletOrderRequest) Table() flatbuffers.Table { + return rcv._tab +} + +func (rcv *WalletOrderRequest) ProductId() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func WalletOrderRequestStart(builder *flatbuffers.Builder) { + builder.StartObject(1) +} +func WalletOrderRequestAddProductId(builder *flatbuffers.Builder, productId flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(productId), 0) +} +func WalletOrderRequestEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + return builder.EndObject() +} diff --git a/pkg/fbs/scrabblefb/WalletOrderResponse.go b/pkg/fbs/scrabblefb/WalletOrderResponse.go new file mode 100644 index 0000000..b3aa961 --- /dev/null +++ b/pkg/fbs/scrabblefb/WalletOrderResponse.go @@ -0,0 +1,71 @@ +// Code generated by the FlatBuffers compiler. DO NOT EDIT. + +package scrabblefb + +import ( + flatbuffers "github.com/google/flatbuffers/go" +) + +type WalletOrderResponse struct { + _tab flatbuffers.Table +} + +func GetRootAsWalletOrderResponse(buf []byte, offset flatbuffers.UOffsetT) *WalletOrderResponse { + n := flatbuffers.GetUOffsetT(buf[offset:]) + x := &WalletOrderResponse{} + x.Init(buf, n+offset) + return x +} + +func FinishWalletOrderResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.Finish(offset) +} + +func GetSizePrefixedRootAsWalletOrderResponse(buf []byte, offset flatbuffers.UOffsetT) *WalletOrderResponse { + n := flatbuffers.GetUOffsetT(buf[offset+flatbuffers.SizeUint32:]) + x := &WalletOrderResponse{} + x.Init(buf, n+offset+flatbuffers.SizeUint32) + return x +} + +func FinishSizePrefixedWalletOrderResponseBuffer(builder *flatbuffers.Builder, offset flatbuffers.UOffsetT) { + builder.FinishSizePrefixed(offset) +} + +func (rcv *WalletOrderResponse) Init(buf []byte, i flatbuffers.UOffsetT) { + rcv._tab.Bytes = buf + rcv._tab.Pos = i +} + +func (rcv *WalletOrderResponse) Table() flatbuffers.Table { + return rcv._tab +} + +func (rcv *WalletOrderResponse) OrderId() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(4)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func (rcv *WalletOrderResponse) RedirectUrl() []byte { + o := flatbuffers.UOffsetT(rcv._tab.Offset(6)) + if o != 0 { + return rcv._tab.ByteVector(o + rcv._tab.Pos) + } + return nil +} + +func WalletOrderResponseStart(builder *flatbuffers.Builder) { + builder.StartObject(2) +} +func WalletOrderResponseAddOrderId(builder *flatbuffers.Builder, orderId flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(0, flatbuffers.UOffsetT(orderId), 0) +} +func WalletOrderResponseAddRedirectUrl(builder *flatbuffers.Builder, redirectUrl flatbuffers.UOffsetT) { + builder.PrependUOffsetTSlot(1, flatbuffers.UOffsetT(redirectUrl), 0) +} +func WalletOrderResponseEnd(builder *flatbuffers.Builder) flatbuffers.UOffsetT { + return builder.EndObject() +} diff --git a/ui/src/gen/fbs/scrabblefb.ts b/ui/src/gen/fbs/scrabblefb.ts index df83fb6..c6dff38 100644 --- a/ui/src/gen/fbs/scrabblefb.ts +++ b/ui/src/gen/fbs/scrabblefb.ts @@ -86,6 +86,8 @@ export { UpdateProfileRequest } from './scrabblefb/update-profile-request.js'; export { VKLoginRequest } from './scrabblefb/vklogin-request.js'; export { Wallet } from './scrabblefb/wallet.js'; export { WalletBuyRequest } from './scrabblefb/wallet-buy-request.js'; +export { WalletOrderRequest } from './scrabblefb/wallet-order-request.js'; +export { WalletOrderResponse } from './scrabblefb/wallet-order-response.js'; export { WalletSegment } from './scrabblefb/wallet-segment.js'; export { WordCheckResult } from './scrabblefb/word-check-result.js'; export { YourTurnEvent } from './scrabblefb/your-turn-event.js'; diff --git a/ui/src/gen/fbs/scrabblefb/wallet-order-request.ts b/ui/src/gen/fbs/scrabblefb/wallet-order-request.ts new file mode 100644 index 0000000..f4e4d8c --- /dev/null +++ b/ui/src/gen/fbs/scrabblefb/wallet-order-request.ts @@ -0,0 +1,48 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +import * as flatbuffers from 'flatbuffers'; + +export class WalletOrderRequest { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):WalletOrderRequest { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsWalletOrderRequest(bb:flatbuffers.ByteBuffer, obj?:WalletOrderRequest):WalletOrderRequest { + return (obj || new WalletOrderRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsWalletOrderRequest(bb:flatbuffers.ByteBuffer, obj?:WalletOrderRequest):WalletOrderRequest { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new WalletOrderRequest()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +productId():string|null +productId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +productId(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +static startWalletOrderRequest(builder:flatbuffers.Builder) { + builder.startObject(1); +} + +static addProductId(builder:flatbuffers.Builder, productIdOffset:flatbuffers.Offset) { + builder.addFieldOffset(0, productIdOffset, 0); +} + +static endWalletOrderRequest(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createWalletOrderRequest(builder:flatbuffers.Builder, productIdOffset:flatbuffers.Offset):flatbuffers.Offset { + WalletOrderRequest.startWalletOrderRequest(builder); + WalletOrderRequest.addProductId(builder, productIdOffset); + return WalletOrderRequest.endWalletOrderRequest(builder); +} +} diff --git a/ui/src/gen/fbs/scrabblefb/wallet-order-response.ts b/ui/src/gen/fbs/scrabblefb/wallet-order-response.ts new file mode 100644 index 0000000..b7314e9 --- /dev/null +++ b/ui/src/gen/fbs/scrabblefb/wallet-order-response.ts @@ -0,0 +1,60 @@ +// automatically generated by the FlatBuffers compiler, do not modify + +import * as flatbuffers from 'flatbuffers'; + +export class WalletOrderResponse { + bb: flatbuffers.ByteBuffer|null = null; + bb_pos = 0; + __init(i:number, bb:flatbuffers.ByteBuffer):WalletOrderResponse { + this.bb_pos = i; + this.bb = bb; + return this; +} + +static getRootAsWalletOrderResponse(bb:flatbuffers.ByteBuffer, obj?:WalletOrderResponse):WalletOrderResponse { + return (obj || new WalletOrderResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +static getSizePrefixedRootAsWalletOrderResponse(bb:flatbuffers.ByteBuffer, obj?:WalletOrderResponse):WalletOrderResponse { + bb.setPosition(bb.position() + flatbuffers.SIZE_PREFIX_LENGTH); + return (obj || new WalletOrderResponse()).__init(bb.readInt32(bb.position()) + bb.position(), bb); +} + +orderId():string|null +orderId(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +orderId(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 4); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +redirectUrl():string|null +redirectUrl(optionalEncoding:flatbuffers.Encoding):string|Uint8Array|null +redirectUrl(optionalEncoding?:any):string|Uint8Array|null { + const offset = this.bb!.__offset(this.bb_pos, 6); + return offset ? this.bb!.__string(this.bb_pos + offset, optionalEncoding) : null; +} + +static startWalletOrderResponse(builder:flatbuffers.Builder) { + builder.startObject(2); +} + +static addOrderId(builder:flatbuffers.Builder, orderIdOffset:flatbuffers.Offset) { + builder.addFieldOffset(0, orderIdOffset, 0); +} + +static addRedirectUrl(builder:flatbuffers.Builder, redirectUrlOffset:flatbuffers.Offset) { + builder.addFieldOffset(1, redirectUrlOffset, 0); +} + +static endWalletOrderResponse(builder:flatbuffers.Builder):flatbuffers.Offset { + const offset = builder.endObject(); + return offset; +} + +static createWalletOrderResponse(builder:flatbuffers.Builder, orderIdOffset:flatbuffers.Offset, redirectUrlOffset:flatbuffers.Offset):flatbuffers.Offset { + WalletOrderResponse.startWalletOrderResponse(builder); + WalletOrderResponse.addOrderId(builder, orderIdOffset); + WalletOrderResponse.addRedirectUrl(builder, redirectUrlOffset); + return WalletOrderResponse.endWalletOrderResponse(builder); +} +}