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:
@@ -14,6 +14,7 @@ import (
|
||||
"scrabble/backend/internal/lobby"
|
||||
"scrabble/backend/internal/postgres"
|
||||
"scrabble/backend/internal/ratewatch"
|
||||
"scrabble/backend/internal/robokassa"
|
||||
"scrabble/backend/internal/robot"
|
||||
"scrabble/backend/internal/telemetry"
|
||||
)
|
||||
@@ -64,6 +65,9 @@ type Config struct {
|
||||
// RendererURL is the base URL of the internal image-render sidecar (e.g.
|
||||
// http://renderer:8090). Empty disables the PNG export artifact.
|
||||
RendererURL string
|
||||
// Robokassa configures the direct-rail (RUB) payment provider. An empty MerchantLogin
|
||||
// leaves the direct order and Result-callback endpoints unregistered.
|
||||
Robokassa robokassa.Config
|
||||
}
|
||||
|
||||
// Defaults applied when the corresponding environment variable is unset.
|
||||
@@ -153,6 +157,13 @@ func Load() (Config, error) {
|
||||
AdminTo: os.Getenv("BACKEND_ADMIN_EMAIL"),
|
||||
}
|
||||
|
||||
robo := robokassa.Config{
|
||||
MerchantLogin: os.Getenv("BACKEND_ROBOKASSA_MERCHANT_LOGIN"),
|
||||
Password1: os.Getenv("BACKEND_ROBOKASSA_PASSWORD1"),
|
||||
Password2: os.Getenv("BACKEND_ROBOKASSA_PASSWORD2"),
|
||||
IsTest: os.Getenv("BACKEND_ROBOKASSA_TEST") == "1",
|
||||
}
|
||||
|
||||
c := Config{
|
||||
HTTPAddr: envOr("BACKEND_HTTP_ADDR", defaultHTTPAddr),
|
||||
GRPCAddr: envOr("BACKEND_GRPC_ADDR", defaultGRPCAddr),
|
||||
@@ -170,6 +181,7 @@ func Load() (Config, error) {
|
||||
GuestRetention: guestRetention,
|
||||
ExportSignKey: os.Getenv("BACKEND_EXPORT_SIGN_KEY"),
|
||||
RendererURL: os.Getenv("BACKEND_RENDERER_URL"),
|
||||
Robokassa: robo,
|
||||
}
|
||||
if err := c.validate(); err != nil {
|
||||
return Config{}, err
|
||||
@@ -222,6 +234,9 @@ func (c Config) validate() error {
|
||||
return fmt.Errorf("config: BACKEND_PUBLIC_BASE_URL %q must be an absolute URL (scheme://host)", c.PublicBaseURL)
|
||||
}
|
||||
}
|
||||
if c.Robokassa.MerchantLogin != "" && (c.Robokassa.Password1 == "" || c.Robokassa.Password2 == "") {
|
||||
return fmt.Errorf("config: BACKEND_ROBOKASSA_PASSWORD1 and BACKEND_ROBOKASSA_PASSWORD2 must be set when BACKEND_ROBOKASSA_MERCHANT_LOGIN is")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user