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.
64 lines
2.4 KiB
Go
64 lines
2.4 KiB
Go
package payments
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
// CanPurchase reports whether an account may open a purchase order on rail, combining the account's
|
|
// per-account override with the rail's operational switch (PurchaseGate). It is the operational
|
|
// availability layer only — the caller still enforces the security gates (trusted platform, the D36
|
|
// email anchor, the VK-iOS spend freeze, the min client version). On a block, reason is localized to
|
|
// lang for the user; err is only a store failure.
|
|
func (s *Service) CanPurchase(ctx context.Context, accountID uuid.UUID, rail, lang string) (ok bool, reason string, err error) {
|
|
ov, err := s.store.purchaseOverride(ctx, accountID)
|
|
if err != nil {
|
|
return false, "", err
|
|
}
|
|
avail, err := s.store.railAvailability(ctx, rail)
|
|
if err != nil {
|
|
return false, "", err
|
|
}
|
|
ok, reason = PurchaseGate(ov, avail, lang)
|
|
return ok, reason, nil
|
|
}
|
|
|
|
// RailStatuses returns every known rail's operational status for the admin editor, filling a rail
|
|
// with no stored row with the fail-open enabled default so the editor always shows one row per rail.
|
|
func (s *Service) RailStatuses(ctx context.Context) (map[string]RailAvailability, error) {
|
|
stored, err := s.store.allRailStatus(ctx)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
out := make(map[string]RailAvailability, len(KnownRails))
|
|
for _, rail := range KnownRails {
|
|
if a, ok := stored[rail]; ok {
|
|
out[rail] = a
|
|
} else {
|
|
out[rail] = RailAvailability{Enabled: true}
|
|
}
|
|
}
|
|
return out, nil
|
|
}
|
|
|
|
// SetRailStatus upserts a rail's operational status from the admin editor. It rejects an unknown rail
|
|
// key so a typo cannot create a dead row.
|
|
func (s *Service) SetRailStatus(ctx context.Context, rail string, a RailAvailability) error {
|
|
if !isKnownRail(rail) {
|
|
return fmt.Errorf("payments: unknown rail %q", rail)
|
|
}
|
|
return s.store.setRailStatus(ctx, rail, a, s.clock())
|
|
}
|
|
|
|
// PurchaseOverrideFor returns an account's purchase override (OverrideDefault when none is set).
|
|
func (s *Service) PurchaseOverrideFor(ctx context.Context, accountID uuid.UUID) (PurchaseOverride, error) {
|
|
return s.store.purchaseOverride(ctx, accountID)
|
|
}
|
|
|
|
// SetPurchaseOverride sets or clears an account's purchase override (OverrideDefault deletes the row).
|
|
func (s *Service) SetPurchaseOverride(ctx context.Context, accountID uuid.UUID, ov PurchaseOverride) error {
|
|
return s.store.setPurchaseOverride(ctx, accountID, ov, s.clock())
|
|
}
|