Skip onboarding overflow bonus on uncapped plans

The free-tier onboarding overflow adds +1 to MaxMonitoredSystems for 14
days after initial setup. Once rc.2 made self-hosted core monitoring
uncapped (MaxMonitoredSystems = 0 on Free), the bonus math silently
converted "unlimited" into a hard cap of 1 — the UI then surfaced
"Over plan by N. N monitored, 1 included." on healthy installs.

Guard the addition on limit > 0 at all three call sites (ledger path,
commercial entitlement payload, runtime capabilities payload) so the
bonus only extends plans that actually have a cap.

Refs #1429
This commit is contained in:
rcourtman 2026-04-17 12:16:03 +01:00
parent 80d9588d49
commit f2d5892aa5
3 changed files with 39 additions and 7 deletions

View file

@ -85,8 +85,10 @@ func maxMonitoredSystemsLimitForContext(ctx context.Context) int {
limit := status.MaxMonitoredSystems
// Apply onboarding overflow bonus for free-tier orgs.
if status.Tier == licenseTierFreeValue {
// Apply onboarding overflow bonus for free-tier orgs. The bonus only
// makes sense on plans that actually have a cap — adding +1 to an
// uncapped limit (0) would convert "unlimited" into a cap of 1.
if status.Tier == licenseTierFreeValue && limit > 0 {
var overflowGrantedAt *int64
// Try evaluator first (covers hosted path with DatabaseSource).

View file

@ -550,6 +550,30 @@ func TestVMwareAdmissionEnforcementSkipsUsageAndInventoryForDisabledConnections(
)
}
func TestOnboardingOverflowBonusSkipsUncappedPlans(t *testing.T) {
enforcement := readAPIPackageFile(t, "monitored_system_limit_enforcement.go")
overflowSegment := requireSourceSegment(
t,
enforcement,
"func maxMonitoredSystemsLimitForContext",
"\n}\n",
)
requireSnippetBefore(
t,
overflowSegment,
"limit > 0",
"overflowBonusFromLicensing",
)
entitlements := readAPIPackageFile(t, "subscription_entitlements.go")
requireSnippetCountAtLeast(
t,
entitlements,
"if status.MaxMonitoredSystems > 0 {",
2,
)
}
func TestMonitoredSystemCountNilMonitor(t *testing.T) {
got := monitoredSystemCount(nil)
if got != 0 {

View file

@ -40,11 +40,15 @@ func (h *LicenseHandlers) buildCommercialEntitlementPayload(
usage := h.entitlementUsageSnapshot(ctx)
trialEndsAtUnix := trialEndsAtUnixFromService(svc)
// Onboarding overflow: +1 agent for 14 days on free tier.
// Onboarding overflow: +1 agent for 14 days on free tier. Only applies
// on plans that have an actual cap — adding the bonus to an uncapped
// limit (0) would surface as a cap of 1 to the UI.
overflowGrantedAt := h.ensureOnboardingOverflow(ctx, status.Tier)
now := time.Now()
if bonus := overflowBonusFromLicensing(status.Tier, overflowGrantedAt, now); bonus > 0 {
status.MaxMonitoredSystems += bonus
if status.MaxMonitoredSystems > 0 {
if bonus := overflowBonusFromLicensing(status.Tier, overflowGrantedAt, now); bonus > 0 {
status.MaxMonitoredSystems += bonus
}
}
payload := buildEntitlementPayloadWithUsage(status, svc.SubscriptionState(), usage, trialEndsAtUnix)
@ -123,8 +127,10 @@ func (h *LicenseHandlers) HandleRuntimeCapabilities(w http.ResponseWriter, r *ht
overflowGrantedAt := h.ensureOnboardingOverflow(r.Context(), status.Tier)
now := time.Now()
if bonus := overflowBonusFromLicensing(status.Tier, overflowGrantedAt, now); bonus > 0 {
status.MaxMonitoredSystems += bonus
if status.MaxMonitoredSystems > 0 {
if bonus := overflowBonusFromLicensing(status.Tier, overflowGrantedAt, now); bonus > 0 {
status.MaxMonitoredSystems += bonus
}
}
payload := buildRuntimeCapabilitiesPayloadWithUsage(status, svc.SubscriptionState(), usage)