package data import ( "strconv" "strings" "sync" "github.com/GMWalletApp/epusdt/config" "github.com/GMWalletApp/epusdt/model/dao" "github.com/GMWalletApp/epusdt/model/mdb" "gorm.io/gorm/clause" ) // Simple in-process cache. Settings are read frequently (on every order // create, every scanner tick, etc.); the DB round-trip isn't free. Cache // is invalidated explicitly via SetString/Delete and expires on process // restart — that's enough for admin-edit semantics without TTL complexity. var ( settingsCache = map[string]string{} settingsCacheMu sync.RWMutex settingsLoadMu sync.Mutex // serializes the lazy bootstrap load settingsLoaded bool ) func loadAllSettings() error { var rows []mdb.Setting if err := dao.Mdb.Find(&rows).Error; err != nil { return err } settingsCacheMu.Lock() defer settingsCacheMu.Unlock() settingsCache = make(map[string]string, len(rows)) for _, r := range rows { settingsCache[r.Key] = r.Value } settingsLoaded = true return nil } // ensureLoaded performs a thread-safe lazy bootstrap of the cache. Uses // a dedicated mutex + double-checked flag so two concurrent callers // don't both hit the DB. Can't use sync.Once because ReloadSettings // needs to be able to rebuild after a failed initial load. func ensureLoaded() { settingsCacheMu.RLock() loaded := settingsLoaded settingsCacheMu.RUnlock() if loaded { return } settingsLoadMu.Lock() defer settingsLoadMu.Unlock() // Recheck under the load mutex — another goroutine may have loaded // while we were blocked. settingsCacheMu.RLock() loaded = settingsLoaded settingsCacheMu.RUnlock() if loaded { return } _ = loadAllSettings() } // ReloadSettings forces a refresh from DB. Call after external writes. func ReloadSettings() error { settingsLoadMu.Lock() defer settingsLoadMu.Unlock() return loadAllSettings() } // GetSettingString returns the string value for key, or fallback if unset. func GetSettingString(key, fallback string) string { ensureLoaded() settingsCacheMu.RLock() defer settingsCacheMu.RUnlock() v, ok := settingsCache[key] if !ok || strings.TrimSpace(v) == "" { return fallback } return v } // GetSettingInt returns the int value for key, or fallback on miss/parse fail. func GetSettingInt(key string, fallback int) int { v := GetSettingString(key, "") if v == "" { return fallback } n, err := strconv.Atoi(v) if err != nil { return fallback } return n } // GetSettingFloat returns the float value for key, or fallback on miss/parse fail. func GetSettingFloat(key string, fallback float64) float64 { v := GetSettingString(key, "") if v == "" { return fallback } f, err := strconv.ParseFloat(v, 64) if err != nil { return fallback } return f } // GetSettingBool returns the bool value for key, or fallback on miss/parse fail. func GetSettingBool(key string, fallback bool) bool { v := GetSettingString(key, "") if v == "" { return fallback } b, err := strconv.ParseBool(v) if err != nil { return fallback } return b } const ( DefaultAmountPrecision = 2 MinAmountPrecision = 2 MaxAmountPrecision = 6 ) // NormalizeAmountPrecision clamps external precision values to the supported // range used by order creation and transaction matching. func NormalizeAmountPrecision(precision int) int { if precision < MinAmountPrecision || precision > MaxAmountPrecision { return DefaultAmountPrecision } return precision } func GetAmountPrecision() int { return NormalizeAmountPrecision(GetSettingInt(mdb.SettingKeyAmountPrecision, DefaultAmountPrecision)) } // SetSetting upserts a setting row and refreshes the cache entry. func SetSetting(group, key, value, valueType string) error { if valueType == "" { valueType = mdb.SettingTypeString } row := mdb.Setting{Group: group, Key: key, Value: value, Type: valueType} err := dao.Mdb.Clauses(clause.OnConflict{ Columns: []clause.Column{{Name: "key"}}, DoUpdates: clause.AssignmentColumns([]string{"group", "value", "type", "updated_at"}), }).Create(&row).Error if err != nil { return err } settingsCacheMu.Lock() settingsCache[key] = value settingsCacheMu.Unlock() return nil } // DeleteSetting removes a setting row and drops the cache entry. func DeleteSetting(key string) error { if err := dao.Mdb.Where("key = ?", key).Delete(&mdb.Setting{}).Error; err != nil { return err } settingsCacheMu.Lock() delete(settingsCache, key) settingsCacheMu.Unlock() return nil } // sensitiveSettingKeys lists keys that must never be returned to API callers. var sensitiveSettingKeys = []string{ mdb.SettingKeyJwtSecret, mdb.SettingKeyInitAdminPasswordPlain, mdb.SettingKeyInitAdminPasswordHash, mdb.SettingKeyInitAdminPasswordFetched, mdb.SettingKeyInitAdminPasswordChanged, } func getFirstNonEmptySetting(fallback string, keys ...string) string { for _, key := range keys { value := strings.TrimSpace(GetSettingString(key, "")) if value != "" { return value } } return fallback } func GetBrandCashierName() string { return getFirstNonEmptySetting("", mdb.SettingKeyBrandCheckoutName, mdb.SettingKeyBrandSiteName) } func GetBrandLogoURL() string { return strings.TrimSpace(GetSettingString(mdb.SettingKeyBrandLogoUrl, "")) } func GetBrandWebsiteTitle() string { return getFirstNonEmptySetting("", mdb.SettingKeyBrandSiteTitle, mdb.SettingKeyBrandPageTitle) } func GetBrandSupportURL() string { return strings.TrimSpace(GetSettingString(mdb.SettingKeyBrandSupportUrl, "")) } func GetBrandBackgroundColor() string { return strings.TrimSpace(GetSettingString(mdb.SettingKeyBrandBackgroundColor, "")) } func GetBrandBackgroundImageURL() string { return strings.TrimSpace(GetSettingString(mdb.SettingKeyBrandBackgroundImageUrl, "")) } // OkPay settings helpers keep the provider-specific defaults in one place so // business logic does not need to repeat raw settings keys. func GetOkPayEnabled() bool { return GetSettingBool(mdb.SettingKeyOkPayEnabled, false) } func GetOkPayShopID() string { return strings.TrimSpace(GetSettingString(mdb.SettingKeyOkPayShopID, "")) } func GetOkPayShopToken() string { return strings.TrimSpace(GetSettingString(mdb.SettingKeyOkPayShopToken, "")) } func GetOkPayAPIURL() string { return strings.TrimSpace(GetSettingString(mdb.SettingKeyOkPayAPIURL, "https://api.okaypay.me/shop/")) } func GetOkPayCallbackURL() string { if configured := strings.TrimSpace(GetSettingString(mdb.SettingKeyOkPayCallbackURL, "")); configured != "" { return configured } appURI := strings.TrimSpace(config.GetAppUri()) if appURI == "" { return "" } return strings.TrimRight(appURI, "/") + "/payments/okpay/v1/notify" } func GetOkPayReturnURL() string { return strings.TrimSpace(GetSettingString(mdb.SettingKeyOkPayReturnURL, "")) } func GetOkPayTimeoutSeconds() int { return GetSettingInt(mdb.SettingKeyOkPayTimeoutSeconds, 10) } func GetOkPayAllowTokens() []string { raw := GetSettingString(mdb.SettingKeyOkPayAllowTokens, "USDT,TRX") if strings.TrimSpace(raw) == "" { return []string{"USDT", "TRX"} } parts := strings.Split(raw, ",") out := make([]string, 0, len(parts)) for _, part := range parts { token := strings.ToUpper(strings.TrimSpace(part)) if token == "" { continue } out = append(out, token) } if len(out) == 0 { return []string{"USDT", "TRX"} } return out } // ListSettingsByGroup returns all rows for a given group (empty group = all), // excluding any keys in sensitiveSettingKeys. func ListSettingsByGroup(group string) ([]mdb.Setting, error) { var rows []mdb.Setting tx := dao.Mdb.Model(&mdb.Setting{}).Not("`key`", sensitiveSettingKeys) if group != "" { tx = tx.Where("`group` = ?", group) } err := tx.Order("`key` ASC").Find(&rows).Error return rows, err }