Drop one-shot tmp/axe scripts and agent i18n scratch so the Gitea tree is deployable.
136 lines
4.5 KiB
Go
136 lines
4.5 KiB
Go
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
|
|
}
|