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:
+15
-6
@@ -219,12 +219,21 @@ the amount, credits, marks `paid`. **Idempotency:** dedup by `(provider, provide
|
||||
valid callback is **always** honoured, even on an expired order (`expired` ≠ cancellation —
|
||||
the money is real, the chips are owed). The user sees only successful purchases.
|
||||
|
||||
**TG bot outbox.** `successful_payment` reaches the bot only (Bot API, not the Mini App), and
|
||||
the bot host is weak and can lose connectivity, so the bot is a durable link. Store-and-
|
||||
forward on **SQLite** on the bot's disk: receive → store → ack the Telegram update → forward
|
||||
to payments (idempotent, dedup by `telegram_payment_charge_id`) → ack → mark `forwarded`.
|
||||
Retries with backoff; re-drives undelivered on restart. At-least-once delivery + idempotent
|
||||
intake = credited exactly once.
|
||||
**TG Stars.** Only the **bot** reaches Telegram, so the whole rail funnels through the reverse
|
||||
mTLS **bot-link** (bot ↔ gateway; the bot cannot dial the backend). The invoice is minted by the
|
||||
bot: on the order path the gateway sends a `CreateInvoice` command and the bot returns a
|
||||
`createInvoiceLink` (XTR) in its Ack, which the Mini App opens with `WebApp.openInvoice`. Before any
|
||||
star moves the bot answers `pre_checkout_query` via a bot→gateway `ValidatePreCheckout` unary
|
||||
(backed by the intake): approve only if the order exists, is still creditable and is **not already
|
||||
paid** — a Stars invoice link is reusable, so this gate is the one place a repeat payment is stopped
|
||||
before the charge; the decline reason is localised to the order account's language.
|
||||
|
||||
`successful_payment` reaches the bot only (Bot API, not the Mini App), and the bot host is weak and
|
||||
can lose connectivity, so the bot is a durable link. Store-and-forward on **SQLite** on the bot's
|
||||
disk (`internal/outbox`): persist on receipt (idempotent on `telegram_payment_charge_id`) → forward
|
||||
over the bot-link (a `ForwardPayment` unary; the gateway proxies to the intake) → on a durable
|
||||
response, mark `forwarded`. Re-drives undelivered on restart and on a periodic tick. At-least-once
|
||||
delivery + idempotent intake (dedup by `telegram_payment_charge_id`) = credited exactly once.
|
||||
|
||||
**Events.** The payments domain writes `payment_events` (succeeded / failed / refunded); a
|
||||
dispatcher fans out over channels — the live gRPC stream if the user is in-app, else the
|
||||
|
||||
Reference in New Issue
Block a user