mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-15 01:07:32 +00:00
parent
9c3d96cab2
commit
9e60f2aec6
8 changed files with 176 additions and 3 deletions
|
|
@ -247,6 +247,15 @@ Insights`, rather than reviving generic `AI Patrol` or `AI ... analysis`
|
|||
value must come from optional extras, hosted convenience, business
|
||||
workflow, support, or similar non-core surfaces rather than using
|
||||
monitored-system volume itself as the primary paid gate.
|
||||
9. Keep migrated self-hosted Community/free billing state uncapped even when
|
||||
the persisted file still carries legacy v5 commercial limit keys:
|
||||
`pkg/licensing/billing_state_normalization.go` and
|
||||
`pkg/licensing/database_source.go` must scrub stale
|
||||
`max_monitored_systems` and `max_guests` values for community/free
|
||||
billing-state plan labels before runtime-capability, entitlement, or
|
||||
warning-banner payloads are built, while leaving non-community plan labels
|
||||
available for bounded hosted or legacy continuity contracts that still
|
||||
carry explicit monitored-system ceilings.
|
||||
|
||||
## Current State
|
||||
|
||||
|
|
|
|||
|
|
@ -494,6 +494,59 @@ func TestEntitlementHandler_GrandfatheredRecurringEvaluatorStateIsUncapped(t *te
|
|||
}
|
||||
}
|
||||
|
||||
func TestHandleRuntimeCapabilities_HostedCommunityEvaluatorStateStripsLegacyCommercialCaps(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
mtp := config.NewMultiTenantPersistence(baseDir)
|
||||
|
||||
orgID := "test-hosted-community-runtime-capabilities"
|
||||
if _, err := mtp.GetPersistence(orgID); err != nil {
|
||||
t.Fatalf("GetPersistence(%s) failed: %v", orgID, err)
|
||||
}
|
||||
|
||||
store := config.NewFileBillingStore(baseDir)
|
||||
if err := store.SaveBillingState(orgID, &entitlements.BillingState{
|
||||
Capabilities: []string{
|
||||
license.FeatureAIPatrol,
|
||||
},
|
||||
Limits: map[string]int64{
|
||||
"max_monitored_systems": 1,
|
||||
"max_guests": 5,
|
||||
},
|
||||
PlanVersion: "community",
|
||||
SubscriptionState: entitlements.SubStateActive,
|
||||
}); err != nil {
|
||||
t.Fatalf("SaveBillingState(%s) failed: %v", orgID, err)
|
||||
}
|
||||
|
||||
h := NewLicenseHandlers(mtp, true)
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/license/runtime-capabilities", nil)
|
||||
req = req.WithContext(context.WithValue(req.Context(), OrgIDContextKey, orgID))
|
||||
rec := httptest.NewRecorder()
|
||||
h.HandleRuntimeCapabilities(rec, req)
|
||||
|
||||
if rec.Code != http.StatusOK {
|
||||
t.Fatalf("status=%d, want %d", rec.Code, http.StatusOK)
|
||||
}
|
||||
|
||||
var payload RuntimeCapabilitiesPayload
|
||||
if err := json.Unmarshal(rec.Body.Bytes(), &payload); err != nil {
|
||||
t.Fatalf("unmarshal payload failed: %v", err)
|
||||
}
|
||||
|
||||
for _, limit := range payload.Limits {
|
||||
if limit.Key == "max_monitored_systems" || limit.Key == "max_guests" {
|
||||
t.Fatalf("expected runtime capabilities to omit stale commercial caps, got %+v", payload.Limits)
|
||||
}
|
||||
}
|
||||
if payload.MonitoredSystemCapacity == nil {
|
||||
t.Fatal("expected monitored_system_capacity in runtime capabilities payload")
|
||||
}
|
||||
if payload.MonitoredSystemCapacity.Limit != 0 {
|
||||
t.Fatalf("monitored_system_capacity.limit=%d, want %d", payload.MonitoredSystemCapacity.Limit, 0)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntitlementHandler_TrialEligibility_FreshOrgAllowed(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
mtp := config.NewMultiTenantPersistence(baseDir)
|
||||
|
|
|
|||
|
|
@ -48,6 +48,9 @@ func NormalizeBillingState(state *BillingState) *BillingState {
|
|||
normalized.CommercialMigration = NormalizeCommercialMigrationStatus(normalized.CommercialMigration)
|
||||
|
||||
normalized.Limits = NormalizeMonitoredSystemLimits(normalized.Limits)
|
||||
if IsSelfHostedCommunityPlanVersion(normalized.PlanVersion) {
|
||||
stripLegacyCommercialCaps(normalized.Limits)
|
||||
}
|
||||
|
||||
// Ensure slices/maps are never nil (JSON marshals as [] / {} instead of null).
|
||||
if normalized.Capabilities == nil {
|
||||
|
|
@ -78,6 +81,9 @@ func NormalizeBillingState(state *BillingState) *BillingState {
|
|||
|
||||
func billingStateStoredMonitoredSystemLimit(planVersion string) (int, bool) {
|
||||
planVersion = CanonicalizePlanVersion(planVersion)
|
||||
if IsSelfHostedCommunityPlanVersion(planVersion) {
|
||||
return 0, false
|
||||
}
|
||||
if IsGrandfatheredRecurringV5PlanVersion(planVersion) {
|
||||
return UnknownPlanDefaultMonitoredSystemLimit, true
|
||||
}
|
||||
|
|
|
|||
|
|
@ -349,6 +349,36 @@ func TestNormalizeBillingState_PreservesNonCloudPlanLimits(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestNormalizeBillingState_SelfHostedCommunityPlanStripsLegacyCommercialCaps(t *testing.T) {
|
||||
state := &BillingState{
|
||||
PlanVersion: " community ",
|
||||
Limits: map[string]int64{
|
||||
"max_monitored_systems": 1,
|
||||
"max_guests": 5,
|
||||
"max_nodes": 99,
|
||||
"max_reports": 7,
|
||||
},
|
||||
SubscriptionState: SubStateActive,
|
||||
}
|
||||
|
||||
normalized := NormalizeBillingState(state)
|
||||
if normalized.PlanVersion != "community" {
|
||||
t.Fatalf("plan_version=%q, want %q", normalized.PlanVersion, "community")
|
||||
}
|
||||
if _, ok := normalized.Limits["max_monitored_systems"]; ok {
|
||||
t.Fatalf("expected max_monitored_systems to be scrubbed, got %v", normalized.Limits)
|
||||
}
|
||||
if _, ok := normalized.Limits["max_guests"]; ok {
|
||||
t.Fatalf("expected max_guests to be scrubbed, got %v", normalized.Limits)
|
||||
}
|
||||
if _, ok := normalized.Limits["max_nodes"]; ok {
|
||||
t.Fatalf("expected max_nodes to be removed during normalization, got %v", normalized.Limits)
|
||||
}
|
||||
if got := normalized.Limits["max_reports"]; got != 7 {
|
||||
t.Fatalf("limits[max_reports]=%d, want %d", got, 7)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNormalizeBillingState_StripsEntitlementsForRevokedSubscriptions(t *testing.T) {
|
||||
state := &BillingState{
|
||||
Capabilities: []string{"relay"},
|
||||
|
|
|
|||
|
|
@ -243,9 +243,9 @@ func normalizeDatabaseSourceState(state BillingState) BillingState {
|
|||
normalized.CommercialMigration = NormalizeCommercialMigrationStatus(normalized.CommercialMigration)
|
||||
|
||||
normalized.Limits = NormalizeMonitoredSystemLimits(normalized.Limits)
|
||||
if IsGrandfatheredRecurringV5PlanVersion(normalized.PlanVersion) {
|
||||
delete(normalized.Limits, MaxMonitoredSystemsLicenseGateKey)
|
||||
delete(normalized.Limits, "max_guests")
|
||||
if IsSelfHostedCommunityPlanVersion(normalized.PlanVersion) ||
|
||||
IsGrandfatheredRecurringV5PlanVersion(normalized.PlanVersion) {
|
||||
stripLegacyCommercialCaps(normalized.Limits)
|
||||
}
|
||||
|
||||
switch normalized.SubscriptionState {
|
||||
|
|
|
|||
|
|
@ -148,6 +148,32 @@ func TestDatabaseSourceGrandfatheredRecurringPlanStripsCappedLimits(t *testing.T
|
|||
}
|
||||
}
|
||||
|
||||
func TestDatabaseSourceSelfHostedCommunityPlanStripsLegacyCommercialCaps(t *testing.T) {
|
||||
store := &mockBillingStore{
|
||||
state: &BillingState{
|
||||
PlanVersion: "community",
|
||||
Limits: map[string]int64{
|
||||
"max_monitored_systems": 1,
|
||||
"max_guests": 5,
|
||||
"max_reports": 7,
|
||||
},
|
||||
SubscriptionState: SubStateActive,
|
||||
},
|
||||
}
|
||||
|
||||
source := NewDatabaseSource(store, "org-1", time.Hour)
|
||||
|
||||
if got := source.PlanVersion(); got != "community" {
|
||||
t.Fatalf("expected plan_version %q, got %q", "community", got)
|
||||
}
|
||||
if got := source.SubscriptionState(); got != SubStateActive {
|
||||
t.Fatalf("expected subscription_state %q, got %q", SubStateActive, got)
|
||||
}
|
||||
if got := source.Limits(); !reflect.DeepEqual(got, map[string]int64{"max_reports": 7}) {
|
||||
t.Fatalf("expected community plan to scrub legacy commercial caps, got limits %v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDatabaseSourcePreservesMissingPlanVersion(t *testing.T) {
|
||||
store := &mockBillingStore{
|
||||
state: &BillingState{
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ package licensing
|
|||
|
||||
import (
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
|
|
@ -160,6 +161,25 @@ func IsGrandfatheredRecurringV5PlanVersion(planVersion string) bool {
|
|||
}
|
||||
}
|
||||
|
||||
// IsSelfHostedCommunityPlanVersion reports whether a persisted billing-state
|
||||
// plan version denotes the uncapped self-hosted Community/free posture. This
|
||||
// is narrower than tier-based self-hosted licensing: billing-state plan labels
|
||||
// like "pro" can still carry explicit continuity limits in hosted and legacy
|
||||
// migration paths, so only community/free variants are safe to scrub here.
|
||||
func IsSelfHostedCommunityPlanVersion(planVersion string) bool {
|
||||
switch strings.ToLower(CanonicalizePlanVersion(planVersion)) {
|
||||
case "community", string(TierFree):
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func stripLegacyCommercialCaps(limits map[string]int64) {
|
||||
delete(limits, MaxMonitoredSystemsLicenseGateKey)
|
||||
delete(limits, "max_guests")
|
||||
}
|
||||
|
||||
// UnknownPlanDefaultMonitoredSystemLimit is the safe-default monitored-system limit applied when a
|
||||
// plan version is not recognized. Fail-closed: unknown plans get the smallest
|
||||
// tier limit rather than unlimited access.
|
||||
|
|
|
|||
|
|
@ -599,6 +599,35 @@ func TestIsGrandfatheredRecurringV5PlanVersion(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestIsSelfHostedCommunityPlanVersion(t *testing.T) {
|
||||
tests := []struct {
|
||||
plan string
|
||||
want bool
|
||||
}{
|
||||
{plan: "community", want: true},
|
||||
{plan: "Community", want: true},
|
||||
{plan: "free", want: true},
|
||||
{plan: "relay", want: false},
|
||||
{plan: "pro", want: false},
|
||||
{plan: "pro_plus", want: false},
|
||||
{plan: "pro_annual", want: false},
|
||||
{plan: "lifetime", want: false},
|
||||
{plan: "v5_lifetime_grandfathered", want: false},
|
||||
{plan: "cloud_starter", want: false},
|
||||
{plan: "trial", want: false},
|
||||
{plan: "pro-v2", want: false},
|
||||
{plan: "", want: false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.plan, func(t *testing.T) {
|
||||
if got := IsSelfHostedCommunityPlanVersion(tt.plan); got != tt.want {
|
||||
t.Fatalf("IsSelfHostedCommunityPlanVersion(%q) = %v, want %v", tt.plan, got, tt.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestPriceIDToPlanVersion_AllMapToKnownPlans ensures every plan version in the
|
||||
// price→plan map is recognized by LimitsForCloudPlan (fail-closed safety net).
|
||||
func TestPriceIDToPlanVersion_AllMapToKnownPlans(t *testing.T) {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue