Files
descrybe/apps/api/internal/billing/feature_enforcement.go
T
greeneclipse 8580c996c3 Initial commit of Descrybe v2 without local scratch artifacts.
Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
2026-08-09 22:47:43 +02:00

77 lines
2.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package billing
import (
"context"
"errors"
"fmt"
"strings"
"github.com/google/uuid"
)
// FeatureKeyFromError extracts the feature key from an ErrFeatureDisabled wrap
// ("feature_disabled: marketing.campaigns.generate_ai").
func FeatureKeyFromError(err error) string {
if err == nil || !errors.Is(err, ErrFeatureDisabled) {
return ""
}
msg := err.Error()
const prefix = "feature_disabled:"
idx := strings.Index(strings.ToLower(msg), prefix)
if idx < 0 {
return ""
}
return strings.TrimSpace(msg[idx+len(prefix):])
}
// FeatureKeysForProcessingType maps a processing job type to registry keys that
// must be effective before StartJob may proceed.
func FeatureKeysForProcessingType(processingType string) []string {
switch strings.ToLower(strings.TrimSpace(processingType)) {
case "title":
return []string{"capability.ai_processing", "catalog.products.process_ai_titles"}
case "description":
return []string{"capability.ai_processing", "catalog.products.process_ai_descriptions"}
case "enhance", "enhance_only", "enhance-only", "seo", "seo_ai":
return []string{"capability.ai_processing"}
case "eprel", "eprel_only":
return []string{"capability.eprel"}
case "email_campaign", "email_campaign_ai", "campaign_ai":
return []string{"capability.campaign_ai", "marketing.campaigns.generate_ai"}
case "normalize", "specs", "fill", "categories", "attributes", "full", "":
return []string{"capability.normalize_specs_fill"}
default:
return []string{"capability.normalize_specs_fill"}
}
}
// AssertFeatures fails closed on the first disabled key.
// Loads Capabilities once for the whole key set (avoids N×CapabilitiesForCompany).
func (s *Service) AssertFeatures(ctx context.Context, companyID uuid.UUID, keys ...string) error {
if len(keys) == 0 {
return nil
}
caps, err := s.CapabilitiesForCompany(ctx, companyID)
if err != nil {
return err
}
for _, key := range keys {
key = strings.TrimSpace(key)
if key == "" {
continue
}
if caps.Features == nil || !caps.Features[key] {
return fmt.Errorf("%w: %s", ErrFeatureDisabled, key)
}
}
return nil
}
// AssertProcessingFeatures enforces plan ∩ global feature keys for a job type.
func (s *Service) AssertProcessingFeatures(ctx context.Context, companyID uuid.UUID, processingType string) error {
if s == nil {
return nil
}
return s.AssertFeatures(ctx, companyID, FeatureKeysForProcessingType(processingType)...)
}