189 lines
6.5 KiB
Go
189 lines
6.5 KiB
Go
package billing
|
|||
|
|
|
||
|
|
import "testing"
|
||
|
|
|
||
|
|
func TestIsCustomPackage(t *testing.T) {
|
||
|
|
cases := []struct {
|
||
|
|
name string
|
||
|
|
isCustom bool
|
||
|
|
want bool
|
||
|
|
}{
|
||
|
|
{"Free", false, false},
|
||
|
|
{"Starter", false, false},
|
||
|
|
{"Growth", false, false},
|
||
|
|
{"Business", false, false},
|
||
|
|
{"Enterprise", true, true},
|
||
|
|
{"Enterprise", false, false}, // public ladder without is_custom flag
|
||
|
|
{"A1", false, false}, // legacy name without is_custom → limited matrix
|
||
|
|
{"A1", true, true}, // dump-faithful A1 PAYG is_custom → custom profile (Stores/AI still gated)
|
||
|
|
{"Legacy", true, false}, // exact Legacy package never enable-all
|
||
|
|
{"Merkur trial", false, true},
|
||
|
|
{" growth ", false, false},
|
||
|
|
{"", false, true}, // empty name is not a public plan name
|
||
|
|
}
|
||
|
|
for _, tc := range cases {
|
||
|
|
got := IsCustomPackage(tc.name, tc.isCustom)
|
||
|
|
if got != tc.want {
|
||
|
|
t.Fatalf("IsCustomPackage(%q, %v)=%v want %v", tc.name, tc.isCustom, got, tc.want)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestAllRegistryFeatures(t *testing.T) {
|
||
|
|
on := AllRegistryFeatures(true)
|
||
|
|
off := AllRegistryFeatures(false)
|
||
|
|
if len(on) != len(FeatureCatalogKeys) || len(off) != len(FeatureCatalogKeys) {
|
||
|
|
t.Fatalf("len on=%d off=%d catalog=%d", len(on), len(off), len(FeatureCatalogKeys))
|
||
|
|
}
|
||
|
|
for _, k := range FeatureCatalogKeys {
|
||
|
|
if !on[k] {
|
||
|
|
t.Fatalf("expected %s enabled", k)
|
||
|
|
}
|
||
|
|
if off[k] {
|
||
|
|
t.Fatalf("expected %s disabled", k)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPrepareCustomPackageCreateFeatures(t *testing.T) {
|
||
|
|
t.Run("custom create without features enables all", func(t *testing.T) {
|
||
|
|
p := Plan{Name: "ClientCo Deal", IsCustom: true}
|
||
|
|
prepareCustomPackageCreateFeatures(&p, true, false)
|
||
|
|
if p.Features == nil || len(p.Features) != len(FeatureCatalogKeys) {
|
||
|
|
t.Fatalf("expected full enable-all features, got %#v", p.Features)
|
||
|
|
}
|
||
|
|
for _, k := range FeatureCatalogKeys {
|
||
|
|
if !p.Features[k] {
|
||
|
|
t.Fatalf("key %s not enabled", k)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
t.Run("non-public name forces is_custom", func(t *testing.T) {
|
||
|
|
p := Plan{Name: "Merkur", IsCustom: false}
|
||
|
|
prepareCustomPackageCreateFeatures(&p, true, false)
|
||
|
|
if !p.IsCustom {
|
||
|
|
t.Fatal("expected is_custom forced true for client deal")
|
||
|
|
}
|
||
|
|
if len(p.Features) != len(FeatureCatalogKeys) {
|
||
|
|
t.Fatalf("expected enable-all after force custom, got %d keys", len(p.Features))
|
||
|
|
}
|
||
|
|
})
|
||
|
|
t.Run("public free create leaves features nil", func(t *testing.T) {
|
||
|
|
p := Plan{Name: "Free", IsCustom: false}
|
||
|
|
prepareCustomPackageCreateFeatures(&p, true, false)
|
||
|
|
if p.Features != nil {
|
||
|
|
t.Fatalf("standard Free must not materialize features: %#v", p.Features)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
t.Run("explicit features respected", func(t *testing.T) {
|
||
|
|
p := Plan{Name: "A1", IsCustom: true, Features: map[string]bool{"catalog.products": false}}
|
||
|
|
prepareCustomPackageCreateFeatures(&p, true, true)
|
||
|
|
if p.Features["catalog.products"] != false || len(p.Features) != 1 {
|
||
|
|
t.Fatalf("explicit features overwritten: %#v", p.Features)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
t.Run("update does not rewrite", func(t *testing.T) {
|
||
|
|
p := Plan{Name: "A1", IsCustom: true, ID: 9}
|
||
|
|
prepareCustomPackageCreateFeatures(&p, false, false)
|
||
|
|
if p.Features != nil {
|
||
|
|
t.Fatalf("update must not inject features: %#v", p.Features)
|
||
|
|
}
|
||
|
|
})
|
||
|
|
t.Run("enterprise is_custom create enables all", func(t *testing.T) {
|
||
|
|
p := Plan{Name: "Enterprise", IsCustom: true}
|
||
|
|
prepareCustomPackageCreateFeatures(&p, true, false)
|
||
|
|
if len(p.Features) != len(FeatureCatalogKeys) {
|
||
|
|
t.Fatalf("enterprise custom create should enable all, got %d", len(p.Features))
|
||
|
|
}
|
||
|
|
})
|
||
|
|
t.Run("A1 custom create uses PAYG matrix without Stores/Marketing/Integrations", func(t *testing.T) {
|
||
|
|
p := Plan{Name: "A1", IsCustom: true}
|
||
|
|
prepareCustomPackageCreateFeatures(&p, true, false)
|
||
|
|
if p.IsLegacy {
|
||
|
|
t.Fatal("A1 PAYG create must clear is_legacy")
|
||
|
|
}
|
||
|
|
if !p.IsCustom {
|
||
|
|
t.Fatal("A1 remains a client deal (is_custom)")
|
||
|
|
}
|
||
|
|
if !p.Features["processing.monitor"] {
|
||
|
|
t.Fatal("A1 PAYG must enable processing.monitor")
|
||
|
|
}
|
||
|
|
if !p.Features["capability.eprel"] {
|
||
|
|
t.Fatal("A1 PAYG must enable capability.eprel")
|
||
|
|
}
|
||
|
|
if p.Features["stores.hub"] || p.Features["marketing.campaigns"] || p.Features["integrations.ai"] || p.Features["integrations.email"] {
|
||
|
|
t.Fatal("A1 PAYG must deny stores, marketing, and integrations")
|
||
|
|
}
|
||
|
|
if len(p.Features) != len(FeatureCatalogKeys) {
|
||
|
|
t.Fatalf("expected full tailored matrix, got %d keys", len(p.Features))
|
||
|
|
}
|
||
|
|
})
|
||
|
|
t.Run("Legacy create seeds legacy sparse", func(t *testing.T) {
|
||
|
|
p := Plan{Name: "Legacy", IsCustom: false, IsLegacy: true}
|
||
|
|
prepareCustomPackageCreateFeatures(&p, true, false)
|
||
|
|
if !p.IsLegacy {
|
||
|
|
t.Fatal("Legacy create must set is_legacy")
|
||
|
|
}
|
||
|
|
if p.Features["processing.monitor"] {
|
||
|
|
t.Fatal("Legacy must not enable processing.monitor")
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestDefaultPlanFeaturesCustomUsesIsCustomPackage(t *testing.T) {
|
||
|
|
// Non-legacy client deal without is_custom flag still all-ON via name.
|
||
|
|
m := DefaultPlanFeatures("ClientCo Deal", false)
|
||
|
|
for _, k := range FeatureCatalogKeys {
|
||
|
|
if !m[k] {
|
||
|
|
t.Fatalf("client deal default missing %s", k)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
// A1 without is_custom stays legacy — limited matrix.
|
||
|
|
a1 := DefaultPlanFeatures("A1", false)
|
||
|
|
if a1["processing.monitor"] {
|
||
|
|
t.Fatal("legacy A1 (is_custom=false) must keep processing.monitor off")
|
||
|
|
}
|
||
|
|
a1Payg := DefaultPlanFeatures("A1", true)
|
||
|
|
if !a1Payg["processing.monitor"] || !a1Payg["capability.eprel"] {
|
||
|
|
t.Fatal("A1 PAYG is_custom must enable core PAYG features")
|
||
|
|
}
|
||
|
|
if a1Payg["stores.hub"] || a1Payg["stores.shopify"] || a1Payg["marketing.campaigns"] || a1Payg["integrations.ai"] || a1Payg["integrations.ai.byok"] || a1Payg["integrations.email"] {
|
||
|
|
t.Fatal("A1 PAYG must keep Stores, Marketing, and Integrations off")
|
||
|
|
}
|
||
|
|
free := DefaultPlanFeatures("Free", false)
|
||
|
|
if free["capability.ai_processing"] {
|
||
|
|
t.Fatal("Free should keep AI processing off by default")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestPlanAllowsFeatureCustomByName(t *testing.T) {
|
||
|
|
if !PlanAllowsFeature("Merkur trial", false, nil, "capability.byok") {
|
||
|
|
t.Fatal("non-public package should allow all keys when overrides empty")
|
||
|
|
}
|
||
|
|
if PlanAllowsFeature("Free", false, nil, "capability.byok") {
|
||
|
|
t.Fatal("Free should deny byok by default")
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
func TestResolveEffectiveFeaturesSectionGate(t *testing.T) {
|
||
|
|
gates := emptyGatesView()
|
||
|
|
gates.Sections["marketing"] = false
|
||
|
|
features, sections, disabled := ResolveEffectiveFeatures("Merkur", true, nil, gates)
|
||
|
|
if sections["marketing"] {
|
||
|
|
t.Fatal("marketing section should be off")
|
||
|
|
}
|
||
|
|
if features["marketing.campaigns"] {
|
||
|
|
t.Fatal("marketing.campaigns should be effective-false when section off")
|
||
|
|
}
|
||
|
|
found := false
|
||
|
|
for _, d := range disabled {
|
||
|
|
if d == "marketing.campaigns" {
|
||
|
|
found = true
|
||
|
|
break
|
||
|
|
}
|
||
|
|
}
|
||
|
|
if !found {
|
||
|
|
t.Fatal("marketing.campaigns should appear in disabled list")
|
||
|
|
}
|
||
|
|
}
|