package store import ( "context" "encoding/json" "strconv" "strings" "github.com/jackc/pgx/v5" ) const pricingRuleSetColumns = ` id::text, rule_set_key, name, COALESCE(description, ''), category, currency, status, metadata, created_at, updated_at` const pricingRuleColumns = ` id::text, COALESCE(rule_set_id::text, ''), rule_key, display_name, scope_type, COALESCE(scope_id::text, ''), resource_type, unit, base_price::float8, currency, base_weight, dynamic_weight, calculator_type, dimension_schema, formula_config, priority, status, metadata, created_at, updated_at` type PricingRuleInput struct { RuleKey string `json:"ruleKey"` DisplayName string `json:"displayName"` ResourceType string `json:"resourceType"` Unit string `json:"unit"` BasePrice float64 `json:"basePrice"` Currency string `json:"currency"` BaseWeight map[string]any `json:"baseWeight"` DynamicWeight map[string]any `json:"dynamicWeight"` CalculatorType string `json:"calculatorType"` DimensionSchema map[string]any `json:"dimensionSchema"` FormulaConfig map[string]any `json:"formulaConfig"` Priority int `json:"priority"` Status string `json:"status"` Metadata map[string]any `json:"metadata"` } type PricingRuleSetInput struct { RuleSetKey string `json:"ruleSetKey"` Name string `json:"name"` Description string `json:"description"` Category string `json:"category"` Currency string `json:"currency"` Status string `json:"status"` Metadata map[string]any `json:"metadata"` Rules []PricingRuleInput `json:"rules"` } type pricingScanner interface { Scan(dest ...any) error } func (s *Store) ListPricingRuleSets(ctx context.Context) ([]PricingRuleSet, error) { rows, err := s.pool.Query(ctx, `SELECT `+pricingRuleSetColumns+` FROM model_pricing_rule_sets ORDER BY category ASC, name ASC`) if err != nil { return nil, err } defer rows.Close() items := make([]PricingRuleSet, 0) byID := map[string]int{} for rows.Next() { item, err := scanPricingRuleSet(rows) if err != nil { return nil, err } byID[item.ID] = len(items) items = append(items, item) } if err := rows.Err(); err != nil { return nil, err } ruleRows, err := s.pool.Query(ctx, ` SELECT `+pricingRuleColumns+` FROM model_pricing_rules WHERE rule_set_id IS NOT NULL ORDER BY rule_set_id, priority ASC, resource_type ASC, rule_key ASC`) if err != nil { return nil, err } defer ruleRows.Close() for ruleRows.Next() { rule, err := scanPricingRule(ruleRows) if err != nil { return nil, err } if index, ok := byID[rule.RuleSetID]; ok { items[index].Rules = append(items[index].Rules, rule) } } return items, ruleRows.Err() } func (s *Store) CreatePricingRuleSet(ctx context.Context, input PricingRuleSetInput) (PricingRuleSet, error) { input = normalizePricingRuleSet(input) tx, err := s.pool.Begin(ctx) if err != nil { return PricingRuleSet{}, err } defer tx.Rollback(ctx) metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata)) item, err := scanPricingRuleSet(tx.QueryRow(ctx, ` INSERT INTO model_pricing_rule_sets (rule_set_key, name, description, category, currency, status, metadata) VALUES ($1, $2, NULLIF($3, ''), $4, $5, $6, $7) RETURNING `+pricingRuleSetColumns, input.RuleSetKey, input.Name, input.Description, input.Category, input.Currency, input.Status, metadata, )) if err != nil { return PricingRuleSet{}, err } if err := insertPricingRules(ctx, tx, item.ID, input.Currency, input.Rules); err != nil { return PricingRuleSet{}, err } if err := tx.Commit(ctx); err != nil { return PricingRuleSet{}, err } item.Rules = pricingInputsToRules(item.ID, input.Currency, input.Rules) return item, nil } func (s *Store) UpdatePricingRuleSet(ctx context.Context, id string, input PricingRuleSetInput) (PricingRuleSet, error) { input = normalizePricingRuleSet(input) tx, err := s.pool.Begin(ctx) if err != nil { return PricingRuleSet{}, err } defer tx.Rollback(ctx) metadata, _ := json.Marshal(emptyObjectIfNil(input.Metadata)) item, err := scanPricingRuleSet(tx.QueryRow(ctx, ` UPDATE model_pricing_rule_sets SET rule_set_key = $2, name = $3, description = NULLIF($4, ''), category = $5, currency = $6, status = $7, metadata = $8, updated_at = now() WHERE id = $1::uuid RETURNING `+pricingRuleSetColumns, id, input.RuleSetKey, input.Name, input.Description, input.Category, input.Currency, input.Status, metadata, )) if err != nil { return PricingRuleSet{}, err } if _, err := tx.Exec(ctx, `DELETE FROM model_pricing_rules WHERE rule_set_id = $1::uuid`, id); err != nil { return PricingRuleSet{}, err } if err := insertPricingRules(ctx, tx, item.ID, input.Currency, input.Rules); err != nil { return PricingRuleSet{}, err } if err := tx.Commit(ctx); err != nil { return PricingRuleSet{}, err } item.Rules = pricingInputsToRules(item.ID, input.Currency, input.Rules) return item, nil } func (s *Store) DeletePricingRuleSet(ctx context.Context, id string) error { var ruleSetKey string if err := s.pool.QueryRow(ctx, `SELECT rule_set_key FROM model_pricing_rule_sets WHERE id = $1::uuid`, id).Scan(&ruleSetKey); err != nil { return err } if ruleSetKey == "default-multimodal-v1" { return ErrProtectedDefault } result, err := s.pool.Exec(ctx, `DELETE FROM model_pricing_rule_sets WHERE id = $1::uuid`, id) if err != nil { return err } if result.RowsAffected() == 0 { return pgx.ErrNoRows } return nil } func (s *Store) PricingRuleSetBillingConfig(ctx context.Context, id string) (map[string]any, error) { id = strings.TrimSpace(id) if id == "" { return nil, nil } rows, err := s.pool.Query(ctx, ` SELECT resource_type, base_price::float8, dynamic_weight, formula_config FROM model_pricing_rules WHERE rule_set_id = $1::uuid AND status = 'active' ORDER BY priority ASC, resource_type ASC`, id) if err != nil { return nil, err } defer rows.Close() config := map[string]any{} for rows.Next() { var resourceType string var basePrice float64 var dynamicWeightBytes []byte var formulaConfigBytes []byte if err := rows.Scan(&resourceType, &basePrice, &dynamicWeightBytes, &formulaConfigBytes); err != nil { return nil, err } dynamicWeight := decodeObject(dynamicWeightBytes) formulaConfig := decodeObject(formulaConfigBytes) switch resourceType { case "text_input": config["textInputPer1k"] = basePrice case "text_output": config["textOutputPer1k"] = basePrice case "text_total": inputPrice := basePrice if value, ok := pricingRuleNumberFromKeys(formulaConfig, "inputTokenPrice", "input_token_price", "textInputPer1k", "text_input"); ok { inputPrice = value } config["textInputPer1k"] = inputPrice if outputPrice, ok := pricingRuleNumberFromKeys(formulaConfig, "outputTokenPrice", "output_token_price", "textOutputPer1k", "text_output"); ok { config["textOutputPer1k"] = outputPrice } resourceConfig := pricingResourceConfig(basePrice, dynamicWeight) if len(formulaConfig) > 0 { resourceConfig["formulaConfig"] = formulaConfig } config["text_total"] = resourceConfig case "image": config["imageBase"] = basePrice config["image"] = pricingResourceConfig(basePrice, dynamicWeight) case "image_edit": config["editBase"] = basePrice config["image_edit"] = pricingResourceConfig(basePrice, dynamicWeight) case "video": config["videoBase"] = basePrice config["video"] = pricingResourceConfig(basePrice, dynamicWeight) default: config[resourceType] = pricingResourceConfig(basePrice, dynamicWeight) } } if err := rows.Err(); err != nil { return nil, err } return config, nil } func pricingRuleNumberFromKeys(config map[string]any, keys ...string) (float64, bool) { if len(config) == 0 { return 0, false } for _, key := range keys { if value, ok := pricingRuleNumberValue(config[key]); ok { return value, true } } return 0, false } func pricingRuleNumberValue(value any) (float64, bool) { switch typed := value.(type) { case float64: return typed, true case float32: return float64(typed), true case int: return float64(typed), true case int64: return float64(typed), true case int32: return float64(typed), true case json.Number: number, err := typed.Float64() return number, err == nil case string: trimmed := strings.TrimSpace(typed) if trimmed == "" { return 0, false } number, err := strconv.ParseFloat(trimmed, 64) return number, err == nil default: return 0, false } } func pricingResourceConfig(basePrice float64, dynamicWeight map[string]any) map[string]any { config := map[string]any{"basePrice": basePrice} if len(dynamicWeight) > 0 { config["dynamicWeight"] = dynamicWeight } return config } func insertPricingRules(ctx context.Context, tx pgx.Tx, ruleSetID string, defaultCurrency string, rules []PricingRuleInput) error { for index, rule := range rules { rule = normalizePricingRule(rule, index, defaultCurrency) baseWeight, _ := json.Marshal(emptyObjectIfNil(rule.BaseWeight)) dynamicWeight, _ := json.Marshal(emptyObjectIfNil(rule.DynamicWeight)) dimensionSchema, _ := json.Marshal(emptyObjectIfNil(rule.DimensionSchema)) formulaConfig, _ := json.Marshal(emptyObjectIfNil(rule.FormulaConfig)) metadata, _ := json.Marshal(emptyObjectIfNil(rule.Metadata)) if _, err := tx.Exec(ctx, ` INSERT INTO model_pricing_rules ( rule_set_id, rule_key, display_name, scope_type, scope_id, resource_type, unit, base_price, currency, base_weight, dynamic_weight, calculator_type, dimension_schema, formula_config, priority, status, metadata ) VALUES ($1::uuid, $2, $3, 'rule_set', $1::uuid, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15)`, ruleSetID, rule.RuleKey, rule.DisplayName, rule.ResourceType, rule.Unit, rule.BasePrice, rule.Currency, baseWeight, dynamicWeight, rule.CalculatorType, dimensionSchema, formulaConfig, rule.Priority, rule.Status, metadata, ); err != nil { return err } } return nil } func scanPricingRuleSet(scanner pricingScanner) (PricingRuleSet, error) { var item PricingRuleSet var metadata []byte if err := scanner.Scan( &item.ID, &item.RuleSetKey, &item.Name, &item.Description, &item.Category, &item.Currency, &item.Status, &metadata, &item.CreatedAt, &item.UpdatedAt, ); err != nil { return PricingRuleSet{}, err } item.Metadata = decodeObject(metadata) item.Rules = []PricingRule{} return item, nil } func scanPricingRule(scanner pricingScanner) (PricingRule, error) { var item PricingRule var baseWeight []byte var dynamicWeight []byte var dimensionSchema []byte var formulaConfig []byte var metadata []byte if err := scanner.Scan( &item.ID, &item.RuleSetID, &item.RuleKey, &item.DisplayName, &item.ScopeType, &item.ScopeID, &item.ResourceType, &item.Unit, &item.BasePrice, &item.Currency, &baseWeight, &dynamicWeight, &item.CalculatorType, &dimensionSchema, &formulaConfig, &item.Priority, &item.Status, &metadata, &item.CreatedAt, &item.UpdatedAt, ); err != nil { return PricingRule{}, err } item.BaseWeight = decodeObject(baseWeight) item.DynamicWeight = decodeObject(dynamicWeight) item.DimensionSchema = decodeObject(dimensionSchema) item.FormulaConfig = decodeObject(formulaConfig) item.Metadata = decodeObject(metadata) return item, nil } func normalizePricingRuleSet(input PricingRuleSetInput) PricingRuleSetInput { input.RuleSetKey = strings.TrimSpace(input.RuleSetKey) input.Name = strings.TrimSpace(input.Name) input.Description = strings.TrimSpace(input.Description) input.Category = strings.TrimSpace(input.Category) input.Currency = strings.TrimSpace(input.Currency) input.Status = strings.TrimSpace(input.Status) if input.Category == "" { input.Category = "custom" } if input.Currency == "" { input.Currency = "resource" } if input.Status == "" { input.Status = "active" } return input } func normalizePricingRule(input PricingRuleInput, index int, defaultCurrency string) PricingRuleInput { input.RuleKey = strings.TrimSpace(input.RuleKey) input.DisplayName = strings.TrimSpace(input.DisplayName) input.ResourceType = strings.TrimSpace(input.ResourceType) input.Unit = strings.TrimSpace(input.Unit) input.Currency = strings.TrimSpace(input.Currency) input.CalculatorType = strings.TrimSpace(input.CalculatorType) input.Status = strings.TrimSpace(input.Status) if input.RuleKey == "" { input.RuleKey = "rule_" + strings.ReplaceAll(input.ResourceType+"_"+input.Unit, " ", "_") } if input.DisplayName == "" { input.DisplayName = input.ResourceType } if input.Unit == "" { input.Unit = "item" } if input.Currency == "" { input.Currency = defaultCurrency } if input.CalculatorType == "" { input.CalculatorType = "unit_weight" } if input.Priority == 0 { input.Priority = (index + 1) * 10 } if input.Status == "" { input.Status = "active" } return input } func pricingInputsToRules(ruleSetID string, defaultCurrency string, rules []PricingRuleInput) []PricingRule { items := make([]PricingRule, 0, len(rules)) for index, input := range rules { input = normalizePricingRule(input, index, defaultCurrency) items = append(items, PricingRule{ RuleSetID: ruleSetID, RuleKey: input.RuleKey, DisplayName: input.DisplayName, ScopeType: "rule_set", ScopeID: ruleSetID, ResourceType: input.ResourceType, Unit: input.Unit, BasePrice: input.BasePrice, Currency: input.Currency, BaseWeight: emptyObjectIfNil(input.BaseWeight), DynamicWeight: emptyObjectIfNil(input.DynamicWeight), CalculatorType: input.CalculatorType, DimensionSchema: emptyObjectIfNil(input.DimensionSchema), FormulaConfig: emptyObjectIfNil(input.FormulaConfig), Priority: input.Priority, Status: input.Status, Metadata: emptyObjectIfNil(input.Metadata), }) } return items }