Files
QuoteForge/internal/localdb/qt_settings.go
T
Mikhail ChusavitinandClaude Sonnet 5 7edb80e498 feat: расчёт стоимости платного тестирования/аренды GPU-серверов
Новая вкладка «Аренда» в конфигураторе (доступна для проектов с флагом
rental_enabled): расчёт по методичке "Регламент расчёта стоимости
платного тестирования и аренды GPU-серверов" — Разовый + Еженедельный
платёж по каждому компоненту (New/БУ чекбоксом), с аплифтом к Estimate.

Заодно: поддержка (support_code) в Base-вкладке теперь добавляется как
обычный LOT в спеку (SVC_{срок}y{уровень}_{платформа}) через
autocomplete-пикер вместо выпадающего списка — партномер собирается
тем же lot_name-based механизмом, что и для GPU/CPU/памяти, справочная
цена по регламенту техподдержки хранится в qt_settings.support_pricing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-22 13:34:07 +03:00

145 lines
5.3 KiB
Go

package localdb
import (
"encoding/json"
"fmt"
"log/slog"
"gorm.io/gorm"
)
// ConfigTypeDef describes one device configuration type as synced from qt_settings.
type ConfigTypeDef struct {
Code string `json:"code"`
NameRu string `json:"name_ru"`
DisplayOrder int `json:"display_order"`
Categories []string `json:"categories"`
}
// TabSection is a named sub-group of categories within a configurator tab.
type TabSection struct {
Title string `json:"title"`
Categories []string `json:"categories"`
}
// TabDef describes one tab in the configurator as synced from qt_settings.
type TabDef struct {
Key string `json:"key"`
Label string `json:"label"`
SingleSelect bool `json:"single_select"`
Categories []string `json:"categories"`
Sections []TabSection `json:"sections,omitempty"`
}
// SupportPricingTable holds the price data from "Регламент расчёта стоимости
// технической поддержки серверов": x86 is a percent of sale price, HGX
// platforms (by chip generation) are a fixed multi-year price. Both are
// keyed level -> duration-in-years (as a string, since JSON object keys are
// strings) -> value. Missing combos are not offered by the regulation.
type SupportPricingTable struct {
X86Percent map[string]map[string]float64 `json:"x86_percent"`
HGXPrice map[string]map[string]map[string]float64 `json:"hgx_price"` // platform -> level -> years -> USD
}
// ConfiguratorSettings holds all server-driven settings consumed by the configurator.
// Fields are nil/empty when the corresponding qt_settings key is absent or unparseable;
// callers are expected to apply hardcoded fallbacks in that case.
type ConfiguratorSettings struct {
ConfigTypes []ConfigTypeDef `json:"config_types"`
TabConfig []TabDef `json:"tab_config"`
AlwaysVisibleTabs []string `json:"always_visible_tabs"`
RequiredCategories map[string][]string `json:"required_categories"`
SupportPricing *SupportPricingTable `json:"support_pricing,omitempty"`
}
// SyncQtSettings reads all rows from qt_settings on MariaDB and replaces the
// local_qt_settings cache in a single SQLite transaction.
// If the read fails (no connection, table missing on old server) or the server
// returns an empty table, the existing local_qt_settings are preserved so the
// configurator keeps working offline or against old server versions.
func (l *LocalDB) SyncQtSettings(mariaDB *gorm.DB) error {
var rows []LocalQtSetting
if err := mariaDB.
Table("qt_settings").
Select("name, value").
Find(&rows).Error; err != nil {
slog.Warn("qt_settings: read from MariaDB failed, keeping existing local cache", "error", err)
return fmt.Errorf("reading qt_settings from MariaDB: %w", err)
}
if len(rows) == 0 {
slog.Warn("qt_settings: server returned empty table, keeping existing local cache")
return nil
}
return l.db.Transaction(func(tx *gorm.DB) error {
if err := tx.Exec("DELETE FROM local_qt_settings").Error; err != nil {
return fmt.Errorf("clearing local_qt_settings: %w", err)
}
if err := tx.Create(&rows).Error; err != nil {
return fmt.Errorf("inserting local_qt_settings: %w", err)
}
slog.Info("qt_settings synced", "count", len(rows))
return nil
})
}
// GetQtSetting returns the raw JSON value for a named setting.
// found is false when the key does not exist.
func (l *LocalDB) GetQtSetting(name string) (value string, found bool, err error) {
var row LocalQtSetting
res := l.db.Where("name = ?", name).First(&row)
if res.Error != nil {
if res.Error == gorm.ErrRecordNotFound {
return "", false, nil
}
return "", false, res.Error
}
return row.Value, true, nil
}
// GetConfiguratorSettings reads all four known settings from local_qt_settings and
// parses them. Any missing or unparseable key is left as nil/zero in the result;
// the caller must apply fallbacks.
func (l *LocalDB) GetConfiguratorSettings() (*ConfiguratorSettings, error) {
out := &ConfiguratorSettings{}
keys := []string{"config_types", "tab_config", "always_visible_tabs", "required_categories", "support_pricing"}
for _, key := range keys {
raw, found, err := l.GetQtSetting(key)
if err != nil {
return out, fmt.Errorf("reading setting %q: %w", key, err)
}
if !found || raw == "" {
continue
}
switch key {
case "config_types":
if err := json.Unmarshal([]byte(raw), &out.ConfigTypes); err != nil {
slog.Warn("failed to parse config_types setting", "error", err)
}
case "tab_config":
if err := json.Unmarshal([]byte(raw), &out.TabConfig); err != nil {
slog.Warn("failed to parse tab_config setting", "error", err)
}
case "always_visible_tabs":
if err := json.Unmarshal([]byte(raw), &out.AlwaysVisibleTabs); err != nil {
slog.Warn("failed to parse always_visible_tabs setting", "error", err)
}
case "required_categories":
if err := json.Unmarshal([]byte(raw), &out.RequiredCategories); err != nil {
slog.Warn("failed to parse required_categories setting", "error", err)
}
case "support_pricing":
var table SupportPricingTable
if err := json.Unmarshal([]byte(raw), &table); err != nil {
slog.Warn("failed to parse support_pricing setting", "error", err)
} else {
out.SupportPricing = &table
}
}
}
return out, nil
}