feat(payments): Telegram Stars payment rail
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 22s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m57s
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 22s
CI / integration (pull_request) Successful in 20s
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m57s
Accept real money via Telegram Stars (XTR) — the third intake rail alongside Robokassa (direct) and VK Votes. Only the bot reaches Telegram, so the rail funnels through the reverse mTLS bot-link: - the gateway mints the invoice on a CreateInvoice command (the bot calls createInvoiceLink, XTR; the link goes to WebApp.openInvoice); - the bot gates each pre_checkout_query via a ValidatePreCheckout unary (the order must exist, be still creditable and not already paid — the reusable-invoice double-pay guard; the decline reason is localised to the order account's language); - a completed successful_payment is queued in a durable pure-Go SQLite outbox and forwarded via a ForwardPayment unary, credited once (idempotent on telegram_payment_charge_id, honours an expired order), re-driven on restart and every 30s. The rail is wired by TELEGRAM_STARS_OUTBOX_DIR (default /data) but stays inert until a chip pack carries an XTR price, so seeding a Stars price in the admin is the go-live. Tests: backend integration (order->forward->credit once, duplicate, pre_checkout gate) + bot outbox unit (idempotent, restart re-drive) + executor createInvoice. Docs: PAYMENTS(+ru) §9, ARCHITECTURE, the platform/telegram README, PLAN.
This commit is contained in:
@@ -27,6 +27,24 @@ service BotLink {
|
||||
// same mTLS channel when a user joins the chat, to decide whether to grant the
|
||||
// write permission. Delivery of the answer is request/response (not best-effort).
|
||||
rpc ResolveChatEligibility(ChatEligibilityRequest) returns (ChatEligibilityResponse);
|
||||
|
||||
// ValidatePreCheckout answers whether a Telegram Stars pre_checkout_query may be
|
||||
// approved before any star is charged: the order in the invoice payload exists, is
|
||||
// still creditable (pending or an honoured-expired order, never one already paid),
|
||||
// and its amount and currency match the invoice. The bot calls it on every
|
||||
// pre_checkout_query and approves only on ok; a not-ok answer or a channel failure
|
||||
// declines the charge (fail-closed). Reusable Stars invoice links make this gate the
|
||||
// one place a repeat payment is stopped before money moves. Request/response.
|
||||
rpc ValidatePreCheckout(PreCheckoutRequest) returns (PreCheckoutResponse);
|
||||
|
||||
// ForwardPayment delivers a completed Telegram Stars payment from the bot's durable
|
||||
// outbox to the gateway for crediting. The bot calls it (retrying until it gets a
|
||||
// response) after Telegram confirms the payment; the gateway forwards it to the
|
||||
// backend intake, which credits the order once, idempotent on
|
||||
// telegram_payment_charge_id. A response means the payment was durably handled
|
||||
// (credited, or recorded as unmatched) and the bot may forget the outbox row; a
|
||||
// transport error leaves the row for a later retry. Request/response.
|
||||
rpc ForwardPayment(ForwardPaymentRequest) returns (ForwardPaymentResponse);
|
||||
}
|
||||
|
||||
// FromBot is a message the bot sends to the gateway: the opening Hello, then one
|
||||
@@ -61,17 +79,21 @@ message Command {
|
||||
scrabble.telegram.v1.SendToUserRequest send_to_user = 3;
|
||||
scrabble.telegram.v1.SendToGameChannelRequest send_to_channel = 4;
|
||||
ChatGateCommand chat_gate = 5;
|
||||
CreateInvoiceCommand create_invoice = 6;
|
||||
}
|
||||
}
|
||||
|
||||
// Ack reports the outcome of the Command with command_id. delivered mirrors the
|
||||
// connector delivery semantics (false when the kind is not rendered out-of-app, the
|
||||
// user never started the bot, or no channel is configured); error carries an
|
||||
// unexpected transport/render failure, distinct from a clean not-delivered.
|
||||
// unexpected transport/render failure, distinct from a clean not-delivered. result
|
||||
// carries a command's return value when it has one (the created invoice link for a
|
||||
// create_invoice command); it is empty otherwise.
|
||||
message Ack {
|
||||
string command_id = 1;
|
||||
bool delivered = 2;
|
||||
string error = 3;
|
||||
string result = 4;
|
||||
}
|
||||
|
||||
// ChatGateCommand sets a Telegram user's write access in the moderated discussion
|
||||
@@ -99,3 +121,48 @@ message ChatEligibilityResponse {
|
||||
bool registered = 1;
|
||||
bool eligible = 2;
|
||||
}
|
||||
|
||||
// CreateInvoiceCommand asks the bot to mint a Telegram Stars invoice link for a
|
||||
// pending order (createInvoiceLink in XTR). payload is the order id, which Telegram
|
||||
// echoes back in the pre_checkout_query and the successful_payment; amount is the
|
||||
// price in whole stars; title and description are shown on the invoice. The bot
|
||||
// returns the link in its Ack result.
|
||||
message CreateInvoiceCommand {
|
||||
string title = 1;
|
||||
string description = 2;
|
||||
string payload = 3;
|
||||
int64 amount = 4;
|
||||
}
|
||||
|
||||
// PreCheckoutRequest asks whether a Stars pre_checkout_query for order_id at amount
|
||||
// (whole stars) in currency may be approved before the charge.
|
||||
message PreCheckoutRequest {
|
||||
string order_id = 1;
|
||||
int64 amount = 2;
|
||||
string currency = 3;
|
||||
}
|
||||
|
||||
// PreCheckoutResponse is the approval answer. ok approves the charge; reason carries a
|
||||
// short user-facing decline message when ok is false.
|
||||
message PreCheckoutResponse {
|
||||
bool ok = 1;
|
||||
string reason = 2;
|
||||
}
|
||||
|
||||
// ForwardPaymentRequest carries a completed Stars payment: order_id from the invoice
|
||||
// payload, the Telegram charge id (the idempotency key), the amount in whole stars,
|
||||
// and the payer's Telegram user id.
|
||||
message ForwardPaymentRequest {
|
||||
string order_id = 1;
|
||||
string telegram_payment_charge_id = 2;
|
||||
int64 amount = 3;
|
||||
int64 telegram_user_id = 4;
|
||||
}
|
||||
|
||||
// ForwardPaymentResponse reports the durable outcome. credited is true when the order
|
||||
// was credited (or already had been); false means the payment was recorded but could
|
||||
// not be matched to a creditable order (an operator follows up). Either way the bot
|
||||
// may forget the outbox row — only a transport error triggers a retry.
|
||||
message ForwardPaymentResponse {
|
||||
bool credited = 1;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user