package postgres import ( "context" "database/sql" "git.gobha.me/xcaliber/chat-switchboard/models" ) type PricingStore struct{} func NewPricingStore() *PricingStore { return &PricingStore{} } // GetForModel returns the pricing entry for a specific provider+model pair. func (s *PricingStore) GetForModel(ctx context.Context, providerConfigID, modelID string) (*models.PricingEntry, error) { var p models.PricingEntry err := DB.QueryRowContext(ctx, ` SELECT id, provider_config_id, model_id, input_per_m, output_per_m, cache_create_per_m, cache_read_per_m, currency, source, updated_at, updated_by FROM model_pricing WHERE provider_config_id = $1 AND model_id = $2 `, providerConfigID, modelID).Scan( &p.ID, &p.ProviderConfigID, &p.ModelID, &p.InputPerM, &p.OutputPerM, &p.CacheCreatePerM, &p.CacheReadPerM, &p.Currency, &p.Source, &p.UpdatedAt, &p.UpdatedBy, ) if err == sql.ErrNoRows { return nil, nil } return &p, err } // Upsert inserts or updates a pricing entry (manual admin override). func (s *PricingStore) Upsert(ctx context.Context, entry *models.PricingEntry) error { _, err := DB.ExecContext(ctx, ` INSERT INTO model_pricing ( provider_config_id, model_id, input_per_m, output_per_m, cache_create_per_m, cache_read_per_m, currency, source, updated_by, updated_at ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, NOW()) ON CONFLICT (provider_config_id, model_id) DO UPDATE SET input_per_m = EXCLUDED.input_per_m, output_per_m = EXCLUDED.output_per_m, cache_create_per_m = EXCLUDED.cache_create_per_m, cache_read_per_m = EXCLUDED.cache_read_per_m, currency = EXCLUDED.currency, source = EXCLUDED.source, updated_by = EXCLUDED.updated_by, updated_at = NOW() `, entry.ProviderConfigID, entry.ModelID, entry.InputPerM, entry.OutputPerM, entry.CacheCreatePerM, entry.CacheReadPerM, entry.Currency, entry.Source, entry.UpdatedBy, ) return err } // UpsertFromCatalog inserts or updates pricing from a catalog sync. // Manual overrides (source='manual') are never overwritten. func (s *PricingStore) UpsertFromCatalog(ctx context.Context, providerConfigID, modelID string, pricing *models.ModelPricing) error { if pricing == nil || (pricing.InputPerM == 0 && pricing.OutputPerM == 0) { return nil // No pricing to store } currency := pricing.Currency if currency == "" { currency = "USD" } _, err := DB.ExecContext(ctx, ` INSERT INTO model_pricing ( provider_config_id, model_id, input_per_m, output_per_m, currency, source, updated_at ) VALUES ($1, $2, $3, $4, $5, 'catalog', NOW()) ON CONFLICT (provider_config_id, model_id) DO UPDATE SET input_per_m = EXCLUDED.input_per_m, output_per_m = EXCLUDED.output_per_m, currency = EXCLUDED.currency, updated_at = NOW() WHERE model_pricing.source = 'catalog' `, providerConfigID, modelID, pricing.InputPerM, pricing.OutputPerM, currency, ) return err } // List returns pricing entries for admin-managed providers (global + team). // Excludes personal BYOK providers — those are not the admin's concern. func (s *PricingStore) List(ctx context.Context) ([]models.PricingEntry, error) { rows, err := DB.QueryContext(ctx, ` SELECT mp.id, mp.provider_config_id, mp.model_id, mp.input_per_m, mp.output_per_m, mp.cache_create_per_m, mp.cache_read_per_m, mp.currency, mp.source, mp.updated_at, mp.updated_by FROM model_pricing mp JOIN provider_configs pc ON pc.id = mp.provider_config_id WHERE pc.scope != 'personal' ORDER BY mp.provider_config_id, mp.model_id `) if err != nil { return nil, err } defer rows.Close() var results []models.PricingEntry for rows.Next() { var p models.PricingEntry if err := rows.Scan( &p.ID, &p.ProviderConfigID, &p.ModelID, &p.InputPerM, &p.OutputPerM, &p.CacheCreatePerM, &p.CacheReadPerM, &p.Currency, &p.Source, &p.UpdatedAt, &p.UpdatedBy, ); err != nil { return nil, err } results = append(results, p) } return results, rows.Err() } // Delete removes a pricing entry. func (s *PricingStore) Delete(ctx context.Context, providerConfigID, modelID string) error { _, err := DB.ExecContext(ctx, ` DELETE FROM model_pricing WHERE provider_config_id = $1 AND model_id = $2 `, providerConfigID, modelID) return err }