Make self-hosted plan summaries entitlement-first

This commit is contained in:
rcourtman
2026-04-22 20:09:24 +01:00
parent f58840e8a8
commit 9458cdb2e5
9 changed files with 475 additions and 66 deletions
@@ -1139,7 +1139,11 @@ plan-owned rather than tier-owned: the navigation label is `Plans`, the
surface title is `Plans & Activation`, and the canonical plan state must make
the current unlocked tier plus capabilities immediately obvious so existing
paid upgrades can confirm what their new key enabled without hunting through
generic billing details.
generic billing details. That entitlement-first summary must stay tier-
specific and continuity-aware: Relay and Pulse Pro should describe the actual
paid capabilities unlocked on this instance, while grandfathered pricing or
captured monitored-system continuity must be visible in that same top summary
instead of only in secondary billing detail blocks further down the page.
That same router-owned billing contract now also includes recovery as a plan
detail state instead of a fragment alias. The canonical recovery arrival is
`/settings/system/billing/plan?details=recovery`, while
@@ -46,6 +46,8 @@ interface ProLicensePlanSectionProps {
badgeClass: string;
statusLabel: string;
unlockedFeatures: string[];
supplementalBadges: string[];
supplementalSummary?: string;
};
entitlements: {
in_grace_period?: boolean;
@@ -123,6 +125,13 @@ export const ProLicensePlanSection: Component<ProLicensePlanSectionProps> = (pro
>
{props.currentPlanSummary.statusLabel}
</span>
<For each={props.currentPlanSummary.supplementalBadges}>
{(badge) => (
<span class="px-2 py-1 text-xs font-medium rounded-full bg-surface text-base-content border border-border">
{badge}
</span>
)}
</For>
<Show when={props.entitlements?.in_grace_period}>
<span class="text-xs text-amber-700 dark:text-amber-300">
Grace until {formatDate(props.entitlements?.grace_period_end)}
@@ -131,6 +140,9 @@ export const ProLicensePlanSection: Component<ProLicensePlanSectionProps> = (pro
</div>
<p class="mt-3 text-lg font-semibold text-base-content">{props.currentPlanSummary.title}</p>
<p class="mt-1 text-sm text-muted">{props.currentPlanSummary.body}</p>
<Show when={props.currentPlanSummary.supplementalSummary}>
{(summary) => <p class="mt-2 text-xs text-muted">{summary()}</p>}
</Show>
<Show when={props.currentPlanSummary.unlockedFeatures.length > 0}>
<div class="mt-4">
<p class="text-xs uppercase tracking-wide text-muted mb-2">Unlocked on this instance</p>
@@ -223,7 +223,10 @@ describe('ProLicensePanel', () => {
'settings_self_hosted_billing_plan',
SELF_HOSTED_PRO_BILLING_PLAN_SELECTION_INTENT,
);
expect(screen.getByText(/Community keeps monitoring free/i)).toBeInTheDocument();
expect(
screen.getByText(/Core self-hosted monitoring stays free on this instance/i),
).toBeInTheDocument();
expect(screen.getByText('Unlimited self-hosted monitoring')).toBeInTheDocument();
const compareLinks = screen.getAllByRole('link', { name: 'Compare plans' });
expect(
compareLinks.some(
@@ -400,6 +403,10 @@ describe('ProLicensePanel', () => {
expect(screen.getByText(tc.expectedLabel)).toBeInTheDocument();
expect(screen.getByText('Grandfathered v5 pricing')).toBeInTheDocument();
expect(screen.getByText('Grandfathered price')).toBeInTheDocument();
expect(
screen.getByText(/keeps its existing recurring price and uncapped guest capacity/i),
).toBeInTheDocument();
expect(
screen.getByText(
/keeps its existing recurring price and uncapped monitored-system and guest capacity until you cancel/i,
@@ -443,6 +450,13 @@ describe('ProLicensePanel', () => {
expect(screen.getByText('Core Monitoring')).toBeInTheDocument();
expect(screen.getByText('Metric History')).toBeInTheDocument();
expect(screen.getByText('Included Extras')).toBeInTheDocument();
expect(screen.getByText('Current plan: Pulse Pro')).toBeInTheDocument();
expect(
screen.getByText(
'Pulse Pro is active on this instance. AI operations, advanced administration, and 90-day history are unlocked right now.',
),
).toBeInTheDocument();
expect(screen.getByText('Patrol Auto-Fix')).toBeInTheDocument();
expect(screen.getByText('90 days')).toBeInTheDocument();
expect(screen.getByText('AI operations and advanced admin')).toBeInTheDocument();
expect(screen.queryByText('Guest Capacity')).not.toBeInTheDocument();
@@ -451,6 +465,34 @@ describe('ProLicensePanel', () => {
expect(screen.queryByRole('tab', { name: 'Usage' })).not.toBeInTheDocument();
});
it('shows Relay entitlement summaries from the paid capabilities unlocked on this instance', async () => {
mockEntitlements = {
capabilities: ['relay', 'mobile_app', 'push_notifications'],
limits: [],
subscription_state: 'active',
upgrade_reasons: [],
tier: 'relay',
plan_version: 'relay_monthly',
licensed_email: 'owner@example.com',
trial_eligible: false,
};
renderPanel();
await waitFor(() => {
expect(screen.getByText('Current plan: Relay')).toBeInTheDocument();
});
expect(
screen.getByText(
'Relay is active on this instance. Remote access, mobile, push, and longer history are unlocked right now.',
),
).toBeInTheDocument();
expect(screen.getByText('Pulse Relay (Remote Access)')).toBeInTheDocument();
expect(screen.getByText('Mobile App Access')).toBeInTheDocument();
expect(screen.getByText('Push Notifications')).toBeInTheDocument();
});
it('shows continuity verification while a bounded fallback migration is still pending', async () => {
mockEntitlements = {
capabilities: ['relay'],
@@ -497,8 +539,9 @@ describe('ProLicensePanel', () => {
});
expect(
screen.getByText(/verifying the grandfathered monitored-system floor/i),
).toBeInTheDocument();
screen.getAllByText(/verifying the grandfathered monitored-system floor/i).length,
).toBeGreaterThan(0);
expect(screen.getByText('Continuity pending')).toBeInTheDocument();
expect(screen.getAllByText('Verifying…').length).toBeGreaterThan(0);
expect(screen.getAllByText('Unavailable').length).toBeGreaterThan(0);
expect(screen.getByText('Plan Monitored System Limit')).toBeInTheDocument();
@@ -605,8 +648,9 @@ describe('ProLicensePanel', () => {
});
expect(
screen.getByText(/keeps an effective monitored-system limit of 23/i),
).toBeInTheDocument();
screen.getAllByText(/keeps an effective monitored-system limit of 23/i).length,
).toBeGreaterThan(0);
expect(screen.getByText('Grandfathered floor')).toBeInTheDocument();
expect(screen.getAllByText('23 monitored systems').length).toBeGreaterThan(0);
expect(screen.getByText('Monitoring capacity')).toBeInTheDocument();
expect(
@@ -869,7 +913,9 @@ describe('ProLicensePanel', () => {
renderPanel();
expect(screen.getAllByText('Compare self-hosted plans').length).toBeGreaterThan(0);
expect(screen.getByText(/Community keeps monitoring free/i)).toBeInTheDocument();
expect(
screen.getByText(/Core self-hosted monitoring stays free on this instance/i),
).toBeInTheDocument();
const compareLinks = screen.getAllByRole('link', { name: 'Compare plans' });
expect(
compareLinks.some(
@@ -1019,6 +1065,7 @@ describe('ProLicensePanel', () => {
expect(proLicensePanelStateSource).toContain('loadCommercialPosture(true)');
expect(proLicensePanelStateSource).toContain('loadRuntimeCapabilities(true)');
expect(proLicensePanelStateSource).toContain('buildSelfHostedCommercialPlanModel');
expect(proLicensePanelStateSource).toContain('getSelfHostedCurrentPlanPresentation({');
expect(proLicensePanelStateSource).toContain('runStartProTrialAction({');
expect(proLicensePanelStateSource).not.toContain('startProTrial()');
expect(proLicensePanelStateSource).toContain("'A license or activation key is required'");
@@ -1026,6 +1073,7 @@ describe('ProLicensePanel', () => {
expect(proLicensePlanSectionSource).toContain('getNoActiveProLicenseState');
expect(proLicensePlanSectionSource).toContain('getTrialEndedProLicenseNotice');
expect(proLicensePlanSectionSource).toContain('currentPlanSummary.title');
expect(proLicensePlanSectionSource).toContain('currentPlanSummary.supplementalBadges');
expect(proLicensePlanSectionSource).toContain('Unlocked on this instance');
expect(proLicensePlanSectionSource).not.toContain('getInactiveProUpsellNotice');
expect(proLicensePlanSectionSource).not.toContain('MonitoredSystemDefinitionDisclosure');
@@ -14,10 +14,10 @@ import {
formatLicensePlanVersion,
getCommercialMigrationNotice,
getGrandfatheredPriceContinuityNotice,
getSelfHostedCurrentPlanPresentation,
getLicenseFeatureLabel,
getMonitoredSystemContinuityNotice,
getPurchaseActivationNotice,
getSelfHostedPlanLabel,
getLicenseSubscriptionStatusPresentation,
getLicenseTierLabel,
getTrialActivationNotice,
@@ -330,12 +330,6 @@ export function useProLicensePanelState() {
if (!current) return 'Unknown';
return getLicenseTierLabel(current.tier);
});
const currentPlanLabel = createMemo(() => {
const current = entitlements();
if (!current) return 'Unknown';
return getSelfHostedPlanLabel(current.tier);
});
const formattedPlanTerms = createMemo(() =>
formatLicensePlanVersion(entitlements()?.plan_version),
);
@@ -572,61 +566,14 @@ export function useProLicensePanelState() {
);
const currentPlanSummary = createMemo(() => {
const status = statusPresentation();
const unlockedFeatures = formattedFeatures();
const current = entitlements();
const planLabel = currentPlanLabel();
const normalizedState = (subscriptionState() || '').trim().toLowerCase();
if (!current) {
return {
title: 'Current plan: Unknown',
body: 'Pulse is still loading the current self-hosted entitlement for this instance.',
badgeClass: status.badgeClass,
statusLabel: status.label,
unlockedFeatures: [] as string[],
};
}
if (normalizedState === 'trial') {
return {
title: `Current plan: ${planLabel} Trial`,
body:
unlockedFeatures.length > 0
? `${planLabel} trial capabilities are active on this instance right now.`
: `${planLabel} trial activation is in progress on this instance.`,
badgeClass: status.badgeClass,
statusLabel: status.label,
unlockedFeatures,
};
}
if (unlockedFeatures.length > 0 && (normalizedState === 'active' || normalizedState === 'grace')) {
return {
title: `Current plan: ${planLabel}`,
body: `${planLabel} is active on this instance. These capabilities are unlocked right now.`,
badgeClass: status.badgeClass,
statusLabel: status.label,
unlockedFeatures,
};
}
if ((current.tier || '').trim().toLowerCase() === 'free') {
return {
title: 'Current plan: Community',
body:
'Community keeps self-hosted core monitoring free on this instance. Upgrade only when you want Relay or Pro capabilities.',
badgeClass: status.badgeClass,
statusLabel: status.label,
unlockedFeatures: [] as string[],
};
}
const summary = getSelfHostedCurrentPlanPresentation({
entitlements: entitlements(),
displayableCapabilities: formattedFeatures(),
});
return {
title: `Current plan: ${planLabel}`,
body: 'Review the plan details below to confirm what this key unlocks on this instance.',
...summary,
badgeClass: status.badgeClass,
statusLabel: status.label,
unlockedFeatures,
};
});
@@ -8,6 +8,7 @@ import {
getCommercialMigrationNotice,
getFeatureMinTierLabel,
getGrandfatheredPriceContinuityNotice,
getSelfHostedCurrentPlanPresentation,
getLicenseFeatureLabel,
getLicenseStatusLoadingState,
getLicenseSubscriptionStatusPresentation,
@@ -196,6 +197,89 @@ describe('licensePresentation', () => {
).toBeNull();
});
it('builds entitlement-first current-plan presentation for community, paid, and grandfathered installs', () => {
expect(
getSelfHostedCurrentPlanPresentation({
entitlements: {
tier: 'free',
subscription_state: 'expired',
capabilities: [],
limits: [],
upgrade_reasons: [],
},
displayableCapabilities: [],
}),
).toEqual({
title: 'Current plan: Community',
body:
'Core self-hosted monitoring stays free on this instance. Upgrade only when you want Relay convenience or Pro operations features.',
unlockedFeatures: [
'Unlimited self-hosted monitoring',
'7-day metric history',
'Pulse Patrol (BYOK)',
'Update alerts',
],
supplementalBadges: [],
supplementalSummary: '',
});
expect(
getSelfHostedCurrentPlanPresentation({
entitlements: {
tier: 'pro',
subscription_state: 'active',
capabilities: ['relay', 'mobile_app', 'ai_autofix'],
limits: [],
upgrade_reasons: [],
},
displayableCapabilities: [
'Pulse Relay (Remote Access)',
'Mobile App Access',
'Patrol Auto-Fix',
],
}),
).toEqual({
title: 'Current plan: Pulse Pro',
body:
'Pulse Pro is active on this instance. AI operations, advanced administration, and 90-day history are unlocked right now.',
unlockedFeatures: [
'Pulse Relay (Remote Access)',
'Mobile App Access',
'Patrol Auto-Fix',
],
supplementalBadges: [],
supplementalSummary: '',
});
expect(
getSelfHostedCurrentPlanPresentation({
entitlements: {
tier: 'pro',
subscription_state: 'active',
plan_version: 'v5_pro_monthly_grandfathered',
capabilities: ['relay'],
limits: [],
upgrade_reasons: [],
monitored_system_continuity: {
plan_limit: 10,
grandfathered_floor: 23,
effective_limit: 23,
capture_pending: false,
},
},
displayableCapabilities: ['Pulse Relay (Remote Access)'],
}),
).toEqual({
title: 'Current plan: Pulse Pro',
body:
'Pulse Pro is active on this instance. AI operations, advanced administration, and 90-day history are unlocked right now.',
unlockedFeatures: ['Pulse Relay (Remote Access)'],
supplementalBadges: ['Grandfathered price', 'Grandfathered floor'],
supplementalSummary:
'This migrated v5 subscription keeps its existing recurring price and uncapped guest capacity until cancellation. This installation keeps an effective monitored-system limit of 23 from the observed legacy estate.',
});
});
it('returns monitored-system continuity notices for pending verification and captured grandfathering', () => {
expect(
getMonitoredSystemContinuityNotice(
@@ -61,12 +61,41 @@ describe('selfHostedPlans', () => {
expect(SELF_HOSTED_PLAN_BY_TIER.community.billingExtrasSummary).toBe(
'Patrol, alerts, and OIDC',
);
expect(SELF_HOSTED_PLAN_BY_TIER.community.entitlementSummary).toContain(
'Core self-hosted monitoring stays free on this instance.',
);
expect(SELF_HOSTED_PLAN_BY_TIER.community.entitlementHighlights).toEqual([
'Unlimited self-hosted monitoring',
'7-day metric history',
'Pulse Patrol (BYOK)',
'Update alerts',
]);
expect(SELF_HOSTED_PLAN_BY_TIER.relay.billingExtrasSummary).toBe(
'Remote access, mobile, and push',
);
expect(SELF_HOSTED_PLAN_BY_TIER.relay.entitlementSummary).toContain(
'Remote access, mobile, push, and longer history are unlocked right now.',
);
expect(SELF_HOSTED_PLAN_BY_TIER.relay.entitlementHighlights).toEqual([
'Pulse Relay (Remote Access)',
'Mobile App Access',
'Push Notifications',
'14-day metric history',
]);
expect(SELF_HOSTED_PLAN_BY_TIER.pro.billingExtrasSummary).toBe(
'AI operations and advanced admin',
);
expect(SELF_HOSTED_PLAN_BY_TIER.pro.entitlementSummary).toContain(
'AI operations, advanced administration, and 90-day history are unlocked right now.',
);
expect(SELF_HOSTED_PLAN_BY_TIER.pro.entitlementHighlights).toEqual([
'Patrol Auto-Fix',
'Pulse Alert Analysis',
'Kubernetes Insights',
'90-day metric history',
'Role-Based Access Control (RBAC)',
'Audit Logging',
]);
expect(SELF_HOSTED_PLAN_BY_TIER.community.highlights).toEqual(
expect.arrayContaining([
'Unlimited self-hosted monitoring',
@@ -1,6 +1,7 @@
import type { BillingState, HostedOrganizationSummary } from '@/api/billingAdmin';
import type {
CommercialMigrationStatus,
LicenseCommercialEntitlements,
LicenseStatus,
MonitoredSystemContinuityStatus,
} from '@/api/license';
@@ -13,6 +14,7 @@ import {
type MonitoredSystemCapacityStatus,
type MonitoredSystemLimitUsageStatus,
} from '@/utils/monitoredSystemPresentation';
import { getSelfHostedPlanDefinitionForBillingTier } from '@/utils/selfHostedPlans';
import { titleCaseDelimitedLabel } from '@/utils/textPresentation';
const TIER_LABELS: Record<string, string> = {
@@ -106,6 +108,14 @@ export interface SelfHostedRecoveryPresentation {
legacyNotice: SelfHostedActivationNoticeCopy;
}
export interface SelfHostedCurrentPlanPresentation {
title: string;
body: string;
unlockedFeatures: string[];
supplementalBadges: string[];
supplementalSummary?: string;
}
const GRANDFATHERED_V5_PLAN_LABELS: Record<string, string> = {
v5_lifetime_grandfathered: 'V5 Lifetime Grandfathered',
v5_pro_monthly_grandfathered: 'V5 Pro Monthly (Grandfathered)',
@@ -248,6 +258,114 @@ export const getMonitoredSystemContinuityNotice = (
return null;
};
export const getSelfHostedCurrentPlanPresentation = ({
entitlements,
displayableCapabilities,
}: {
entitlements?: LicenseCommercialEntitlements | null;
displayableCapabilities: string[];
}): SelfHostedCurrentPlanPresentation => {
const current = entitlements;
if (!current) {
return {
title: 'Current plan: Unknown',
body: 'Pulse is still loading the current self-hosted entitlement for this instance.',
unlockedFeatures: [],
supplementalBadges: [],
};
}
const normalizedState = (current.subscription_state || '').trim().toLowerCase();
const normalizedTier = (current.tier || '').trim().toLowerCase();
const planLabel = getSelfHostedPlanLabel(current.tier);
const planDefinition = getSelfHostedPlanDefinitionForBillingTier(current.tier);
const fallbackHighlights = [...(planDefinition?.entitlementHighlights ?? [])];
const unlockedFeatures =
normalizedTier === 'free'
? fallbackHighlights
: displayableCapabilities.length > 0
? displayableCapabilities
: fallbackHighlights;
const supplementalBadges: string[] = [];
const supplementalDetails: string[] = [];
if (isGrandfatheredRecurringV5PlanVersion(current.plan_version)) {
supplementalBadges.push('Grandfathered price');
supplementalDetails.push(
'This migrated v5 subscription keeps its existing recurring price and uncapped guest capacity until cancellation.',
);
} else if (current.is_lifetime && isUncappedGrandfatheredPlanVersion(current.plan_version, true)) {
supplementalBadges.push('Grandfathered lifetime');
supplementalDetails.push(
'This migrated lifetime install keeps uncapped monitored-system and guest capacity continuity.',
);
}
const continuity = current.monitored_system_continuity;
if (continuity?.capture_pending) {
supplementalBadges.push('Continuity pending');
supplementalDetails.push(
'Pulse is still verifying the grandfathered monitored-system floor for this migrated v5 installation.',
);
} else if (
continuity &&
typeof continuity.grandfathered_floor === 'number' &&
continuity.grandfathered_floor > 0 &&
continuity.effective_limit > continuity.plan_limit
) {
supplementalBadges.push('Grandfathered floor');
supplementalDetails.push(
`This installation keeps an effective monitored-system limit of ${continuity.effective_limit} from the observed legacy estate.`,
);
}
if (normalizedState === 'trial') {
return {
title: `Current plan: ${planLabel} Trial`,
body:
unlockedFeatures.length > 0
? `${planLabel} trial capabilities are active on this instance right now.`
: `${planLabel} trial activation is in progress on this instance.`,
unlockedFeatures,
supplementalBadges,
supplementalSummary: supplementalDetails.join(' '),
};
}
if (normalizedTier === 'free') {
return {
title: 'Current plan: Community',
body:
planDefinition?.entitlementSummary ||
'Community keeps self-hosted core monitoring free on this instance. Upgrade only when you want Relay or Pro capabilities.',
unlockedFeatures,
supplementalBadges,
supplementalSummary: supplementalDetails.join(' '),
};
}
if (normalizedState === 'active' || normalizedState === 'grace') {
return {
title: `Current plan: ${planLabel}`,
body:
planDefinition?.entitlementSummary ||
`${planLabel} is active on this instance. These capabilities are unlocked right now.`,
unlockedFeatures,
supplementalBadges,
supplementalSummary: supplementalDetails.join(' '),
};
}
return {
title: `Current plan: ${planLabel}`,
body: 'Review the plan details below to confirm what this key unlocks on this instance.',
unlockedFeatures,
supplementalBadges,
supplementalSummary: supplementalDetails.join(' '),
};
};
export const getCommercialMigrationActionText = (action?: string): string => {
switch (action) {
case 'retry_activation':
@@ -7,6 +7,8 @@ export interface SelfHostedPlanDefinition {
subline: string;
metricHistoryDays: number;
billingExtrasSummary: string;
entitlementSummary: string;
entitlementHighlights: readonly string[];
highlights: readonly string[];
}
@@ -46,6 +48,14 @@ export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] =
subline: 'Unlimited self-hosted monitoring',
metricHistoryDays: 7,
billingExtrasSummary: 'Patrol, alerts, and OIDC',
entitlementSummary:
'Core self-hosted monitoring stays free on this instance. Upgrade only when you want Relay convenience or Pro operations features.',
entitlementHighlights: [
'Unlimited self-hosted monitoring',
'7-day metric history',
'Pulse Patrol (BYOK)',
'Update alerts',
],
highlights: [
'Real-time monitoring',
'Unlimited self-hosted monitoring',
@@ -64,6 +74,14 @@ export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] =
subline: 'or $39/year',
metricHistoryDays: 14,
billingExtrasSummary: 'Remote access, mobile, and push',
entitlementSummary:
'Relay is active on this instance. Remote access, mobile, push, and longer history are unlocked right now.',
entitlementHighlights: [
'Pulse Relay (Remote Access)',
'Mobile App Access',
'Push Notifications',
'14-day metric history',
],
highlights: [
'Everything in Community',
'Remote access via Relay',
@@ -80,6 +98,16 @@ export const SELF_HOSTED_PLAN_DEFINITIONS: readonly SelfHostedPlanDefinition[] =
subline: 'or $79/year',
metricHistoryDays: 90,
billingExtrasSummary: 'AI operations and advanced admin',
entitlementSummary:
'Pulse Pro is active on this instance. AI operations, advanced administration, and 90-day history are unlocked right now.',
entitlementHighlights: [
'Patrol Auto-Fix',
'Pulse Alert Analysis',
'Kubernetes Insights',
'90-day metric history',
'Role-Based Access Control (RBAC)',
'Audit Logging',
],
highlights: [
'Everything in Relay',
'Patrol Auto-Fix & investigation',
@@ -0,0 +1,139 @@
import { expect, test, type Page } from '@playwright/test';
import { ensureAuthenticated } from './helpers';
const PRO_PLAN_ENTITLEMENTS = {
capabilities: [
'relay',
'mobile_app',
'push_notifications',
'ai_patrol',
'ai_autofix',
'ai_alerts',
'kubernetes_ai',
'rbac',
'audit_logging',
],
limits: [{ key: 'max_monitored_systems', limit: 23, current: 23, state: 'enforced' }],
monitored_system_capacity: {
mode: 'at_limit_blocking_new',
urgency: 'enforced',
current: 23,
limit: 23,
current_available: true,
available_slots: 0,
overage: 0,
reason: 'limit_reached',
blocks_new_systems: true,
existing_monitoring_continues: true,
},
monitored_system_continuity: {
plan_limit: 10,
grandfathered_floor: 23,
effective_limit: 23,
capture_pending: false,
captured_at: 1_768_000_000,
},
subscription_state: 'active',
upgrade_reasons: [],
tier: 'pro',
plan_version: 'v5_pro_monthly_grandfathered',
licensed_email: 'owner@example.com',
trial_eligible: false,
hosted_mode: false,
valid: true,
is_lifetime: false,
days_remaining: 30,
in_grace_period: false,
grace_period_end: null,
max_history_days: 90,
};
const PRO_PLAN_COMMERCIAL_POSTURE = {
subscription_state: 'active',
upgrade_reasons: [],
tier: 'pro',
trial_eligible: false,
monitored_system_capacity: PRO_PLAN_ENTITLEMENTS.monitored_system_capacity,
monitored_system_continuity: PRO_PLAN_ENTITLEMENTS.monitored_system_continuity,
has_migration_gap: false,
};
const PRO_PLAN_RUNTIME_CAPABILITIES = {
capabilities: PRO_PLAN_ENTITLEMENTS.capabilities,
limits: PRO_PLAN_ENTITLEMENTS.limits,
monitored_system_capacity: PRO_PLAN_ENTITLEMENTS.monitored_system_capacity,
hosted_mode: false,
max_history_days: 90,
};
async function stubSelfHostedPlanEndpoints(page: Page) {
await page.route('**/api/license/runtime-capabilities', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(PRO_PLAN_RUNTIME_CAPABILITIES),
});
});
await page.route('**/api/license/entitlements', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(PRO_PLAN_ENTITLEMENTS),
});
});
await page.route('**/api/license/commercial-posture', async (route) => {
await route.fulfill({
status: 200,
contentType: 'application/json',
body: JSON.stringify(PRO_PLAN_COMMERCIAL_POSTURE),
});
});
}
test.describe('Self-hosted plans entitlement summary', () => {
test('shows paid Pulse Pro entitlements and continuity at the top of Plans', async ({
page,
}, testInfo) => {
test.skip(testInfo.project.name.startsWith('mobile-'), 'Desktop-only plans coverage');
await stubSelfHostedPlanEndpoints(page);
await ensureAuthenticated(page);
await page.goto('/settings/system/billing/plan', { waitUntil: 'domcontentloaded' });
await expect(
page.locator('[aria-label="Settings navigation"]').getByText('Plans', { exact: true }),
).toBeVisible();
await expect(page.getByRole('heading', { name: 'Plans & Activation' }).first()).toBeVisible();
const currentPlanCard = page
.locator('div.rounded-md.border.border-border.bg-surface-alt.p-4')
.filter({ has: page.getByText('Current plan: Pulse Pro') })
.first();
await expect(currentPlanCard.getByText('Current plan: Pulse Pro')).toBeVisible();
await expect(
currentPlanCard.getByText(
'Pulse Pro is active on this instance. AI operations, advanced administration, and 90-day history are unlocked right now.',
),
).toBeVisible();
await expect(currentPlanCard.getByText('Grandfathered price')).toBeVisible();
await expect(
currentPlanCard
.locator('span')
.filter({ hasText: 'Grandfathered floor' })
.first(),
).toBeVisible();
await expect(
currentPlanCard.getByText(
/existing recurring price and uncapped guest capacity until cancellation/i,
),
).toBeVisible();
await expect(currentPlanCard.getByText(/effective monitored-system limit of 23/i)).toBeVisible();
await expect(currentPlanCard.getByText('Pulse Relay (Remote Access)')).toBeVisible();
await expect(currentPlanCard.getByText('Mobile App Access')).toBeVisible();
await expect(currentPlanCard.getByText('Patrol Auto-Fix')).toBeVisible();
await expect(currentPlanCard.getByText('Pulse Alert Analysis')).toBeVisible();
});
});