package service import ( "context" "crypto/sha256" "encoding/hex" "errors" "fmt" "strings" ) var ErrUsageBillingRequestIDRequired = errors.New("usage billing request_id is required") var ErrUsageBillingRequestConflict = errors.New("usage billing request fingerprint conflict") // UsageBillingCommand describes one billable request that must be applied at most once. type UsageBillingCommand struct { RequestID string APIKeyID int64 RequestFingerprint string RequestPayloadHash string UserID int64 AccountID int64 SubscriptionID *int64 AccountType string Model string ServiceTier string ReasoningEffort string BillingType int8 InputTokens int OutputTokens int CacheCreationTokens int CacheReadTokens int ImageCount int MediaType string BalanceCost float64 SubscriptionCost float64 APIKeyQuotaCost float64 APIKeyRateLimitCost float64 AccountQuotaCost float64 } func (c *UsageBillingCommand) Normalize() { if c == nil { return } c.RequestID = strings.TrimSpace(c.RequestID) if strings.TrimSpace(c.RequestFingerprint) == "" { c.RequestFingerprint = buildUsageBillingFingerprint(c) } } func buildUsageBillingFingerprint(c *UsageBillingCommand) string { if c == nil { return "" } raw := fmt.Sprintf( "%d|%d|%d|%s|%s|%s|%s|%d|%d|%d|%d|%d|%d|%s|%d|%0.10f|%0.10f|%0.10f|%0.10f|%0.10f", c.UserID, c.AccountID, c.APIKeyID, strings.TrimSpace(c.AccountType), strings.TrimSpace(c.Model), strings.TrimSpace(c.ServiceTier), strings.TrimSpace(c.ReasoningEffort), c.BillingType, c.InputTokens, c.OutputTokens, c.CacheCreationTokens, c.CacheReadTokens, c.ImageCount, strings.TrimSpace(c.MediaType), valueOrZero(c.SubscriptionID), c.BalanceCost, c.SubscriptionCost, c.APIKeyQuotaCost, c.APIKeyRateLimitCost, c.AccountQuotaCost, ) if payloadHash := strings.TrimSpace(c.RequestPayloadHash); payloadHash != "" { raw += "|" + payloadHash } sum := sha256.Sum256([]byte(raw)) return hex.EncodeToString(sum[:]) } func HashUsageRequestPayload(payload []byte) string { if len(payload) == 0 { return "" } sum := sha256.Sum256(payload) return hex.EncodeToString(sum[:]) } func valueOrZero(v *int64) int64 { if v == nil { return 0 } return *v } type UsageBillingApplyResult struct { Applied bool APIKeyQuotaExhausted bool } type UsageBillingRepository interface { Apply(ctx context.Context, cmd *UsageBillingCommand) (*UsageBillingApplyResult, error) }