Decouple monitored-system continuity capture from billing reads

This commit is contained in:
rcourtman
2026-04-08 18:32:43 +01:00
parent 87bec03f58
commit 81f64edace
27 changed files with 1035 additions and 155 deletions
+28 -7
View File
@@ -82,6 +82,10 @@ type EntitlementPayload struct {
// CommercialMigration reports unresolved paid-license migration work entering
// from v5-era commercial state.
CommercialMigration *CommercialMigrationStatus `json:"commercial_migration,omitempty"`
// MonitoredSystemContinuity exposes migrated monitored-system continuity
// state for billing and support-grade plan-limit presentation.
MonitoredSystemContinuity *MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"`
}
// CommercialPosturePayload is the canonical non-billing commercial contract
@@ -158,6 +162,10 @@ type LimitStatus struct {
// usage value rather than an unavailable best-effort fallback.
CurrentAvailable *bool `json:"current_available,omitempty"`
// CurrentUnavailableReason explains why Current is unavailable when
// CurrentAvailable is false.
CurrentUnavailableReason string `json:"current_unavailable_reason,omitempty"`
// State describes the over-limit UX state.
// Values: "ok", "warning", "enforced"
State string `json:"state"`
@@ -186,11 +194,12 @@ func (c LegacyConnectionCounts) Total() int64 {
}
type EntitlementUsageSnapshot struct {
MonitoredSystems int64
MonitoredSystemsAvailable bool
Nodes int64 // legacy compatibility alias for monitored systems
Guests int64
LegacyConnections LegacyConnectionCounts
MonitoredSystems int64
MonitoredSystemsAvailable bool
MonitoredSystemsUnavailableReason string
Nodes int64 // legacy compatibility alias for monitored systems
Guests int64
LegacyConnections LegacyConnectionCounts
}
func (s EntitlementUsageSnapshot) monitoredSystemCount() int64 {
@@ -207,6 +216,10 @@ func (s EntitlementUsageSnapshot) monitoredSystemCountAvailable() bool {
return s.MonitoredSystems > 0 || s.Nodes > 0
}
func (s EntitlementUsageSnapshot) monitoredSystemCountUnavailableReason() string {
return s.MonitoredSystemsUnavailableReason
}
// BuildEntitlementPayload constructs the normalized payload from LicenseStatus.
func BuildEntitlementPayload(status *LicenseStatus, subscriptionState string) EntitlementPayload {
return BuildEntitlementPayloadWithUsage(status, subscriptionState, EntitlementUsageSnapshot{}, nil)
@@ -334,6 +347,10 @@ func BuildEntitlementPayloadWithUsage(
LegacyConnections: usage.LegacyConnections,
HasMigrationGap: false,
}
if status.MonitoredSystemContinuity != nil {
continuity := *status.MonitoredSystemContinuity
payload.MonitoredSystemContinuity = &continuity
}
if payload.Capabilities == nil {
payload.Capabilities = []string{}
@@ -363,13 +380,17 @@ func BuildEntitlementPayloadWithUsage(
// Build limits.
if status.MaxMonitoredSystems > 0 {
currentSystems := usage.monitoredSystemCount()
payload.Limits = append(payload.Limits, LimitStatus{
limit := LimitStatus{
Key: MaxMonitoredSystemsLicenseGateKey,
Limit: int64(status.MaxMonitoredSystems),
Current: currentSystems,
CurrentAvailable: boolPointer(usage.monitoredSystemCountAvailable()),
State: LimitState(currentSystems, int64(status.MaxMonitoredSystems)),
})
}
if !usage.monitoredSystemCountAvailable() {
limit.CurrentUnavailableReason = usage.monitoredSystemCountUnavailableReason()
}
payload.Limits = append(payload.Limits, limit)
}
if status.MaxGuests > 0 {
payload.Limits = append(payload.Limits, LimitStatus{
+43 -1
View File
@@ -156,7 +156,9 @@ func TestBuildEntitlementPayloadWithUsage_MonitoredSystemUsageUnavailable(t *tes
MaxMonitoredSystems: 50,
}
payload := BuildEntitlementPayloadWithUsage(status, "", EntitlementUsageSnapshot{}, nil)
payload := BuildEntitlementPayloadWithUsage(status, "", EntitlementUsageSnapshot{
MonitoredSystemsUnavailableReason: "supplemental_inventory_unsettled",
}, nil)
if len(payload.Limits) != 1 {
t.Fatalf("expected one limit, got %d", len(payload.Limits))
}
@@ -166,6 +168,46 @@ func TestBuildEntitlementPayloadWithUsage_MonitoredSystemUsageUnavailable(t *tes
if payload.Limits[0].CurrentAvailable == nil || *payload.Limits[0].CurrentAvailable {
t.Fatalf("expected unresolved current availability to be false, got %+v", payload.Limits[0].CurrentAvailable)
}
if payload.Limits[0].CurrentUnavailableReason != "supplemental_inventory_unsettled" {
t.Fatalf("CurrentUnavailableReason=%q, want %q", payload.Limits[0].CurrentUnavailableReason, "supplemental_inventory_unsettled")
}
}
func TestBuildEntitlementPayloadWithUsage_CopiesMonitoredSystemContinuity(t *testing.T) {
status := &LicenseStatus{
Valid: true,
Tier: TierPro,
Features: append([]string(nil), TierFeatures[TierPro]...),
MaxMonitoredSystems: 23,
MonitoredSystemContinuity: &MonitoredSystemContinuityStatus{
PlanLimit: 10,
GrandfatheredFloor: 23,
EffectiveLimit: 23,
CapturePending: false,
CapturedAt: 123,
},
}
payload := BuildEntitlementPayloadWithUsage(status, "", EntitlementUsageSnapshot{
MonitoredSystems: 23,
MonitoredSystemsAvailable: true,
}, nil)
if payload.MonitoredSystemContinuity == nil {
t.Fatal("expected monitored-system continuity to be copied")
}
if payload.MonitoredSystemContinuity.PlanLimit != 10 {
t.Fatalf("PlanLimit=%d, want %d", payload.MonitoredSystemContinuity.PlanLimit, 10)
}
if payload.MonitoredSystemContinuity.EffectiveLimit != 23 {
t.Fatalf("EffectiveLimit=%d, want %d", payload.MonitoredSystemContinuity.EffectiveLimit, 23)
}
if payload.MonitoredSystemContinuity.GrandfatheredFloor != 23 {
t.Fatalf("GrandfatheredFloor=%d, want %d", payload.MonitoredSystemContinuity.GrandfatheredFloor, 23)
}
if payload.MonitoredSystemContinuity.CapturePending {
t.Fatal("expected continuity capture to be settled")
}
}
func TestBuildEntitlementPayload_CopiesStatusDisplayFields(t *testing.T) {
+23 -12
View File
@@ -194,16 +194,27 @@ const (
// LicenseStatus is the JSON response for license status API.
type LicenseStatus struct {
Valid bool `json:"valid"`
Tier Tier `json:"tier"`
PlanVersion string `json:"plan_version,omitempty"`
Email string `json:"email,omitempty"`
ExpiresAt *string `json:"expires_at,omitempty"`
IsLifetime bool `json:"is_lifetime"`
DaysRemaining int `json:"days_remaining"`
Features []string `json:"features"`
MaxMonitoredSystems int `json:"max_monitored_systems,omitempty"`
MaxGuests int `json:"max_guests,omitempty"`
InGracePeriod bool `json:"in_grace_period,omitempty"`
GracePeriodEnd *string `json:"grace_period_end,omitempty"`
Valid bool `json:"valid"`
Tier Tier `json:"tier"`
PlanVersion string `json:"plan_version,omitempty"`
Email string `json:"email,omitempty"`
ExpiresAt *string `json:"expires_at,omitempty"`
IsLifetime bool `json:"is_lifetime"`
DaysRemaining int `json:"days_remaining"`
Features []string `json:"features"`
MaxMonitoredSystems int `json:"max_monitored_systems,omitempty"`
MaxGuests int `json:"max_guests,omitempty"`
InGracePeriod bool `json:"in_grace_period,omitempty"`
GracePeriodEnd *string `json:"grace_period_end,omitempty"`
MonitoredSystemContinuity *MonitoredSystemContinuityStatus `json:"monitored_system_continuity,omitempty"`
}
// MonitoredSystemContinuityStatus describes the effective monitored-system
// limit continuity applied to a migrated legacy installation.
type MonitoredSystemContinuityStatus struct {
PlanLimit int `json:"plan_limit"`
GrandfatheredFloor int `json:"grandfathered_floor,omitempty"`
EffectiveLimit int `json:"effective_limit"`
CapturePending bool `json:"capture_pending"`
CapturedAt int64 `json:"captured_at,omitempty"`
}
+42
View File
@@ -230,6 +230,48 @@ func TestClaims_EffectiveLimitsPreservesNonCloudPlanLimits(t *testing.T) {
}
}
func TestLicenseStatusJSON_EncodesMonitoredSystemContinuity(t *testing.T) {
status := LicenseStatus{
Valid: true,
Tier: TierPro,
MaxMonitoredSystems: 23,
MonitoredSystemContinuity: &MonitoredSystemContinuityStatus{
PlanLimit: 10,
GrandfatheredFloor: 23,
EffectiveLimit: 23,
CapturePending: false,
CapturedAt: 123,
},
}
data, err := json.Marshal(status)
if err != nil {
t.Fatalf("marshal status: %v", err)
}
var decoded map[string]any
if err := json.Unmarshal(data, &decoded); err != nil {
t.Fatalf("decode status json: %v", err)
}
continuity, ok := decoded["monitored_system_continuity"].(map[string]any)
if !ok {
t.Fatalf("expected monitored_system_continuity object, got %#v", decoded["monitored_system_continuity"])
}
if got := continuity["plan_limit"]; got != float64(10) {
t.Fatalf("plan_limit=%v, want %v", got, float64(10))
}
if got := continuity["effective_limit"]; got != float64(23) {
t.Fatalf("effective_limit=%v, want %v", got, float64(23))
}
if got := continuity["grandfathered_floor"]; got != float64(23) {
t.Fatalf("grandfathered_floor=%v, want %v", got, float64(23))
}
if got := continuity["capture_pending"]; got != false {
t.Fatalf("capture_pending=%v, want false", got)
}
}
func TestClaims_EffectiveLimitsMissingCloudPlanFailsClosed(t *testing.T) {
claims := Claims{
Tier: TierCloud,
+55
View File
@@ -469,6 +469,59 @@ func (s *Service) CaptureLegacyMonitoredSystemGrandfatherFloor(count int) error
return nil
}
func (s *Service) needsLegacyMonitoredSystemCaptureLocked() bool {
if s == nil || s.activationState == nil {
return false
}
return normalizeActivationContinuity(s.activationState.Continuity).needsLegacyMonitoredSystemCapture()
}
// NeedsLegacyMonitoredSystemCapture reports whether a migrated activation is
// still waiting for its one-time monitored-system continuity capture.
func (s *Service) NeedsLegacyMonitoredSystemCapture() bool {
if s == nil {
return false
}
s.mu.Lock()
defer s.mu.Unlock()
return s.needsLegacyMonitoredSystemCaptureLocked()
}
func (s *Service) monitoredSystemContinuityStatusLocked() *MonitoredSystemContinuityStatus {
if s == nil || s.activationState == nil {
return nil
}
continuity := normalizeActivationContinuity(s.activationState.Continuity)
if !continuity.LegacyMigration {
return nil
}
planLimit := 0
if gc, err := verifyAndParseGrantJWT(s.activationState.GrantJWT); err == nil && gc != nil {
planLimit = gc.MaxMonitoredSystems
}
effectiveLimit := planLimit
if s.license != nil {
effectiveLimit = monitoredSystemLimitFromClaims(s.license.Claims)
}
if effectiveLimit <= 0 {
effectiveLimit = planLimit
}
status := &MonitoredSystemContinuityStatus{
PlanLimit: planLimit,
EffectiveLimit: effectiveLimit,
CapturePending: continuity.needsLegacyMonitoredSystemCapture(),
CapturedAt: continuity.GrandfatheredMonitoredSystemsCapturedAt,
}
if continuity.GrandfatheredMaxMonitoredSystems > 0 {
status.GrandfatheredFloor = continuity.GrandfatheredMaxMonitoredSystems
}
return status
}
// Clear removes the current license.
// If an activation-key license is present, it also stops the refresh loop and clears the state.
func (s *Service) Clear() {
@@ -704,6 +757,7 @@ func (s *Service) Status() *LicenseStatus {
if isDemoMode() || isDevMode() {
status.Features = devModeFeatures()
}
status.MonitoredSystemContinuity = s.monitoredSystemContinuityStatusLocked()
return status
}
@@ -720,6 +774,7 @@ func (s *Service) Status() *LicenseStatus {
if maxGuests, ok := s.license.Claims.EffectiveLimits()["max_guests"]; ok {
status.MaxGuests = safeIntFromInt64(maxGuests)
}
status.MonitoredSystemContinuity = s.monitoredSystemContinuityStatusLocked()
// Apply the tier default monitored-system limit when claims don't specify one.
// For recognized tiers, use their defined limit (0 = unlimited for Cloud/MSP/Enterprise).
+86
View File
@@ -266,6 +266,92 @@ func TestServiceCaptureLegacyMonitoredSystemGrandfatherFloorPersistsAndUpdatesSt
}
}
func TestServiceStatus_ExposesMonitoredSystemContinuity(t *testing.T) {
setupTestPublicKey(t)
grantJWT := makeTestGrantJWT(t, &GrantClaims{
LicenseID: "lic_continuity_status",
Tier: "pro",
PlanKey: "v5_pro_monthly_grandfathered",
State: "active",
MaxMonitoredSystems: 10,
IssuedAt: time.Now().Unix(),
ExpiresAt: time.Now().Add(72 * time.Hour).Unix(),
})
tests := []struct {
name string
continuity ActivationContinuity
wantPlanLimit int
wantEffectiveLimit int
wantFloor int
wantCapturePending bool
wantMaxSystems int
}{
{
name: "pending capture",
continuity: ActivationContinuity{
LegacyMigration: true,
},
wantPlanLimit: 10,
wantEffectiveLimit: 10,
wantFloor: 0,
wantCapturePending: true,
wantMaxSystems: 10,
},
{
name: "captured floor",
continuity: ActivationContinuity{
LegacyMigration: true,
GrandfatheredMaxMonitoredSystems: 23,
GrandfatheredMonitoredSystemsCapturedAt: 123,
},
wantPlanLimit: 10,
wantEffectiveLimit: 23,
wantFloor: 23,
wantCapturePending: false,
wantMaxSystems: 23,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
svc := NewService()
if err := svc.RestoreActivation(&ActivationState{
InstallationID: "inst_continuity_status",
InstallationToken: "pit_live_continuity_status",
LicenseID: "lic_continuity_status",
GrantJWT: grantJWT,
GrantJTI: "grant_continuity_status",
InstanceFingerprint: "fp-continuity-status",
Continuity: tt.continuity,
}); err != nil {
t.Fatalf("RestoreActivation: %v", err)
}
status := svc.Status()
if status.MaxMonitoredSystems != tt.wantMaxSystems {
t.Fatalf("status.MaxMonitoredSystems=%d, want %d", status.MaxMonitoredSystems, tt.wantMaxSystems)
}
if status.MonitoredSystemContinuity == nil {
t.Fatal("expected monitored-system continuity in status")
}
if status.MonitoredSystemContinuity.PlanLimit != tt.wantPlanLimit {
t.Fatalf("PlanLimit=%d, want %d", status.MonitoredSystemContinuity.PlanLimit, tt.wantPlanLimit)
}
if status.MonitoredSystemContinuity.EffectiveLimit != tt.wantEffectiveLimit {
t.Fatalf("EffectiveLimit=%d, want %d", status.MonitoredSystemContinuity.EffectiveLimit, tt.wantEffectiveLimit)
}
if status.MonitoredSystemContinuity.GrandfatheredFloor != tt.wantFloor {
t.Fatalf("GrandfatheredFloor=%d, want %d", status.MonitoredSystemContinuity.GrandfatheredFloor, tt.wantFloor)
}
if status.MonitoredSystemContinuity.CapturePending != tt.wantCapturePending {
t.Fatalf("CapturePending=%v, want %v", status.MonitoredSystemContinuity.CapturePending, tt.wantCapturePending)
}
})
}
}
func TestServiceActivate_RejectsMalformedLegacyKeyOutsideDevMode(t *testing.T) {
t.Setenv("PULSE_LICENSE_DEV_MODE", "false")