feat(payments): VK Votes payment rail
CI / changes (pull_request) Successful in 2s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 18s
CI / ui (pull_request) Successful in 1m10s
CI / conformance (pull_request) Successful in 9s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 1m45s

Wire the VK Mini Apps ("голоса") rail end to end, reusing the intake engine. The
wallet.order endpoint branches by rail: a VK context opens a pending order
(provider vk) and returns its id, which the client passes to VKWebAppShowOrderBox.
VK's two-phase payment callback is verified at the gateway with the app protected
key (GATEWAY_VK_APP_SECRET) and proxied to a backend intake handler: get_item
returns the ordered pack's title and vote price; a chargeable order_status_change
credits the vk segment exactly once (the same Fund, idempotent on VK's own order
id) and records a succeeded event, so the dispatcher push refreshes the wallet.
Integration test for the VK order->credit path.
This commit is contained in:
Ilia Denisov
2026-07-09 19:27:57 +02:00
parent 3e0763463f
commit 3bb9f10fad
10 changed files with 308 additions and 47 deletions
+9 -4
View File
@@ -73,11 +73,16 @@ func (s *Server) registerRoutes() {
u.GET("/wallet/catalog", s.handleWalletCatalog)
u.POST("/wallet/buy", s.handleWalletBuy)
}
if s.payments != nil && s.robokassa.MerchantLogin != "" {
// Direct-rail (Robokassa): open a pending order (user group), and receive the verified
// Result callback the gateway forwards (internal group, gateway-only — the single writer).
if s.payments != nil {
// The money order endpoint dispatches by rail (direct → Robokassa, vk → VK); an
// unsupported or unconfigured rail returns 501 from the handler. The provider callbacks are
// gateway-only (the single writer): the VK payment callback (both phases handled here), and
// the Robokassa Result callback when a merchant is configured.
u.POST("/wallet/order", s.handleWalletOrder)
s.internal.POST("/payments/robokassa/result", s.handleRobokassaResult)
s.internal.POST("/payments/vk/callback", s.handleVKCallback)
if s.robokassa.MerchantLogin != "" {
s.internal.POST("/payments/robokassa/result", s.handleRobokassaResult)
}
}
if s.links != nil {
// Account linking & merge. The request step always mails a code;