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.
This commit is contained in:
@@ -0,0 +1,135 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user