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 }