Remove guest capacity from billing presentation

This commit is contained in:
rcourtman 2026-04-28 20:56:41 +01:00
parent 9217c065df
commit efc0f371cf
7 changed files with 25 additions and 49 deletions

View file

@ -1394,7 +1394,11 @@ when a migrated recurring v5 plan is active or in grace, the settings surface
must render plan terms and a continuity notice that makes it clear the
existing recurring price remains in force until cancellation, while
self-hosted monitoring and child-resource volume stay uncapped under the
current v6 policy.
current v6 policy. The same plan summary must not render a separate
`Guest Capacity`, child-resource allowance, or equivalent volume-cap row for
uncapped/grandfathered self-hosted plans; the customer-facing continuity story
is existing-price protection plus uncapped self-hosted monitoring and
child-resource volume, not a new paid guest-capacity benefit.
The self-hosted commercial presentation on that same surface is now locked to
the no-cap monitored-system model as well. `ProLicensePanel.tsx`,
`CommercialBillingSections.tsx`, and

View file

@ -375,7 +375,7 @@ describe('ProLicensePanel', () => {
),
).toBeInTheDocument();
expect(screen.queryByText('Included Monitored Systems')).not.toBeInTheDocument();
expect(screen.getByText('Guest Capacity')).toBeInTheDocument();
expect(screen.queryByText('Guest Capacity')).not.toBeInTheDocument();
expect(screen.getByText('Core Monitoring')).toBeInTheDocument();
expect(screen.queryByText('Monitored Systems')).not.toBeInTheDocument();
expect(screen.queryByText('Capacity Status')).not.toBeInTheDocument();
@ -423,17 +423,17 @@ describe('ProLicensePanel', () => {
expect(screen.getByText('Grandfathered price')).toBeInTheDocument();
expect(
screen.getAllByText(
/keeps its existing recurring price and uncapped monitored-system and guest capacity/i,
/keeps its existing recurring price until/i,
).length,
).toBeGreaterThan(0);
expect(
screen.queryByText(/keeps its existing recurring price and uncapped guest capacity/i),
).not.toBeInTheDocument();
expect(
screen.getByText(
/keeps its existing recurring price and uncapped monitored-system and guest capacity until you cancel/i,
),
).toBeInTheDocument();
screen.getAllByText(
/self-hosted monitoring and child-resource volume remain uncapped under the current v6 policy/i,
).length,
).toBeGreaterThan(0);
expect(
within(screen.getByText('Core Monitoring').parentElement as HTMLElement).getByText(
'Unlimited',
@ -441,7 +441,7 @@ describe('ProLicensePanel', () => {
).toBeInTheDocument();
expect(screen.queryByText('Capacity Status')).not.toBeInTheDocument();
expect(screen.queryByText('Included Monitored Systems')).not.toBeInTheDocument();
expect(screen.getByText('Guest Capacity')).toBeInTheDocument();
expect(screen.queryByText('Guest Capacity')).not.toBeInTheDocument();
expect(screen.queryByText('Monitored-system policy')).not.toBeInTheDocument();
expect(screen.queryByRole('tab', { name: 'Usage' })).not.toBeInTheDocument();
expect(screen.getAllByText('Unlimited').length).toBeGreaterThan(0);

View file

@ -162,9 +162,6 @@ export function useProLicensePanelState() {
const monitoredSystemCapacity = createMemo(() =>
uncappedGrandfatheredPlan() ? undefined : entitlements()?.monitored_system_capacity,
);
const guestLimitStatus = createMemo(() =>
uncappedGrandfatheredPlan() ? undefined : limitStatus('max_guests'),
);
const displayableMonitoredSystemContinuity = createMemo(() =>
getDisplayableMonitoredSystemContinuity({
continuity: monitoredSystemContinuity(),
@ -507,17 +504,6 @@ export function useProLicensePanelState() {
),
);
const showGuestCapacity = createMemo(() => {
if (currentRetailPlanDefinition()) {
return false;
}
if (uncappedGrandfatheredPlan()) {
return true;
}
const guestLimit = guestLimitStatus()?.limit;
return typeof guestLimit === 'number' && guestLimit > 0;
});
const commercialPlanModel = createMemo(() =>
buildSelfHostedCommercialPlanModel({
licensedEmail: entitlements()?.licensed_email,
@ -533,12 +519,7 @@ export function useProLicensePanelState() {
monitoredSystemLimitStatus()!.limit > 0
? monitoredSystemLimitStatus()!.limit
: 'Unlimited',
guestCapacity:
typeof guestLimitStatus()?.limit === 'number' && guestLimitStatus()!.limit > 0
? guestLimitStatus()!.limit
: 'Unlimited',
retailPlanDefinition: currentRetailPlanDefinition(),
showGuestCapacity: showGuestCapacity(),
monitoredSystemContinuity: displayableMonitoredSystemContinuity() ?? null,
continuityCapturedAt: continuityCapturedAt(),
}),

View file

@ -13,7 +13,6 @@ const createBaseInput = () => ({
monitoredSystemsSummary: 'Unlimited',
capacityStatusSummary: 'Unlimited',
maxMonitoredSystems: 'Unlimited' as const,
guestCapacity: 'Unlimited' as const,
});
describe('commercialBillingModel', () => {
@ -21,7 +20,6 @@ describe('commercialBillingModel', () => {
const model = buildSelfHostedCommercialPlanModel({
...createBaseInput(),
retailPlanDefinition: SELF_HOSTED_PLAN_BY_TIER.pro,
showGuestCapacity: false,
});
expect(model.summary).toEqual([
@ -38,20 +36,19 @@ describe('commercialBillingModel', () => {
]);
});
it('keeps guest capacity visible for uncapped grandfathered continuity states', () => {
it('keeps uncapped grandfathered continuity focused on core monitoring', () => {
const model = buildSelfHostedCommercialPlanModel({
...createBaseInput(),
planTerms: 'V5 Pro Monthly (Grandfathered)',
retailPlanDefinition: null,
showGuestCapacity: true,
});
expect(model.summary).toEqual([
{ label: 'Core Monitoring', value: 'Unlimited' },
{ label: 'Guest Capacity', value: 'Unlimited' },
{ label: 'Plan Status', value: 'Active' },
]);
expect(model.details.map((item) => item.label)).not.toContain('Included Monitored Systems');
expect(model.summary.map((item) => item.label)).not.toContain('Guest Capacity');
});
it('keeps bounded monitored-system details on legacy fallback paths', () => {
@ -60,9 +57,7 @@ describe('commercialBillingModel', () => {
monitoredSystemsSummary: '7 monitored systems',
capacityStatusSummary: '3 remaining',
maxMonitoredSystems: 10,
guestCapacity: 50,
retailPlanDefinition: null,
showGuestCapacity: true,
});
expect(model.summary).toEqual([

View file

@ -193,8 +193,14 @@ describe('licensePresentation', () => {
).toMatchObject({
title: 'Grandfathered v5 pricing',
tone: expect.stringContaining('green'),
body: expect.stringContaining('uncapped monitored-system and guest capacity'),
body: expect.stringContaining('keeps its existing recurring price until you cancel'),
});
expect(
getGrandfatheredPriceContinuityNotice('v5_pro_monthly_grandfathered', 'active')?.body,
).toContain('Self-hosted monitoring and child-resource volume remain uncapped');
expect(
getGrandfatheredPriceContinuityNotice('v5_pro_monthly_grandfathered', 'active')?.body,
).not.toContain('guest capacity');
expect(
getGrandfatheredPriceContinuityNotice('v5_pro_annual_grandfathered', 'grace'),
).toMatchObject({
@ -343,7 +349,7 @@ describe('licensePresentation', () => {
],
supplementalBadges: ['Grandfathered price'],
supplementalSummary:
'This migrated v5 subscription keeps its existing recurring price and uncapped monitored-system and guest capacity until cancellation.',
'This migrated v5 subscription keeps its existing recurring price until cancellation. Self-hosted monitoring and child-resource volume remain uncapped under the current v6 policy.',
});
});

View file

@ -33,12 +33,10 @@ export interface SelfHostedCommercialModelInput {
monitoredSystemsSummary: string | number;
capacityStatusSummary: string | number;
maxMonitoredSystems: string | number;
guestCapacity: string | number;
retailPlanDefinition?: Pick<
SelfHostedPlanDefinition,
'billingExtrasSummary' | 'metricHistoryDays'
> | null;
showGuestCapacity?: boolean;
monitoredSystemContinuity?: MonitoredSystemContinuityStatus | null;
continuityCapturedAt?: string;
}
@ -123,14 +121,6 @@ export const buildSelfHostedCommercialPlanModel = (
label: 'Core Monitoring',
value: 'Unlimited',
},
...(input.showGuestCapacity
? [
{
label: 'Guest Capacity',
value: input.guestCapacity,
},
]
: []),
{
label: 'Plan Status',
value: input.statusLabel,

View file

@ -251,7 +251,7 @@ export const getGrandfatheredPriceContinuityNotice = (
return {
tone: 'border-green-200 dark:border-green-900 bg-green-50 dark:bg-green-900 text-green-900 dark:text-green-100',
title: 'Grandfathered v5 pricing',
body: 'This migrated v5 Pro subscription keeps its existing recurring price and uncapped monitored-system and guest capacity until you cancel. If you cancel and return later, current v6 pricing applies; public self-hosted monitoring remains unlimited.',
body: 'This migrated v5 Pro subscription keeps its existing recurring price until you cancel. Self-hosted monitoring and child-resource volume remain uncapped under the current v6 policy. If you cancel and return later, current v6 pricing applies for paid features.',
};
};
@ -481,12 +481,12 @@ export const getSelfHostedCurrentPlanPresentation = ({
) {
supplementalBadges.push('Grandfathered price');
supplementalDetails.push(
'This migrated v5 subscription keeps its existing recurring price and uncapped monitored-system and guest capacity until cancellation.',
'This migrated v5 subscription keeps its existing recurring price until cancellation. Self-hosted monitoring and child-resource volume remain uncapped under the current v6 policy.',
);
} else if (hasUncappedContinuity && current.is_lifetime) {
supplementalBadges.push('Grandfathered lifetime');
supplementalDetails.push(
'This migrated lifetime install keeps uncapped monitored-system and guest capacity continuity.',
'This migrated lifetime install remains valid permanently, with uncapped self-hosted monitoring and child-resource volume.',
);
}