package billing import ( "context" "encoding/json" "errors" "regexp" "strings" "time" "data-terminal-cloud/internal/audit" "data-terminal-cloud/internal/idempotency" "data-terminal-cloud/internal/identity" "github.com/google/uuid" "github.com/jackc/pgx/v5" "github.com/jackc/pgx/v5/pgxpool" ) var ( ErrInvalid = errors.New("billing input is invalid") ErrAccessDenied = errors.New("billing operations access denied") ErrNotFound = errors.New("billing resource not found") ErrStateConflict = errors.New("billing resource state conflict") ErrVersionConflict = errors.New("billing resource version conflict") ErrSelfApproval = errors.New("billing price creator cannot publish own version") ) var codePattern = regexp.MustCompile(`^[a-z][a-z0-9_.-]{1,79}$`) type RateInput struct { UsageCategory string `json:"usage_category"` MetricCode string `json:"metric_code"` Unit string `json:"unit"` UnitPriceMicros int64 `json:"unit_price_micros"` } type CreatePriceBookInput struct { TenantID string `json:"-"` Provider string `json:"provider"` Currency string `json:"currency"` SourceRef string `json:"source_ref"` ChangeSummary string `json:"change_summary"` EffectiveFrom time.Time `json:"effective_from"` Rates []RateInput `json:"rates"` } type PriceRate struct { ID string `json:"id"` UsageCategory string `json:"usage_category"` MetricCode string `json:"metric_code"` Unit string `json:"unit"` UnitPriceMicros int64 `json:"unit_price_micros"` } type PriceBook struct { ID string `json:"id"` TenantID string `json:"tenant_id"` Provider string `json:"provider"` Currency string `json:"currency"` VersionNo int `json:"version_no"` Status string `json:"status"` SourceRef string `json:"source_ref"` ChangeSummary string `json:"change_summary"` EffectiveFrom time.Time `json:"effective_from"` CreatedBy string `json:"created_by"` PublishedBy *string `json:"published_by,omitempty"` PublishedAt *time.Time `json:"published_at,omitempty"` RetiredAt *time.Time `json:"retired_at,omitempty"` RowVersion int64 `json:"row_version"` Rates []PriceRate `json:"rates"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type QuotaInput struct { ScopeType string `json:"scope_type"` ScopeID string `json:"scope_id,omitempty"` BillingTimezone string `json:"billing_timezone"` Currency string `json:"currency"` DailyLimitMicros int64 `json:"daily_limit_micros"` MonthlyLimitMicros int64 `json:"monthly_limit_micros"` MaxConcurrentCalls int `json:"max_concurrent_calls"` MaxCallDurationSec int `json:"max_call_duration_sec"` InProgressAction string `json:"in_progress_action"` ChangeSummary string `json:"change_summary"` } type QuotaPolicy struct { ID string `json:"id"` TenantID string `json:"tenant_id"` ScopeType string `json:"scope_type"` ScopeID *string `json:"scope_id,omitempty"` VersionNo int `json:"version_no"` Status string `json:"status"` BillingTimezone string `json:"billing_timezone"` Currency string `json:"currency"` DailyLimitMicros int64 `json:"daily_limit_micros"` MonthlyLimitMicros int64 `json:"monthly_limit_micros"` MaxConcurrentCalls int `json:"max_concurrent_calls"` MaxCallDurationSec int `json:"max_call_duration_sec"` InProgressAction string `json:"in_progress_action"` ChangeSummary string `json:"change_summary"` CreatedBy string `json:"created_by"` RetiredAt *time.Time `json:"retired_at,omitempty"` RowVersion int64 `json:"row_version"` CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } type actor struct { tenantID, tenantPublicID, userID, userPublicID, role string } type Service struct { pool *pgxpool.Pool audit *audit.Repository idem *idempotency.Executor } func NewService(pool *pgxpool.Pool, auditRepository *audit.Repository) (*Service, error) { if pool == nil || auditRepository == nil { return nil, errors.New("billing service dependencies are required") } return &Service{pool: pool, audit: auditRepository, idem: idempotency.NewExecutor(pool)}, nil } func (s *Service) CreatePriceBook(ctx context.Context, principal identity.Principal, key, requestID string, input CreatePriceBookInput) (PriceBook, bool, error) { normalizePriceInput(&input) if err := validatePriceInput(input); err != nil { return PriceBook{}, false, err } currentActor, err := s.resolveActor(ctx, principal, input.TenantID, "ops_operator", "ops_approver", "ops_admin") if err != nil { return PriceBook{}, false, err } requestBody, _ := json.Marshal(input) result, err := s.idem.Execute(ctx, idempotency.Request{TenantID: ¤tActor.tenantID, ActorKey: "ops:" + currentActor.userPublicID, Scope: "POST /api/v1/ops/price-books", Key: key, RequestHash: idempotency.Hash(requestBody), ExpiresAfter: 24 * time.Hour}, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) { if _, queryErr := tx.Exec(operationContext, `SELECT pg_advisory_xact_lock(hashtextextended($1,0))`, currentActor.tenantID+":"+input.Provider); queryErr != nil { return idempotency.Result{}, queryErr } var versionNo int if queryErr := tx.QueryRow(operationContext, `SELECT COALESCE(MAX(version_no),0)+1 FROM billing_price_book WHERE tenant_id=$1::uuid AND provider=$2`, currentActor.tenantID, input.Provider).Scan(&versionNo); queryErr != nil { return idempotency.Result{}, queryErr } publicID := uuid.NewString() var internalID string if queryErr := tx.QueryRow(operationContext, `INSERT INTO billing_price_book(public_id,tenant_id,provider,currency,version_no,source_ref,change_summary,effective_from,created_by) VALUES($1::uuid,$2::uuid,$3,$4,$5,$6,$7,$8,$9::uuid) RETURNING id::text`, publicID, currentActor.tenantID, input.Provider, input.Currency, versionNo, input.SourceRef, input.ChangeSummary, input.EffectiveFrom.UTC(), currentActor.userID).Scan(&internalID); queryErr != nil { return idempotency.Result{}, queryErr } for _, rate := range input.Rates { if _, queryErr := tx.Exec(operationContext, `INSERT INTO billing_price_rate(price_book_id,usage_category,metric_code,unit,unit_price_micros) VALUES($1::uuid,$2,$3,$4,$5)`, internalID, rate.UsageCategory, rate.MetricCode, rate.Unit, rate.UnitPriceMicros); queryErr != nil { return idempotency.Result{}, queryErr } } if queryErr := s.audit.Record(operationContext, tx, audit.Event{TenantID: ¤tActor.tenantID, ActorType: "user", ActorPublicID: ¤tActor.userPublicID, Action: "billing.price_book.create", ResourceType: "billing_price_book", ResourceID: &internalID, RequestID: requestID, Purpose: input.ChangeSummary, Result: "success", Detail: map[string]any{"provider": input.Provider, "currency": input.Currency, "version_no": versionNo, "rate_count": len(input.Rates)}}); queryErr != nil { return idempotency.Result{}, queryErr } item, queryErr := getPriceBookTx(operationContext, tx, currentActor.tenantID, publicID) if queryErr != nil { return idempotency.Result{}, queryErr } body, _ := json.Marshal(item) return idempotency.Result{StatusCode: 201, Body: body, ResourceID: &internalID}, nil }) if err != nil { return PriceBook{}, false, err } var item PriceBook if err = json.Unmarshal(result.Body, &item); err != nil { return PriceBook{}, false, err } return item, !result.Replayed, nil } func (s *Service) PublishPriceBook(ctx context.Context, principal identity.Principal, tenantPublicID, publicID, requestID, reason string, expectedVersion int64) (PriceBook, error) { reason = strings.TrimSpace(reason) if reason == "" || len([]rune(reason)) > 500 { return PriceBook{}, ErrInvalid } currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_approver", "ops_admin") if err != nil { return PriceBook{}, err } tx, err := s.pool.BeginTx(ctx, pgx.TxOptions{}) if err != nil { return PriceBook{}, err } defer func() { _ = tx.Rollback(context.Background()) }() var internalID, status, creatorID, provider, currency string var rowVersion int64 var effectiveFrom time.Time err = tx.QueryRow(ctx, `SELECT id::text,status,created_by::text,provider,currency,row_version,effective_from FROM billing_price_book WHERE tenant_id=$1::uuid AND public_id=$2::uuid FOR UPDATE`, currentActor.tenantID, publicID).Scan(&internalID, &status, &creatorID, &provider, ¤cy, &rowVersion, &effectiveFrom) if errors.Is(err, pgx.ErrNoRows) { return PriceBook{}, ErrNotFound } if err != nil { return PriceBook{}, err } if rowVersion != expectedVersion { return PriceBook{}, ErrVersionConflict } if status != "DRAFT" { return PriceBook{}, ErrStateConflict } if creatorID == currentActor.userID { return PriceBook{}, ErrSelfApproval } if effectiveFrom.After(time.Now().UTC().Add(5 * time.Minute)) { return PriceBook{}, ErrStateConflict } var incompatibleCurrency bool if err = tx.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM billing_price_book WHERE tenant_id=$1::uuid AND provider<>$2 AND status='PUBLISHED' AND currency<>$3) OR EXISTS(SELECT 1 FROM usage_quota_policy WHERE tenant_id=$1::uuid AND status='ACTIVE' AND currency<>$3)`, currentActor.tenantID, provider, currency).Scan(&incompatibleCurrency); err != nil { return PriceBook{}, err } if incompatibleCurrency { return PriceBook{}, ErrStateConflict } if _, err = tx.Exec(ctx, `SELECT pg_advisory_xact_lock(hashtextextended($1,0))`, currentActor.tenantID+":"+provider); err != nil { return PriceBook{}, err } if _, err = tx.Exec(ctx, `UPDATE billing_price_book SET status='RETIRED',retired_at=now(),row_version=row_version+1,updated_at=now() WHERE tenant_id=$1::uuid AND provider=$2 AND status='PUBLISHED'`, currentActor.tenantID, provider); err != nil { return PriceBook{}, err } if _, err = tx.Exec(ctx, `UPDATE billing_price_book SET status='PUBLISHED',published_by=$2::uuid,published_at=now(),row_version=row_version+1,updated_at=now() WHERE id=$1::uuid`, internalID, currentActor.userID); err != nil { return PriceBook{}, err } if err = s.audit.Record(ctx, tx, audit.Event{TenantID: ¤tActor.tenantID, ActorType: "user", ActorPublicID: ¤tActor.userPublicID, Action: "billing.price_book.publish", ResourceType: "billing_price_book", ResourceID: &internalID, RequestID: requestID, Purpose: reason, Result: "success", Detail: map[string]any{"provider": provider, "currency": currency}}); err != nil { return PriceBook{}, err } item, err := getPriceBookTx(ctx, tx, currentActor.tenantID, publicID) if err == nil { err = tx.Commit(ctx) } return item, err } func (s *Service) ListPriceBooks(ctx context.Context, principal identity.Principal, tenantPublicID string) ([]PriceBook, error) { currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_viewer", "ops_operator", "ops_approver", "ops_admin") if err != nil { return nil, err } rows, err := s.pool.Query(ctx, `SELECT public_id::text FROM billing_price_book WHERE tenant_id=$1::uuid ORDER BY provider,currency,version_no DESC LIMIT 500`, currentActor.tenantID) if err != nil { return nil, err } defer rows.Close() ids := []string{} for rows.Next() { var id string if err = rows.Scan(&id); err != nil { return nil, err } ids = append(ids, id) } if err = rows.Err(); err != nil { return nil, err } items := make([]PriceBook, 0, len(ids)) for _, id := range ids { item, queryErr := getPriceBookPool(ctx, s.pool, currentActor.tenantID, id) if queryErr != nil { return nil, queryErr } items = append(items, item) } return items, nil } func (s *Service) CreateQuotaPolicy(ctx context.Context, principal identity.Principal, tenantPublicID, key, requestID string, input QuotaInput) (QuotaPolicy, bool, error) { normalizeQuotaInput(&input) if err := validateQuotaInput(input); err != nil { return QuotaPolicy{}, false, err } currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_approver", "ops_admin") if err != nil { return QuotaPolicy{}, false, err } requestBody, _ := json.Marshal(input) result, err := s.idem.Execute(ctx, idempotency.Request{TenantID: ¤tActor.tenantID, ActorKey: "ops:" + currentActor.userPublicID, Scope: "POST /api/v1/ops/quota-policies", Key: key, RequestHash: idempotency.Hash(requestBody), ExpiresAfter: 24 * time.Hour}, func(operationContext context.Context, tx pgx.Tx) (idempotency.Result, error) { var incompatiblePriceCurrency bool if queryErr := tx.QueryRow(operationContext, `SELECT EXISTS(SELECT 1 FROM billing_price_book WHERE tenant_id=$1::uuid AND status='PUBLISHED' AND currency<>$2)`, currentActor.tenantID, input.Currency).Scan(&incompatiblePriceCurrency); queryErr != nil { return idempotency.Result{}, queryErr } if incompatiblePriceCurrency { return idempotency.Result{}, ErrInvalid } var deviceID *string if input.ScopeType == "DEVICE" { var id string queryErr := tx.QueryRow(operationContext, `SELECT id::text FROM device WHERE tenant_id=$1::uuid AND public_id=$2::uuid AND status='registered'`, currentActor.tenantID, input.ScopeID).Scan(&id) if errors.Is(queryErr, pgx.ErrNoRows) { return idempotency.Result{}, ErrAccessDenied } if queryErr != nil { return idempotency.Result{}, queryErr } deviceID = &id var tenantTimezone, tenantCurrency string queryErr = tx.QueryRow(operationContext, `SELECT billing_timezone,currency FROM usage_quota_policy WHERE tenant_id=$1::uuid AND scope_type='TENANT' AND status='ACTIVE'`, currentActor.tenantID).Scan(&tenantTimezone, &tenantCurrency) if queryErr != nil && !errors.Is(queryErr, pgx.ErrNoRows) { return idempotency.Result{}, queryErr } if queryErr == nil && (tenantTimezone != input.BillingTimezone || tenantCurrency != input.Currency) { return idempotency.Result{}, ErrInvalid } } else { var incompatibleDevicePolicy bool queryErr := tx.QueryRow(operationContext, `SELECT EXISTS(SELECT 1 FROM usage_quota_policy WHERE tenant_id=$1::uuid AND scope_type='DEVICE' AND status='ACTIVE' AND (billing_timezone<>$2 OR currency<>$3))`, currentActor.tenantID, input.BillingTimezone, input.Currency).Scan(&incompatibleDevicePolicy) if queryErr != nil { return idempotency.Result{}, queryErr } if incompatibleDevicePolicy { return idempotency.Result{}, ErrInvalid } } lockKey := currentActor.tenantID + ":quota:" + input.ScopeType + ":" + input.ScopeID if _, queryErr := tx.Exec(operationContext, `SELECT pg_advisory_xact_lock(hashtextextended($1,0))`, lockKey); queryErr != nil { return idempotency.Result{}, queryErr } var versionNo int if queryErr := tx.QueryRow(operationContext, `SELECT COALESCE(MAX(version_no),0)+1 FROM usage_quota_policy WHERE tenant_id=$1::uuid AND scope_type=$2 AND (($2='TENANT') OR device_id=$3::uuid)`, currentActor.tenantID, input.ScopeType, deviceID).Scan(&versionNo); queryErr != nil { return idempotency.Result{}, queryErr } if _, queryErr := tx.Exec(operationContext, `UPDATE usage_quota_policy SET status='RETIRED',retired_at=now(),row_version=row_version+1,updated_at=now() WHERE tenant_id=$1::uuid AND scope_type=$2 AND status='ACTIVE' AND (($2='TENANT') OR device_id=$3::uuid)`, currentActor.tenantID, input.ScopeType, deviceID); queryErr != nil { return idempotency.Result{}, queryErr } publicID := uuid.NewString() var internalID string if queryErr := tx.QueryRow(operationContext, `INSERT INTO usage_quota_policy(public_id,tenant_id,scope_type,device_id,version_no,billing_timezone,currency,daily_limit_micros,monthly_limit_micros,max_concurrent_calls,max_call_duration_sec,in_progress_action,change_summary,created_by) VALUES($1::uuid,$2::uuid,$3,$4::uuid,$5,$6,$7,$8,$9,$10,$11,$12,$13,$14::uuid) RETURNING id::text`, publicID, currentActor.tenantID, input.ScopeType, deviceID, versionNo, input.BillingTimezone, input.Currency, input.DailyLimitMicros, input.MonthlyLimitMicros, input.MaxConcurrentCalls, input.MaxCallDurationSec, input.InProgressAction, input.ChangeSummary, currentActor.userID).Scan(&internalID); queryErr != nil { return idempotency.Result{}, queryErr } if queryErr := s.audit.Record(operationContext, tx, audit.Event{TenantID: ¤tActor.tenantID, ActorType: "user", ActorPublicID: ¤tActor.userPublicID, Action: "billing.quota_policy.activate", ResourceType: "usage_quota_policy", ResourceID: &internalID, RequestID: requestID, Purpose: input.ChangeSummary, Result: "success", Detail: map[string]any{"scope_type": input.ScopeType, "scope_id": input.ScopeID, "version_no": versionNo}}); queryErr != nil { return idempotency.Result{}, queryErr } item, queryErr := getQuotaTx(operationContext, tx, currentActor.tenantID, publicID) if queryErr != nil { return idempotency.Result{}, queryErr } body, _ := json.Marshal(item) return idempotency.Result{StatusCode: 201, Body: body, ResourceID: &internalID}, nil }) if err != nil { return QuotaPolicy{}, false, err } var item QuotaPolicy if err = json.Unmarshal(result.Body, &item); err != nil { return QuotaPolicy{}, false, err } return item, !result.Replayed, nil } func (s *Service) ListQuotaPolicies(ctx context.Context, principal identity.Principal, tenantPublicID string) ([]QuotaPolicy, error) { currentActor, err := s.resolveActor(ctx, principal, tenantPublicID, "ops_viewer", "ops_operator", "ops_approver", "ops_admin") if err != nil { return nil, err } rows, err := s.pool.Query(ctx, quotaSelect+` WHERE qp.tenant_id=$1::uuid ORDER BY CASE qp.status WHEN 'ACTIVE' THEN 1 ELSE 2 END,qp.scope_type,qp.version_no DESC LIMIT 1000`, currentActor.tenantID) if err != nil { return nil, err } defer rows.Close() items := []QuotaPolicy{} for rows.Next() { item, scanErr := scanQuota(rows) if scanErr != nil { return nil, scanErr } items = append(items, item) } return items, rows.Err() } func (s *Service) resolveActor(ctx context.Context, principal identity.Principal, tenantPublicID string, roles ...string) (actor, error) { if _, err := uuid.Parse(tenantPublicID); err != nil { return actor{}, ErrInvalid } var result actor err := s.pool.QueryRow(ctx, `SELECT t.id::text,t.public_id::text,ua.id::text,ua.public_id::text,ua.platform_role FROM user_account ua JOIN tenant t ON t.id=ua.tenant_id AND t.status='active' WHERE t.public_id=$1::uuid AND ua.oidc_issuer=$2 AND ua.oidc_subject=$3 AND ua.status='active'`, tenantPublicID, principal.Issuer, principal.Subject).Scan(&result.tenantID, &result.tenantPublicID, &result.userID, &result.userPublicID, &result.role) if errors.Is(err, pgx.ErrNoRows) { return actor{}, ErrAccessDenied } if err != nil { return actor{}, err } for _, role := range roles { if result.role == role { return result, nil } } return actor{}, ErrAccessDenied } func normalizePriceInput(input *CreatePriceBookInput) { input.Provider = strings.ToLower(strings.TrimSpace(input.Provider)) input.Currency = strings.ToUpper(strings.TrimSpace(input.Currency)) input.SourceRef = strings.TrimSpace(input.SourceRef) input.ChangeSummary = strings.TrimSpace(input.ChangeSummary) for index := range input.Rates { input.Rates[index].UsageCategory = strings.ToUpper(strings.TrimSpace(input.Rates[index].UsageCategory)) input.Rates[index].MetricCode = strings.ToLower(strings.TrimSpace(input.Rates[index].MetricCode)) input.Rates[index].Unit = strings.ToLower(strings.TrimSpace(input.Rates[index].Unit)) } } func validatePriceInput(input CreatePriceBookInput) error { if _, err := uuid.Parse(input.TenantID); err != nil { return ErrInvalid } if !regexp.MustCompile(`^[a-z][a-z0-9_.-]{1,39}$`).MatchString(input.Provider) || !regexp.MustCompile(`^[A-Z]{3}$`).MatchString(input.Currency) || input.SourceRef == "" || len([]rune(input.SourceRef)) > 300 || input.ChangeSummary == "" || len([]rune(input.ChangeSummary)) > 500 || input.EffectiveFrom.IsZero() || len(input.Rates) == 0 || len(input.Rates) > 100 { return ErrInvalid } seen := map[string]struct{}{} for _, rate := range input.Rates { if !validCategory(rate.UsageCategory) || !codePattern.MatchString(rate.MetricCode) || !regexp.MustCompile(`^[a-z][a-z0-9_.-]{1,39}$`).MatchString(rate.Unit) || rate.UnitPriceMicros < 0 { return ErrInvalid } key := rate.UsageCategory + ":" + rate.MetricCode + ":" + rate.Unit if _, exists := seen[key]; exists { return ErrInvalid } seen[key] = struct{}{} } return nil } func normalizeQuotaInput(input *QuotaInput) { input.ScopeType = strings.ToUpper(strings.TrimSpace(input.ScopeType)) input.BillingTimezone = strings.TrimSpace(input.BillingTimezone) input.Currency = strings.ToUpper(strings.TrimSpace(input.Currency)) input.InProgressAction = strings.ToUpper(strings.TrimSpace(input.InProgressAction)) input.ChangeSummary = strings.TrimSpace(input.ChangeSummary) } func validateQuotaInput(input QuotaInput) error { if input.ScopeType != "TENANT" && input.ScopeType != "DEVICE" { return ErrInvalid } if input.ScopeType == "TENANT" && strings.TrimSpace(input.ScopeID) != "" { return ErrInvalid } if input.ScopeType == "DEVICE" { if _, err := uuid.Parse(input.ScopeID); err != nil { return ErrInvalid } } if _, err := time.LoadLocation(input.BillingTimezone); err != nil { return ErrInvalid } if !regexp.MustCompile(`^[A-Z]{3}$`).MatchString(input.Currency) || input.DailyLimitMicros <= 0 || input.MonthlyLimitMicros < input.DailyLimitMicros || input.MaxConcurrentCalls < 1 || input.MaxConcurrentCalls > 100000 || input.MaxCallDurationSec < 60 || input.MaxCallDurationSec > 86400 || (input.InProgressAction != "STOP_IMMEDIATELY" && input.InProgressAction != "COMPLETE_CURRENT_TURN") || input.ChangeSummary == "" || len([]rune(input.ChangeSummary)) > 500 { return ErrInvalid } return nil } func validCategory(value string) bool { switch value { case "RTC", "END_TO_END_VOICE", "ASR", "LLM", "TTS", "STORAGE", "POST_PROCESSING": return true } return false } type rowScanner interface{ Scan(...any) error } const priceBookSelect = `SELECT pb.public_id::text,t.public_id::text,pb.provider,pb.currency,pb.version_no,pb.status,pb.source_ref,pb.change_summary,pb.effective_from,creator.public_id::text,publisher.public_id::text,pb.published_at,pb.retired_at,pb.row_version,pb.created_at,pb.updated_at FROM billing_price_book pb JOIN tenant t ON t.id=pb.tenant_id JOIN user_account creator ON creator.id=pb.created_by LEFT JOIN user_account publisher ON publisher.id=pb.published_by` func scanPriceBook(row rowScanner) (PriceBook, error) { var item PriceBook err := row.Scan(&item.ID, &item.TenantID, &item.Provider, &item.Currency, &item.VersionNo, &item.Status, &item.SourceRef, &item.ChangeSummary, &item.EffectiveFrom, &item.CreatedBy, &item.PublishedBy, &item.PublishedAt, &item.RetiredAt, &item.RowVersion, &item.CreatedAt, &item.UpdatedAt) return item, err } func getPriceBookTx(ctx context.Context, tx pgx.Tx, tenantID, publicID string) (PriceBook, error) { item, err := scanPriceBook(tx.QueryRow(ctx, priceBookSelect+` WHERE pb.tenant_id=$1::uuid AND pb.public_id=$2::uuid`, tenantID, publicID)) if err != nil { return PriceBook{}, err } rows, err := tx.Query(ctx, `SELECT id::text,usage_category,metric_code,unit,unit_price_micros FROM billing_price_rate WHERE price_book_id=(SELECT id FROM billing_price_book WHERE tenant_id=$1::uuid AND public_id=$2::uuid) ORDER BY usage_category,metric_code,unit`, tenantID, publicID) if err != nil { return PriceBook{}, err } defer rows.Close() item.Rates = []PriceRate{} for rows.Next() { var rate PriceRate if err = rows.Scan(&rate.ID, &rate.UsageCategory, &rate.MetricCode, &rate.Unit, &rate.UnitPriceMicros); err != nil { return PriceBook{}, err } item.Rates = append(item.Rates, rate) } return item, rows.Err() } func getPriceBookPool(ctx context.Context, pool *pgxpool.Pool, tenantID, publicID string) (PriceBook, error) { item, err := scanPriceBook(pool.QueryRow(ctx, priceBookSelect+` WHERE pb.tenant_id=$1::uuid AND pb.public_id=$2::uuid`, tenantID, publicID)) if err != nil { return PriceBook{}, err } rows, err := pool.Query(ctx, `SELECT id::text,usage_category,metric_code,unit,unit_price_micros FROM billing_price_rate WHERE price_book_id=(SELECT id FROM billing_price_book WHERE tenant_id=$1::uuid AND public_id=$2::uuid) ORDER BY usage_category,metric_code,unit`, tenantID, publicID) if err != nil { return PriceBook{}, err } defer rows.Close() item.Rates = []PriceRate{} for rows.Next() { var rate PriceRate if err = rows.Scan(&rate.ID, &rate.UsageCategory, &rate.MetricCode, &rate.Unit, &rate.UnitPriceMicros); err != nil { return PriceBook{}, err } item.Rates = append(item.Rates, rate) } return item, rows.Err() } const quotaSelect = `SELECT qp.public_id::text,t.public_id::text,qp.scope_type,d.public_id::text,qp.version_no,qp.status,qp.billing_timezone,qp.currency,qp.daily_limit_micros,qp.monthly_limit_micros,qp.max_concurrent_calls,qp.max_call_duration_sec,qp.in_progress_action,qp.change_summary,ua.public_id::text,qp.retired_at,qp.row_version,qp.created_at,qp.updated_at FROM usage_quota_policy qp JOIN tenant t ON t.id=qp.tenant_id JOIN user_account ua ON ua.id=qp.created_by LEFT JOIN device d ON d.id=qp.device_id` func scanQuota(row rowScanner) (QuotaPolicy, error) { var item QuotaPolicy err := row.Scan(&item.ID, &item.TenantID, &item.ScopeType, &item.ScopeID, &item.VersionNo, &item.Status, &item.BillingTimezone, &item.Currency, &item.DailyLimitMicros, &item.MonthlyLimitMicros, &item.MaxConcurrentCalls, &item.MaxCallDurationSec, &item.InProgressAction, &item.ChangeSummary, &item.CreatedBy, &item.RetiredAt, &item.RowVersion, &item.CreatedAt, &item.UpdatedAt) return item, err } func getQuotaTx(ctx context.Context, tx pgx.Tx, tenantID, publicID string) (QuotaPolicy, error) { item, err := scanQuota(tx.QueryRow(ctx, quotaSelect+` WHERE qp.tenant_id=$1::uuid AND qp.public_id=$2::uuid`, tenantID, publicID)) return item, err }