Files
descrybe/apps/api/internal/billing/custom_package_features.go
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

131 lines
4.3 KiB
Go

package billing
import (
"context"
"strings"
"github.com/google/uuid"
)
// IsCustomPackage reports whether a plan should get the "custom deal" feature
// treatment (all dashboard features ON by default for non-A1 deals).
//
// Product semantics (see IsPublicProductPlan + plans.is_custom):
// - Client / admin deals with is_custom=true → custom (including A1 PAYG)
// - Public Enterprise (and any row with is_custom=true) → custom
// - Exact "Legacy" plan name → never enable-all (restricted migrated matrix)
// - Free / Starter / Growth / Business with is_custom=false → not custom
//
// is_custom wins over A1* name patterns for PAYG billing / PlanProfileCustom,
// but A1* custom deals use A1PaygPlanFeatures (not literal enable-all) so
// Stores, Marketing, and Integrations stay off.
func IsCustomPackage(name string, isCustom bool) bool {
if strings.EqualFold(strings.TrimSpace(name), LegacyPlanName) {
return false
}
if isCustom {
return true
}
// Legacy-named plans without is_custom stay on the limited matrix.
if IsLegacyPlanName(name) {
return false
}
return !IsPublicProductPlan(name)
}
// A1PaygFeatureDenied reports keys that stay OFF on A1 PAYG custom deals.
// Nav: Stores → stores.*; Marketing → marketing.*; Integrations → integrations.*.
// Cutover honesty chrome (ETL gaps / reconnect / migrated checklist) stays OFF — A1 is not a
// hypercare merchant surface (see MigratedEtlGapsPanel + IsA1CohortCompany product rules).
func A1PaygFeatureDenied(key string) bool {
switch key {
case "dashboard.etl_gaps", "dashboard.store_reconnect", "dashboard.migrated_checklist":
return true
}
return strings.HasPrefix(key, "stores.") ||
strings.HasPrefix(key, "marketing.") ||
strings.HasPrefix(key, "integrations.")
}
// A1PaygPlanFeatures is the dump-faithful A1 PAYG matrix: custom enable-all
// minus Stores, Marketing, and Integrations sections.
func A1PaygPlanFeatures() map[string]bool {
out := AllRegistryFeatures(true)
for k := range out {
if A1PaygFeatureDenied(k) {
out[k] = false
}
}
return out
}
// SparseA1PaygOverrides returns explicit false overrides for A1 PAYG denied keys.
func SparseA1PaygOverrides() map[string]bool {
out := make(map[string]bool)
for _, k := range FeatureCatalogKeys {
if A1PaygFeatureDenied(k) {
out[k] = false
}
}
return out
}
// AllRegistryFeatures returns every FeatureCatalogKeys entry set to enabled.
func AllRegistryFeatures(enabled bool) map[string]bool {
out := make(map[string]bool, len(FeatureCatalogKeys))
for _, k := range FeatureCatalogKeys {
out[k] = enabled
}
return out
}
// EnableSectionForAllPlans turns a section master switch ON for every plan
// (global gate; missing rows already default ON).
func (s *Service) EnableSectionForAllPlans(ctx context.Context, section string, updatedBy *uuid.UUID) (FeatureGatesView, error) {
return s.SetSectionGate(ctx, section, true, updatedBy)
}
// DisableSectionForAllPlans turns a section master switch OFF for every plan.
func (s *Service) DisableSectionForAllPlans(ctx context.Context, section string, updatedBy *uuid.UUID) (FeatureGatesView, error) {
return s.SetSectionGate(ctx, section, false, updatedBy)
}
// prepareCustomPackageCreateFeatures applies create-time defaults for custom packages:
// when the caller omitted features, materialize enable-all overrides so admin UIs
// show an explicit all-ON matrix (resolve already treats empty+is_custom as all ON).
func prepareCustomPackageCreateFeatures(p *Plan, creating, featuresProvided bool) {
if !creating {
return
}
if IsLegacyPlan(p.Name, p.IsLegacy) && !IsCustomPackage(p.Name, p.IsCustom) {
p.IsLegacy = true
if strings.EqualFold(strings.TrimSpace(p.Name), LegacyPlanName) {
p.IsCustom = false
} else if !IsPublicProductPlan(p.Name) {
p.IsCustom = true
}
if !featuresProvided {
p.Features = SparseLegacyOverrides()
}
return
}
// Non-public ladder names are client deals — keep is_custom aligned.
if !IsPublicProductPlan(p.Name) {
p.IsCustom = true
}
// A1 PAYG / custom deals are never the restricted Legacy matrix.
if IsCustomPackage(p.Name, p.IsCustom) {
p.IsLegacy = false
}
if featuresProvided {
return
}
if IsCustomPackage(p.Name, p.IsCustom) {
if IsLegacyPlanName(p.Name) {
p.Features = A1PaygPlanFeatures()
return
}
p.Features = AllRegistryFeatures(true)
}
}