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
|
||||
frontend-primitives-owned provider-first landing contract instead of
|
||||
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
|
||||
|
||||
|
|
|
|||
|
|
@ -38,6 +38,7 @@ const (
|
|||
TierProPlus = licensing.TierProPlus
|
||||
TierProAnnual = licensing.TierProAnnual
|
||||
TierLifetime = licensing.TierLifetime
|
||||
TierBusiness = licensing.TierBusiness
|
||||
TierCloud = licensing.TierCloud
|
||||
TierMSP = licensing.TierMSP
|
||||
TierEnterprise = licensing.TierEnterprise
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ const (
|
|||
TierProPlus Tier = "pro_plus"
|
||||
TierProAnnual Tier = "pro_annual" // 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"
|
||||
TierMSP Tier = "msp"
|
||||
TierEnterprise Tier = "enterprise"
|
||||
|
|
@ -134,7 +135,7 @@ func IsSelfHostedCommunityPlanVersion(planVersion string) bool {
|
|||
// self-hosted v6 contract where core monitoring volume is not monetized.
|
||||
func IsSelfHostedCoreMonitoringUncappedTier(tier Tier) bool {
|
||||
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
|
||||
default:
|
||||
return false
|
||||
|
|
@ -151,6 +152,8 @@ func IsSelfHostedCoreMonitoringUncappedPlanVersion(planVersion string) bool {
|
|||
string(TierProPlus),
|
||||
string(TierProAnnual),
|
||||
string(TierLifetime),
|
||||
string(TierBusiness),
|
||||
"business_annual",
|
||||
"v5_lifetime_grandfathered":
|
||||
return true
|
||||
default:
|
||||
|
|
@ -215,6 +218,7 @@ var TierHistoryDays = map[Tier]int{
|
|||
TierProPlus: 90,
|
||||
TierProAnnual: 90,
|
||||
TierLifetime: 90,
|
||||
TierBusiness: 365,
|
||||
TierCloud: 90,
|
||||
TierMSP: 90,
|
||||
TierEnterprise: 90,
|
||||
|
|
@ -274,6 +278,7 @@ var TierFeatures = map[Tier][]string{
|
|||
TierProPlus: proFeatures, // Legacy compatibility tier; same runtime features as Pro
|
||||
TierProAnnual: 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
|
||||
TierMSP: mspFeatures,
|
||||
TierEnterprise: enterpriseFeatures,
|
||||
|
|
@ -406,6 +411,8 @@ func GetTierDisplayName(tier Tier) string {
|
|||
return "Pro (Annual)"
|
||||
case TierLifetime:
|
||||
return "Pro (Lifetime)"
|
||||
case TierBusiness:
|
||||
return "Business"
|
||||
case TierCloud:
|
||||
return "Cloud"
|
||||
case TierMSP:
|
||||
|
|
@ -421,7 +428,7 @@ func GetTierDisplayName(tier Tier) string {
|
|||
// This is used for user-facing messages like "requires Pulse Relay or above".
|
||||
// The tier ordering is: Free < Relay < Pro < MSP < Enterprise.
|
||||
func GetFeatureMinTierName(feature string) string {
|
||||
orderedTiers := []Tier{TierFree, TierRelay, TierPro, TierMSP, TierEnterprise}
|
||||
orderedTiers := []Tier{TierFree, TierRelay, TierPro, TierBusiness, TierMSP, TierEnterprise}
|
||||
for _, tier := range orderedTiers {
|
||||
if TierHasFeature(tier, feature) {
|
||||
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