mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-07-09 16:00:59 +00:00
Add the dormant Business tier to the licensing core
TierBusiness: Pro's feature set, 365-day history retention, uncapped core monitoring like every self-hosted tier, Business display name, and a slot in the min-tier ordering. Differentiation is by max_users limit, retention, and support rather than features (multi-user is already a Pro capability via RBAC; see specs/pricing-segmentation.md binder findings). Dormant until the license server issues business plan versions; no checkout, pricing, or UI surface references it yet. Contract shape recorded as cloud-paid Extension Point 26 and pinned by TestBusinessTierContractShape.
This commit is contained in:
parent
a205ba3498
commit
fca5975c38
4 changed files with 61 additions and 2 deletions
|
|
@ -1034,6 +1034,22 @@ hands-on Patrol modes, issue investigation, verified fixes, and longer history`.
|
||||||
Landing behavior for paid and hosted shells must also defer to the
|
Landing behavior for paid and hosted shells must also defer to the
|
||||||
frontend-primitives-owned provider-first landing contract instead of
|
frontend-primitives-owned provider-first landing contract instead of
|
||||||
defining a cloud-paid-specific order.
|
defining a cloud-paid-specific order.
|
||||||
|
26. Introduce the self-hosted `business` tier only through this shape:
|
||||||
|
`TierBusiness` in `pkg/licensing/features.go` carries exactly the Pro
|
||||||
|
feature set (no feature-level differentiation), 365-day history
|
||||||
|
retention in `TierHistoryDays`, membership in the self-hosted
|
||||||
|
core-monitoring-uncapped tier and plan-version sets (`business`,
|
||||||
|
`business_annual`), the `Business` display name, and a slot between Pro
|
||||||
|
and MSP in the min-tier ordering. Business differentiates commercially
|
||||||
|
by the `max_users` license limit (unlimited for Business; newly issued
|
||||||
|
Pro licenses may carry a finite `max_users` while previously issued
|
||||||
|
licenses keep their unlimited posture), retention, and support, never
|
||||||
|
by gating features away from Pro. The tier stays dormant until the
|
||||||
|
license server issues business plan versions through a governed
|
||||||
|
rollout; no checkout, pricing-model payload, public pricing page, or
|
||||||
|
in-product plan surface may reference Business before that rollout, and
|
||||||
|
monitored-system volume stays out of the Business plan model per the
|
||||||
|
self-hosted commercial boundary above.
|
||||||
|
|
||||||
## Forbidden Paths
|
## Forbidden Paths
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -38,6 +38,7 @@ const (
|
||||||
TierProPlus = licensing.TierProPlus
|
TierProPlus = licensing.TierProPlus
|
||||||
TierProAnnual = licensing.TierProAnnual
|
TierProAnnual = licensing.TierProAnnual
|
||||||
TierLifetime = licensing.TierLifetime
|
TierLifetime = licensing.TierLifetime
|
||||||
|
TierBusiness = licensing.TierBusiness
|
||||||
TierCloud = licensing.TierCloud
|
TierCloud = licensing.TierCloud
|
||||||
TierMSP = licensing.TierMSP
|
TierMSP = licensing.TierMSP
|
||||||
TierEnterprise = licensing.TierEnterprise
|
TierEnterprise = licensing.TierEnterprise
|
||||||
|
|
|
||||||
|
|
@ -55,6 +55,7 @@ const (
|
||||||
TierProPlus Tier = "pro_plus"
|
TierProPlus Tier = "pro_plus"
|
||||||
TierProAnnual Tier = "pro_annual" // Legacy: same features as TierPro
|
TierProAnnual Tier = "pro_annual" // Legacy: same features as TierPro
|
||||||
TierLifetime Tier = "lifetime" // Legacy: same features as TierPro
|
TierLifetime Tier = "lifetime" // Legacy: same features as TierPro
|
||||||
|
TierBusiness Tier = "business" // Pro features; differentiated by user limit, retention, and support
|
||||||
TierCloud Tier = "cloud"
|
TierCloud Tier = "cloud"
|
||||||
TierMSP Tier = "msp"
|
TierMSP Tier = "msp"
|
||||||
TierEnterprise Tier = "enterprise"
|
TierEnterprise Tier = "enterprise"
|
||||||
|
|
@ -134,7 +135,7 @@ func IsSelfHostedCommunityPlanVersion(planVersion string) bool {
|
||||||
// self-hosted v6 contract where core monitoring volume is not monetized.
|
// self-hosted v6 contract where core monitoring volume is not monetized.
|
||||||
func IsSelfHostedCoreMonitoringUncappedTier(tier Tier) bool {
|
func IsSelfHostedCoreMonitoringUncappedTier(tier Tier) bool {
|
||||||
switch Tier(strings.ToLower(strings.TrimSpace(string(tier)))) {
|
switch Tier(strings.ToLower(strings.TrimSpace(string(tier)))) {
|
||||||
case TierFree, TierRelay, TierPro, TierProPlus, TierProAnnual, TierLifetime:
|
case TierFree, TierRelay, TierPro, TierProPlus, TierProAnnual, TierLifetime, TierBusiness:
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
return false
|
return false
|
||||||
|
|
@ -151,6 +152,8 @@ func IsSelfHostedCoreMonitoringUncappedPlanVersion(planVersion string) bool {
|
||||||
string(TierProPlus),
|
string(TierProPlus),
|
||||||
string(TierProAnnual),
|
string(TierProAnnual),
|
||||||
string(TierLifetime),
|
string(TierLifetime),
|
||||||
|
string(TierBusiness),
|
||||||
|
"business_annual",
|
||||||
"v5_lifetime_grandfathered":
|
"v5_lifetime_grandfathered":
|
||||||
return true
|
return true
|
||||||
default:
|
default:
|
||||||
|
|
@ -215,6 +218,7 @@ var TierHistoryDays = map[Tier]int{
|
||||||
TierProPlus: 90,
|
TierProPlus: 90,
|
||||||
TierProAnnual: 90,
|
TierProAnnual: 90,
|
||||||
TierLifetime: 90,
|
TierLifetime: 90,
|
||||||
|
TierBusiness: 365,
|
||||||
TierCloud: 90,
|
TierCloud: 90,
|
||||||
TierMSP: 90,
|
TierMSP: 90,
|
||||||
TierEnterprise: 90,
|
TierEnterprise: 90,
|
||||||
|
|
@ -274,6 +278,7 @@ var TierFeatures = map[Tier][]string{
|
||||||
TierProPlus: proFeatures, // Legacy compatibility tier; same runtime features as Pro
|
TierProPlus: proFeatures, // Legacy compatibility tier; same runtime features as Pro
|
||||||
TierProAnnual: proFeatures, // Legacy: same features as Pro
|
TierProAnnual: proFeatures, // Legacy: same features as Pro
|
||||||
TierLifetime: proFeatures, // Legacy: same features as Pro
|
TierLifetime: proFeatures, // Legacy: same features as Pro
|
||||||
|
TierBusiness: proFeatures, // Same features as Pro; differentiated by max_users limit, 365-day history, and support
|
||||||
TierCloud: proFeatures, // Cloud includes all Pro features + managed hosting
|
TierCloud: proFeatures, // Cloud includes all Pro features + managed hosting
|
||||||
TierMSP: mspFeatures,
|
TierMSP: mspFeatures,
|
||||||
TierEnterprise: enterpriseFeatures,
|
TierEnterprise: enterpriseFeatures,
|
||||||
|
|
@ -406,6 +411,8 @@ func GetTierDisplayName(tier Tier) string {
|
||||||
return "Pro (Annual)"
|
return "Pro (Annual)"
|
||||||
case TierLifetime:
|
case TierLifetime:
|
||||||
return "Pro (Lifetime)"
|
return "Pro (Lifetime)"
|
||||||
|
case TierBusiness:
|
||||||
|
return "Business"
|
||||||
case TierCloud:
|
case TierCloud:
|
||||||
return "Cloud"
|
return "Cloud"
|
||||||
case TierMSP:
|
case TierMSP:
|
||||||
|
|
@ -421,7 +428,7 @@ func GetTierDisplayName(tier Tier) string {
|
||||||
// This is used for user-facing messages like "requires Pulse Relay or above".
|
// This is used for user-facing messages like "requires Pulse Relay or above".
|
||||||
// The tier ordering is: Free < Relay < Pro < MSP < Enterprise.
|
// The tier ordering is: Free < Relay < Pro < MSP < Enterprise.
|
||||||
func GetFeatureMinTierName(feature string) string {
|
func GetFeatureMinTierName(feature string) string {
|
||||||
orderedTiers := []Tier{TierFree, TierRelay, TierPro, TierMSP, TierEnterprise}
|
orderedTiers := []Tier{TierFree, TierRelay, TierPro, TierBusiness, TierMSP, TierEnterprise}
|
||||||
for _, tier := range orderedTiers {
|
for _, tier := range orderedTiers {
|
||||||
if TierHasFeature(tier, feature) {
|
if TierHasFeature(tier, feature) {
|
||||||
return GetTierDisplayName(tier)
|
return GetTierDisplayName(tier)
|
||||||
|
|
|
||||||
|
|
@ -846,3 +846,38 @@ func TestPriceIDToPlanVersion_AllMapToKnownPlans(t *testing.T) {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestBusinessTierContractShape pins the dormant Business tier's contract:
|
||||||
|
// Pro's exact feature set (differentiation is by max_users limit, retention,
|
||||||
|
// and support, never by features), 365-day history, uncapped self-hosted core
|
||||||
|
// monitoring, recognized plan versions, and the Business display name. See
|
||||||
|
// cloud-paid Extension Point 26.
|
||||||
|
func TestBusinessTierContractShape(t *testing.T) {
|
||||||
|
proSet := TierFeatures[TierPro]
|
||||||
|
businessSet := TierFeatures[TierBusiness]
|
||||||
|
if len(proSet) != len(businessSet) {
|
||||||
|
t.Fatalf("Business features (%d) must equal Pro features (%d)", len(businessSet), len(proSet))
|
||||||
|
}
|
||||||
|
for i, feature := range proSet {
|
||||||
|
if businessSet[i] != feature {
|
||||||
|
t.Fatalf("Business feature[%d] = %q, want Pro's %q", i, businessSet[i], feature)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := TierHistoryDays[TierBusiness]; got != 365 {
|
||||||
|
t.Fatalf("TierHistoryDays[TierBusiness] = %d, want 365", got)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !IsSelfHostedCoreMonitoringUncappedTier(TierBusiness) {
|
||||||
|
t.Fatal("TierBusiness must be self-hosted core-monitoring uncapped")
|
||||||
|
}
|
||||||
|
for _, plan := range []string{"business", "business_annual"} {
|
||||||
|
if !IsSelfHostedCoreMonitoringUncappedPlanVersion(plan) {
|
||||||
|
t.Fatalf("plan version %q must be self-hosted core-monitoring uncapped", plan)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if got := GetTierDisplayName(TierBusiness); got != "Business" {
|
||||||
|
t.Fatalf("GetTierDisplayName(TierBusiness) = %q, want Business", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue