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

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:
Ilia Denisov
2026-07-09 21:35:29 +02:00
parent 5612bb624d
commit 6e03ce0131
32 changed files with 1830 additions and 94 deletions
+22 -9
View File
@@ -522,9 +522,20 @@ refetch fallback. The **VK Votes rail** is delivered too: the client opens
`VKWebAppShowOrderBox({item: order_id})`; a two-phase signed server callback (`get_item` → the pack
title + vote price; a chargeable `order_status_change` → the same `Fund` with source=`vk`, idempotent
on VK's own order id) is verified at the gateway with the app protected key (`GATEWAY_VK_APP_SECRET`,
already deployed) and proxied to the backend intake. Remaining: the TG-Stars rail and refunds; and
hiding the ad banner on a no-ads purchase (a spend-path `NotifyBanner`, deferred with the owner's
agreement).
already deployed) and proxied to the backend intake. The **Telegram Stars rail** is delivered on
`feature/payment-intake-tg-stars`: only the bot reaches Telegram, so the **invoice is minted by the
bot** — on the `wallet.order` path the gateway sends a new `CreateInvoice` command over the reverse
bot-link and the bot returns the `createInvoiceLink` (XTR) in its Ack, handed to the client's
`WebApp.openInvoice`. The bot answers `pre_checkout_query` via a new bot→gateway **`ValidatePreCheckout`**
unary (backed by the backend: the order must exist, be still creditable and **not already paid** — the
reusable-invoice double-pay guard — with a matching amount); the decline reason is localised to the
order account's language. A completed `successful_payment` is persisted to a pure-Go **SQLite outbox**
(`modernc.org/sqlite`) then forwarded by a new bot→gateway **`ForwardPayment`** unary into the same
`Fund` (source=`telegram`, idempotent on `telegram_payment_charge_id`, honours an expired order),
re-driven at startup and every 30 s. The rail is wired by `TELEGRAM_STARS_OUTBOX_DIR` (defaults to the
bot `/data` volume) but stays **inert until a chip pack carries an XTR price**, so seeding a Stars price
in the admin is the go-live. Remaining: refunds; and hiding the ad banner on a no-ads purchase (a
spend-path `NotifyBanner`, deferred with the owner's agreement).
**Goal.** Accept real money on all three rails into the payments domain: order-flow,
verified provider callbacks, idempotency, the TG bot SQLite outbox, the event dispatcher,
@@ -547,12 +558,14 @@ receipts, and refunds.
**TG bot outbox (`platform/telegram/`).**
- The bot receives `successful_payment` (and `pre_checkout_query`) via Bot API. Add a
**SQLite** store on the bot's disk: on receipt, persist → ack the Telegram update → forward
to a backend payments-intake endpoint (internal, authenticated over the existing reverse
mTLS bot-link) → on backend ack, mark `forwarded`. Retries with backoff; re-drive
undelivered on restart. Backend intake dedups by `telegram_payment_charge_id`.
- Provide the invoice creation path (Stars) from the Mini App via the bot as needed.
- The bot receives `successful_payment` (and `pre_checkout_query`) via Bot API. A **SQLite**
store on the bot's disk (`internal/outbox`): on receipt, persist → forward over the reverse
mTLS bot-link to the **gateway** (a `ForwardPayment` unary; the bot cannot dial the backend
directly) → the gateway proxies to the backend payments-intake REST → on a durable response,
mark `forwarded`. Re-drive undelivered on startup and on a 30 s tick. Backend intake dedups by
`telegram_payment_charge_id`.
- Invoice creation (Stars) is minted by the bot (`createInvoiceLink`, XTR) on the gateway's
`CreateInvoice` bot-link command, returned to the Mini App as the `openInvoice` link.
**Events & notifications.**