package localdb import ( "encoding/json" "fmt" "sort" "time" ) // BuildConfigurationSnapshot serializes the full local configuration state. func BuildConfigurationSnapshot(localCfg *LocalConfiguration) (string, error) { snapshot := map[string]interface{}{ "id": localCfg.ID, "uuid": localCfg.UUID, "server_id": localCfg.ServerID, "project_uuid": localCfg.ProjectUUID, "current_version_id": localCfg.CurrentVersionID, "is_active": localCfg.IsActive, "name": localCfg.Name, "items": localCfg.Items, "total_price": localCfg.TotalPrice, "custom_price": localCfg.CustomPrice, "notes": localCfg.Notes, "is_template": localCfg.IsTemplate, "server_count": localCfg.ServerCount, "server_model": localCfg.ServerModel, "support_code": localCfg.SupportCode, "article": localCfg.Article, "pricelist_id": localCfg.PricelistID, "only_in_stock": localCfg.OnlyInStock, "line": localCfg.Line, "price_updated_at": localCfg.PriceUpdatedAt, "created_at": localCfg.CreatedAt, "updated_at": localCfg.UpdatedAt, "synced_at": localCfg.SyncedAt, "sync_status": localCfg.SyncStatus, "original_user_id": localCfg.OriginalUserID, "original_username": localCfg.OriginalUsername, } data, err := json.Marshal(snapshot) if err != nil { return "", fmt.Errorf("marshal configuration snapshot: %w", err) } return string(data), nil } // DecodeConfigurationSnapshot returns editable fields from one saved snapshot. func DecodeConfigurationSnapshot(data string) (*LocalConfiguration, error) { var snapshot struct { ProjectUUID *string `json:"project_uuid"` IsActive *bool `json:"is_active"` Name string `json:"name"` Items LocalConfigItems `json:"items"` TotalPrice *float64 `json:"total_price"` CustomPrice *float64 `json:"custom_price"` Notes string `json:"notes"` IsTemplate bool `json:"is_template"` ServerCount int `json:"server_count"` ServerModel string `json:"server_model"` SupportCode string `json:"support_code"` Article string `json:"article"` PricelistID *uint `json:"pricelist_id"` OnlyInStock bool `json:"only_in_stock"` Line int `json:"line"` PriceUpdatedAt *time.Time `json:"price_updated_at"` OriginalUserID uint `json:"original_user_id"` OriginalUsername string `json:"original_username"` } if err := json.Unmarshal([]byte(data), &snapshot); err != nil { return nil, fmt.Errorf("unmarshal snapshot JSON: %w", err) } isActive := true if snapshot.IsActive != nil { isActive = *snapshot.IsActive } return &LocalConfiguration{ IsActive: isActive, ProjectUUID: snapshot.ProjectUUID, Name: snapshot.Name, Items: snapshot.Items, TotalPrice: snapshot.TotalPrice, CustomPrice: snapshot.CustomPrice, Notes: snapshot.Notes, IsTemplate: snapshot.IsTemplate, ServerCount: snapshot.ServerCount, ServerModel: snapshot.ServerModel, SupportCode: snapshot.SupportCode, Article: snapshot.Article, PricelistID: snapshot.PricelistID, OnlyInStock: snapshot.OnlyInStock, Line: snapshot.Line, PriceUpdatedAt: snapshot.PriceUpdatedAt, OriginalUserID: snapshot.OriginalUserID, OriginalUsername: snapshot.OriginalUsername, }, nil } type configurationSpecPriceFingerprint struct { Items []configurationSpecPriceFingerprintItem `json:"items"` ServerCount int `json:"server_count"` TotalPrice *float64 `json:"total_price,omitempty"` CustomPrice *float64 `json:"custom_price,omitempty"` } type configurationSpecPriceFingerprintItem struct { LotName string `json:"lot_name"` Quantity int `json:"quantity"` UnitPrice float64 `json:"unit_price"` } // BuildConfigurationSpecPriceFingerprint returns a stable JSON key based on // spec + price fields only, used for revision deduplication. func BuildConfigurationSpecPriceFingerprint(localCfg *LocalConfiguration) (string, error) { items := make([]configurationSpecPriceFingerprintItem, 0, len(localCfg.Items)) for _, item := range localCfg.Items { items = append(items, configurationSpecPriceFingerprintItem{ LotName: item.LotName, Quantity: item.Quantity, UnitPrice: item.UnitPrice, }) } sort.Slice(items, func(i, j int) bool { if items[i].LotName != items[j].LotName { return items[i].LotName < items[j].LotName } if items[i].Quantity != items[j].Quantity { return items[i].Quantity < items[j].Quantity } return items[i].UnitPrice < items[j].UnitPrice }) payload := configurationSpecPriceFingerprint{ Items: items, ServerCount: localCfg.ServerCount, TotalPrice: localCfg.TotalPrice, CustomPrice: localCfg.CustomPrice, } raw, err := json.Marshal(payload) if err != nil { return "", fmt.Errorf("marshal spec+price fingerprint: %w", err) } return string(raw), nil }