Files
descrybe/apps/api/internal/billing/features_api.go
T

136 lines
4.5 KiB
Go
Raw Normal View History

package billing
import (
"context"
"fmt"
"strings"
"github.com/google/uuid"
)
// FeatureDef is one catalog entry for listFeatures / admin editors.
type FeatureDef struct {
Key string `json:"key"`
Section string `json:"section"`
Label string `json:"label,omitempty"`
}
// ListFeatures returns the canonical feature registry (listFeatures).
func (s *Service) ListFeatures(_ context.Context) ([]FeatureDef, error) {
out := make([]FeatureDef, 0, len(FeatureCatalogKeys))
for _, key := range FeatureCatalogKeys {
section, _ := SectionOfFeature(key)
out = append(out, FeatureDef{
Key: key,
Section: section,
Label: key,
})
}
return out, nil
}
// IsAllowed reports effective(feature) for a company's active plan (isAllowed).
func (s *Service) IsAllowed(ctx context.Context, companyID uuid.UUID, key string) (bool, error) {
caps, err := s.CapabilitiesForCompany(ctx, companyID)
if err != nil {
return false, err
}
key = strings.TrimSpace(key)
if caps.Features == nil {
return false, nil
}
return caps.Features[key], nil
}
// IsAllowedForPlan reports effective(feature) for a plan id (globals still apply).
func (s *Service) IsAllowedForPlan(ctx context.Context, planID int64, key string) (bool, error) {
name, isCustom, isLegacy, overrides, err := s.loadPlanFeaturesRow(ctx, planID)
if err != nil {
return false, err
}
gates, err := s.GetFeatureGates(ctx)
if err != nil {
return false, err
}
features, _, _ := ResolveEffectiveFeaturesEx(name, isCustom, isLegacy, overrides, gates)
return features[strings.TrimSpace(key)], nil
}
// SetPlanFeature merges one override into plans.features (setPlanFeature).
func (s *Service) SetPlanFeature(ctx context.Context, planID int64, key string, enabled bool) error {
key = strings.TrimSpace(key)
if !IsKnownFeatureKey(key) {
return fmt.Errorf("%w: %s", ErrUnknownFeatureKey, key)
}
_, _, _, overrides, err := s.loadPlanFeaturesRow(ctx, planID)
if err != nil {
return err
}
if overrides == nil {
overrides = map[string]bool{}
}
overrides[key] = enabled
_, err = s.SetPlanFeatures(ctx, planID, overrides)
return err
}
// SetGlobalFeature upserts one platform master switch (setGlobalFeature).
// Section ids use kind=section; feature keys use kind=feature.
func (s *Service) SetGlobalFeature(ctx context.Context, gateKey string, enabled bool, updatedBy *uuid.UUID) error {
gateKey = strings.TrimSpace(gateKey)
if gateKey == "" {
return fmt.Errorf("%w: empty gate key", ErrUnknownFeatureKey)
}
if IsKnownFeatureSection(gateKey) {
_, err := s.SetSectionGate(ctx, gateKey, enabled, updatedBy)
return err
}
if !IsKnownFeatureKey(gateKey) {
return fmt.Errorf("%w: %s", ErrUnknownFeatureKey, gateKey)
}
_, err := s.SetFeatureGates(ctx, nil, map[string]bool{gateKey: enabled}, updatedBy)
return err
}
// EnableAllForPlan writes all registry keys as true overrides (enableAllForPlan).
func (s *Service) EnableAllForPlan(ctx context.Context, planID int64) error {
_, err := s.EnableAllPlanFeatures(ctx, planID)
return err
}
// ApplyDefaultMatrix replaces plans.features with sparse defaults for that plan (applyDefaultMatrix).
// Custom packages get an empty override map (is_custom => all ON at resolve).
// Legacy packages get SparseLegacyOverrides (processing.monitor OFF; image-nav ON).
func (s *Service) ApplyDefaultMatrix(ctx context.Context, planID int64) error {
name, isCustom, isLegacy, _, err := s.loadPlanFeaturesRow(ctx, planID)
if err != nil {
return err
}
_, err = s.SetPlanFeatures(ctx, planID, SparseDefaultOverridesEx(name, isCustom, isLegacy))
return err
}
// AssertFeature fails closed when a feature is not effective for the company.
func (s *Service) AssertFeature(ctx context.Context, companyID uuid.UUID, key string) error {
ok, err := s.IsAllowed(ctx, companyID, key)
if err != nil {
return err
}
if !ok {
return fmt.Errorf("%w: %s", ErrFeatureDisabled, strings.TrimSpace(key))
}
return nil
}
// ResolveFeatures is the contract name for ResolveEffectiveFeatures (plan ∧ globals).
func ResolveFeatures(planName string, isCustom bool, overrides map[string]bool, gates FeatureGatesView) map[string]bool {
features, _, _ := ResolveEffectiveFeatures(planName, isCustom, overrides, gates)
return features
}
// ResolveFeaturesEx includes an explicit is_legacy flag.
func ResolveFeaturesEx(planName string, isCustom, isLegacy bool, overrides map[string]bool, gates FeatureGatesView) map[string]bool {
features, _, _ := ResolveEffectiveFeaturesEx(planName, isCustom, isLegacy, overrides, gates)
return features
}