1507ceb793
CI / changes (pull_request) Successful in 3s
CI / unit (pull_request) Successful in 11s
CI / integration (pull_request) Successful in 21s
CI / ui (pull_request) Successful in 1m15s
CI / conformance (pull_request) Successful in 10s
CI / gate (pull_request) Successful in 0s
CI / deploy (pull_request) Successful in 2m0s
Let an operator disable purchases live from the admin — a whole rail/channel or one account — and show the user a localized reason on their next attempt, so a provider outage or a misconfig is explained instead of a silent dead button. - rail kill switch (payments.rail_status, per rail direct:web / direct:android / vk / telegram): enabled + a per-language message, edited on the catalog page. Fail-open — a rail with no row stays enabled, so payments are never accidentally killed. The intake gate (CanPurchase in handleWalletOrder, before the order) returns payment_unavailable + the localized message, orthogonal to the security gates. - per-account override (payments.account_payment_override, a row only for non-default): allow / deny / default, edited on the user card. "allow" bypasses ONLY the ops rail switch, never the security gates (trusted platform, the email anchor, the VK-iOS freeze, the min client version). - wire: an additive ExecuteResponse.message envelope field (frozen-contract-safe); the gateway forwards a backend domain-error message; the client shows it on a payment_unavailable buy attempt. - admin: rail toggles on the catalog page, the override control on the user card. - tests: the pure gate (unit, TDD), the store + gate + override end-to-end (integration, migration 00016), the client (svelte-check / vitest). - docs: PAYMENTS (+ru), the decisions log (D45/D46). Fiscalization stays cabinet-side (owner decision) — no itemized-receipt code. Contour-safe: additive migration (two new tables, no wipe), the wire add is additive, and fail-open so nothing is disabled until an operator acts.
41 lines
1.7 KiB
Go
41 lines
1.7 KiB
Go
package payments
|
|
|
|
import "testing"
|
|
|
|
func TestPurchaseGate(t *testing.T) {
|
|
on := RailAvailability{Enabled: true}
|
|
offMsg := RailAvailability{Enabled: false, MessageRU: "Чиним", MessageEN: "Fixing"}
|
|
offNoMsg := RailAvailability{Enabled: false}
|
|
|
|
// OverrideDefault follows the rail switch.
|
|
if ok, _ := PurchaseGate(OverrideDefault, on, "en"); !ok {
|
|
t.Error("default + enabled rail should allow")
|
|
}
|
|
if ok, r := PurchaseGate(OverrideDefault, offMsg, "ru"); ok || r != "Чиним" {
|
|
t.Errorf("default + off rail (ru) = %v/%q, want false/Чиним", ok, r)
|
|
}
|
|
if ok, r := PurchaseGate(OverrideDefault, offMsg, "en"); ok || r != "Fixing" {
|
|
t.Errorf("default + off rail (en) = %v/%q, want false/Fixing", ok, r)
|
|
}
|
|
|
|
// Off rail with no custom message → the built-in default, localized (not the English default in ru).
|
|
if ok, r := PurchaseGate(OverrideDefault, offNoMsg, "ru"); ok || r != defaultUnavailable("ru") {
|
|
t.Errorf("default + off no-msg (ru) = %v/%q, want false + the ru default", ok, r)
|
|
}
|
|
|
|
// OverrideAllow bypasses the ops switch even when the rail is off.
|
|
if ok, r := PurchaseGate(OverrideAllow, offMsg, "en"); !ok || r != "" {
|
|
t.Errorf("allow + off rail = %v/%q, want true/empty (bypasses the ops switch)", ok, r)
|
|
}
|
|
|
|
// OverrideDeny blocks even when the rail is on, with a non-empty reason.
|
|
if ok, r := PurchaseGate(OverrideDeny, on, "en"); ok || r == "" {
|
|
t.Errorf("deny + on rail = %v/%q, want false + a reason", ok, r)
|
|
}
|
|
|
|
// The message falls back to the other language when only one is set.
|
|
if _, r := PurchaseGate(OverrideDefault, RailAvailability{MessageEN: "OnlyEN"}, "ru"); r != "OnlyEN" {
|
|
t.Errorf("off rail ru with only EN msg = %q, want OnlyEN (fallback)", r)
|
|
}
|
|
}
|