package payments import "slices" // PurchaseOverride is a per-account purchase override that forces purchases allowed or denied for // one account regardless of the operational rail switch, or is absent (OverrideDefault) when the // account follows the rail switch. The zero value is OverrideDefault, so a missing override row maps // to it. OverrideAllow bypasses ONLY the operational rail switch — never the security / compliance // gates (trusted platform, the D36 email anchor, the VK-iOS spend freeze, the min client version), // which CreateOrder enforces separately. type PurchaseOverride int const ( // OverrideDefault follows the rail switch (no override row for the account). OverrideDefault PurchaseOverride = iota // OverrideAllow always allows the purchase, bypassing only the operational rail switch. OverrideAllow // OverrideDeny always denies the purchase for the account. OverrideDeny ) // RailAvailability is a payment rail's operational availability: whether purchases are enabled and, // when disabled, the operator's explanation in each language, shown to the user on a purchase // attempt. A rail with no status row is treated as enabled (fail-open — see the store), so the // zero value is never used as a live "enabled" default. type RailAvailability struct { Enabled bool MessageRU string MessageEN string } // PurchaseGate decides whether an account may open a purchase order on a rail, from the per-account // override and the rail's operational availability. It is the operational layer only — the caller // still enforces the security gates. On a block it returns a reason localized to lang ("ru", else // English). Fail-open: OverrideDefault on an enabled rail allows. func PurchaseGate(override PurchaseOverride, rail RailAvailability, lang string) (ok bool, reason string) { switch override { case OverrideAllow: return true, "" // bypasses only the ops switch; the security gates still apply upstream case OverrideDeny: return false, defaultUnavailable(lang) // a per-account block — a neutral reason default: // OverrideDefault — follow the rail switch if rail.Enabled { return true, "" } return false, railMessage(rail, lang) } } // railMessage returns the operator's rail-off message in lang, falling back to the other language // and then to the built-in default when the operator left both blank. func railMessage(rail RailAvailability, lang string) string { if lang == "ru" { if rail.MessageRU != "" { return rail.MessageRU } if rail.MessageEN != "" { return rail.MessageEN } } else { if rail.MessageEN != "" { return rail.MessageEN } if rail.MessageRU != "" { return rail.MessageRU } } return defaultUnavailable(lang) } // defaultUnavailable is the built-in localized "payments unavailable" reason used when the operator // set no custom message, and for a per-account deny. func defaultUnavailable(lang string) string { if lang == "ru" { return "Оплата временно недоступна. Попробуйте позже." } return "Payments are temporarily unavailable. Please try again later." } // Rail keys name the operational payment rails the kill switch and the per-account override key on. // The direct rail is split per channel (D42); vk and telegram are single-rail. const ( RailDirectWeb = "direct:web" RailDirectAndroid = "direct:android" RailVK = "vk" RailTelegram = "telegram" ) // KnownRails is the fixed set of rail keys, for the admin editor and for validation. var KnownRails = []string{RailDirectWeb, RailDirectAndroid, RailVK, RailTelegram} // RailKey maps a payment context to its operational rail key: the direct rail by channel subtype // (android, else web), or the store rail's own name (vk / telegram). func RailKey(kind Source, subtype string) string { if kind == SourceDirect { if subtype == "android" { return RailDirectAndroid } return RailDirectWeb } return string(kind) } // isKnownRail reports whether rail is one of the fixed rail keys. func isKnownRail(rail string) bool { return slices.Contains(KnownRails, rail) }