feat(payments): direct-rail order + intake handlers, config and reaper

Wire the Robokassa direct rail into the backend transport. POST
/api/v1/user/wallet/order (walletGate + a D36 confirmed-email gate for the
direct rail) opens a pending order and returns the signed Robokassa payment
URL. The internal, gateway-only /payments/robokassa/result endpoint verifies
the Result signature, credits the matched order exactly once via Fund (honoured
even if expired), records a succeeded payment event, and answers Robokassa's
"OK<InvId>". Add the Robokassa env config, an account HasConfirmedEmail check
(D36), the payment_events writer, and a periodic pending-order reaper. The
routes register only when a Robokassa merchant login is configured.
This commit is contained in:
Ilia Denisov
2026-07-09 16:59:34 +02:00
parent 7860efce48
commit 36a5730e52
8 changed files with 221 additions and 0 deletions
+6
View File
@@ -31,6 +31,7 @@ import (
"scrabble/backend/internal/payments"
"scrabble/backend/internal/ratewatch"
"scrabble/backend/internal/render"
"scrabble/backend/internal/robokassa"
"scrabble/backend/internal/session"
"scrabble/backend/internal/social"
"scrabble/backend/internal/telemetry"
@@ -109,6 +110,9 @@ type Deps struct {
// Renderer is the image-render sidecar client for the PNG export artifact. A
// nil Renderer makes the PNG download answer 404 (the GCG artifact still works).
Renderer *render.Client
// Robokassa configures the direct-rail (RUB) provider; an empty MerchantLogin leaves the
// order and Result-callback endpoints unregistered.
Robokassa robokassa.Config
}
// Server owns the gin engine, the underlying HTTP server and the readiness
@@ -136,6 +140,7 @@ type Server struct {
banview *banview.View
ads *ads.Service
payments *payments.Service
robokassa robokassa.Config
notifier notify.Publisher
console *adminconsole.Renderer
exportKey []byte
@@ -189,6 +194,7 @@ func New(addr string, deps Deps) *Server {
banview: deps.BanView,
ads: deps.Ads,
payments: deps.Payments,
robokassa: deps.Robokassa,
notifier: notifier,
renderer: deps.Renderer,
http: &http.Server{Addr: addr, Handler: engine},