mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-05-20 01:01:20 +00:00
Protect grandfathered Pro plan presentation
This commit is contained in:
parent
430f0d3fa2
commit
68eefb2b5f
7 changed files with 242 additions and 50 deletions
|
|
@ -143,13 +143,9 @@ const ProLicensePanelContent: Component = () => {
|
|||
>
|
||||
<MonitoredSystemLedgerPanel
|
||||
embedded
|
||||
monitoredSystemCapacity={state.entitlements()?.monitored_system_capacity}
|
||||
monitoredSystemContinuity={state.entitlements()?.monitored_system_continuity}
|
||||
monitoredSystemLimit={
|
||||
state
|
||||
.entitlements()
|
||||
?.limits?.find((entry) => entry.key === 'max_monitored_systems') ?? null
|
||||
}
|
||||
monitoredSystemCapacity={state.monitoredSystemCapacity()}
|
||||
monitoredSystemContinuity={state.displayableMonitoredSystemContinuity()}
|
||||
monitoredSystemLimit={state.monitoredSystemLimitStatus() ?? null}
|
||||
showCountingRulesByDefault={state.showCountingRulesByDefault()}
|
||||
/>
|
||||
</CommercialSection>
|
||||
|
|
|
|||
|
|
@ -297,7 +297,7 @@ describe('ProLicensePanel', () => {
|
|||
expect(screen.getByText('Patrol Auto-Fix')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows migrated plan terms when plan_version is present', async () => {
|
||||
it('shows active recurring v5 plan terms as uncapped even if stale limit metadata is present', async () => {
|
||||
mockEntitlements = {
|
||||
capabilities: ['ai_patrol'],
|
||||
limits: [{ key: 'max_monitored_systems', limit: 12, current: 5, state: 'ok' }],
|
||||
|
|
@ -308,6 +308,24 @@ describe('ProLicensePanel', () => {
|
|||
licensed_email: 'owner@example.com',
|
||||
is_lifetime: false,
|
||||
trial_eligible: false,
|
||||
monitored_system_continuity: {
|
||||
plan_limit: 12,
|
||||
grandfathered_floor: 23,
|
||||
effective_limit: 23,
|
||||
capture_pending: false,
|
||||
},
|
||||
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,
|
||||
},
|
||||
};
|
||||
|
||||
renderPanel();
|
||||
|
|
@ -316,20 +334,19 @@ describe('ProLicensePanel', () => {
|
|||
expect(screen.getByText('Plan Terms')).toBeInTheDocument();
|
||||
});
|
||||
|
||||
expect(screen.getAllByText('5 monitored systems').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('7 remaining').length).toBeGreaterThan(0);
|
||||
expect(screen.getAllByText('Active').length).toBeGreaterThan(0);
|
||||
expect(screen.getByText('V5 Pro Monthly (Grandfathered)')).toBeInTheDocument();
|
||||
expect(screen.getByText('Included Monitored Systems')).toBeInTheDocument();
|
||||
expect(screen.getByText('Core Monitoring')).toBeInTheDocument();
|
||||
expect(
|
||||
within(screen.getByText('Core Monitoring').parentElement as HTMLElement).getByText(
|
||||
'Unlimited',
|
||||
),
|
||||
).toBeInTheDocument();
|
||||
expect(screen.queryByText('Included Monitored Systems')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Grandfathered monitored-system floor')).not.toBeInTheDocument();
|
||||
expect(screen.queryByText('Plan Monitored System Limit')).not.toBeInTheDocument();
|
||||
expect(screen.getByRole('tab', { name: 'Plan' })).toHaveAttribute('aria-selected', 'true');
|
||||
expect(screen.getByRole('tab', { name: 'Usage' })).toHaveAttribute('aria-selected', 'false');
|
||||
|
||||
fireEvent.click(screen.getByRole('tab', { name: 'Usage' }));
|
||||
|
||||
expect(navigateMock).toHaveBeenCalledWith(SELF_HOSTED_PRO_BILLING_USAGE_HREF, {
|
||||
replace: false,
|
||||
scroll: false,
|
||||
});
|
||||
expect(screen.queryByRole('tab', { name: 'Usage' })).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('shows lifetime grandfathered plans as uncapped', async () => {
|
||||
|
|
@ -788,7 +805,7 @@ describe('ProLicensePanel', () => {
|
|||
{
|
||||
purchase: SELF_HOSTED_PRO_BILLING_PURCHASE_EXPIRED,
|
||||
title: 'Upgrade return expired',
|
||||
actionLabel: 'Restart upgrade',
|
||||
actionLabel: 'Compare plans',
|
||||
actionHref: getSelfHostedPurchaseStartUrl(),
|
||||
},
|
||||
{
|
||||
|
|
|
|||
|
|
@ -24,8 +24,9 @@ import {
|
|||
getLicenseSubscriptionStatusPresentation,
|
||||
getLicenseTierLabel,
|
||||
getTrialActivationNotice,
|
||||
getDisplayableMonitoredSystemContinuity,
|
||||
hasActiveUncappedSelfHostedContinuity,
|
||||
isDisplayableLicenseFeature,
|
||||
isUncappedGrandfatheredPlanVersion,
|
||||
} from '@/utils/licensePresentation';
|
||||
import {
|
||||
getSelfHostedBillingHref,
|
||||
|
|
@ -157,16 +158,38 @@ export function useProLicensePanelState() {
|
|||
);
|
||||
|
||||
const limitStatus = (key: string) => entitlements()?.limits?.find((entry) => entry.key === key);
|
||||
const monitoredSystemLimitStatus = createMemo(() => limitStatus('max_monitored_systems'));
|
||||
const monitoredSystemCapacity = createMemo(() => entitlements()?.monitored_system_capacity);
|
||||
const monitoredSystemContinuity = createMemo(() => entitlements()?.monitored_system_continuity);
|
||||
const uncappedGrandfatheredPlan = createMemo(() =>
|
||||
hasActiveUncappedSelfHostedContinuity({
|
||||
planVersion: entitlements()?.plan_version,
|
||||
isLifetime: entitlements()?.is_lifetime,
|
||||
subscriptionState: entitlements()?.subscription_state,
|
||||
}),
|
||||
);
|
||||
const monitoredSystemLimitStatus = createMemo(() =>
|
||||
uncappedGrandfatheredPlan() ? undefined : limitStatus('max_monitored_systems'),
|
||||
);
|
||||
const monitoredSystemCapacity = createMemo(() =>
|
||||
uncappedGrandfatheredPlan() ? undefined : entitlements()?.monitored_system_capacity,
|
||||
);
|
||||
const guestLimitStatus = createMemo(() =>
|
||||
uncappedGrandfatheredPlan() ? undefined : limitStatus('max_guests'),
|
||||
);
|
||||
const displayableMonitoredSystemContinuity = createMemo(() =>
|
||||
getDisplayableMonitoredSystemContinuity({
|
||||
continuity: monitoredSystemContinuity(),
|
||||
planVersion: entitlements()?.plan_version,
|
||||
isLifetime: entitlements()?.is_lifetime,
|
||||
subscriptionState: entitlements()?.subscription_state,
|
||||
}),
|
||||
);
|
||||
|
||||
const showUsageSection = createMemo(() => {
|
||||
if (!panelDataSettled()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
const continuity = monitoredSystemContinuity();
|
||||
const continuity = displayableMonitoredSystemContinuity();
|
||||
if (continuity) {
|
||||
if (continuity.capture_pending) {
|
||||
return true;
|
||||
|
|
@ -351,9 +374,6 @@ export function useProLicensePanelState() {
|
|||
return segments.length === 3 && segments.every((segment) => segment.length > 0);
|
||||
});
|
||||
|
||||
const uncappedGrandfatheredPlan = createMemo(() =>
|
||||
isUncappedGrandfatheredPlanVersion(entitlements()?.plan_version, entitlements()?.is_lifetime),
|
||||
);
|
||||
const monitoredSystemUsageSummary = createMemo(() => {
|
||||
const limit = monitoredSystemLimitStatus();
|
||||
const capacity = monitoredSystemCapacity();
|
||||
|
|
@ -374,7 +394,7 @@ export function useProLicensePanelState() {
|
|||
resolveMonitoredSystemCapacityStatus(monitoredSystemCapacity(), monitoredSystemLimitStatus()),
|
||||
);
|
||||
const currentRetailPlanDefinition = createMemo(() => {
|
||||
if (monitoredSystemContinuity()) {
|
||||
if (displayableMonitoredSystemContinuity()) {
|
||||
return null;
|
||||
}
|
||||
if (uncappedGrandfatheredPlan()) {
|
||||
|
|
@ -390,6 +410,11 @@ export function useProLicensePanelState() {
|
|||
monitoredSystemContinuity(),
|
||||
monitoredSystemLimitStatus(),
|
||||
monitoredSystemCapacity(),
|
||||
{
|
||||
planVersion: entitlements()?.plan_version,
|
||||
isLifetime: entitlements()?.is_lifetime,
|
||||
subscriptionState: entitlements()?.subscription_state,
|
||||
},
|
||||
),
|
||||
);
|
||||
const monitoredSystemCapacitySection = createMemo(() => {
|
||||
|
|
@ -406,7 +431,7 @@ export function useProLicensePanelState() {
|
|||
};
|
||||
});
|
||||
const continuityCapturedAt = createMemo(() => {
|
||||
const capturedAt = monitoredSystemContinuity()?.captured_at;
|
||||
const capturedAt = displayableMonitoredSystemContinuity()?.captured_at;
|
||||
return typeof capturedAt === 'number' && capturedAt > 0
|
||||
? formatUnixDate(capturedAt)
|
||||
: undefined;
|
||||
|
|
@ -511,7 +536,7 @@ export function useProLicensePanelState() {
|
|||
if (uncappedGrandfatheredPlan()) {
|
||||
return true;
|
||||
}
|
||||
const guestLimit = limitStatus('max_guests')?.limit;
|
||||
const guestLimit = guestLimitStatus()?.limit;
|
||||
return typeof guestLimit === 'number' && guestLimit > 0;
|
||||
});
|
||||
|
||||
|
|
@ -526,17 +551,17 @@ export function useProLicensePanelState() {
|
|||
monitoredSystemsSummary: monitoredSystemUsageSummary(),
|
||||
capacityStatusSummary: monitoredSystemCapacityStatusSummary(),
|
||||
maxMonitoredSystems:
|
||||
typeof limitStatus('max_monitored_systems')?.limit === 'number' &&
|
||||
limitStatus('max_monitored_systems')!.limit > 0
|
||||
? limitStatus('max_monitored_systems')!.limit
|
||||
typeof monitoredSystemLimitStatus()?.limit === 'number' &&
|
||||
monitoredSystemLimitStatus()!.limit > 0
|
||||
? monitoredSystemLimitStatus()!.limit
|
||||
: 'Unlimited',
|
||||
guestCapacity:
|
||||
typeof limitStatus('max_guests')?.limit === 'number' && limitStatus('max_guests')!.limit > 0
|
||||
? limitStatus('max_guests')!.limit
|
||||
typeof guestLimitStatus()?.limit === 'number' && guestLimitStatus()!.limit > 0
|
||||
? guestLimitStatus()!.limit
|
||||
: 'Unlimited',
|
||||
retailPlanDefinition: currentRetailPlanDefinition(),
|
||||
showGuestCapacity: showGuestCapacity(),
|
||||
monitoredSystemContinuity: monitoredSystemContinuity() ?? null,
|
||||
monitoredSystemContinuity: displayableMonitoredSystemContinuity() ?? null,
|
||||
continuityCapturedAt: continuityCapturedAt(),
|
||||
}),
|
||||
);
|
||||
|
|
@ -634,6 +659,8 @@ export function useProLicensePanelState() {
|
|||
currentPlanSummary,
|
||||
planComparisonSummary,
|
||||
monitoredSystemCapacity,
|
||||
monitoredSystemLimitStatus,
|
||||
displayableMonitoredSystemContinuity,
|
||||
monitoredSystemCapacitySection,
|
||||
monitoredSystemContinuityNotice,
|
||||
entitlements,
|
||||
|
|
|
|||
|
|
@ -38,6 +38,8 @@ describe('licensePresentation', () => {
|
|||
expect(getLicenseTierLabel('enterprise')).toBe('Enterprise');
|
||||
expect(getLicenseTierLabel('custom_tier')).toBe('Custom Tier');
|
||||
expect(getSelfHostedPlanLabel('pro')).toBe('Pulse Pro');
|
||||
expect(getSelfHostedPlanLabel('pro_plus')).toBe('Pulse Pro+');
|
||||
expect(getSelfHostedPlanLabel('lifetime')).toBe('Pulse Pro Lifetime');
|
||||
expect(getSelfHostedPlanLabel('relay')).toBe('Relay');
|
||||
});
|
||||
|
||||
|
|
@ -279,6 +281,25 @@ describe('licensePresentation', () => {
|
|||
supplementalSummary: '',
|
||||
});
|
||||
|
||||
expect(
|
||||
getSelfHostedCurrentPlanPresentation({
|
||||
entitlements: {
|
||||
tier: 'pro_plus',
|
||||
subscription_state: 'active',
|
||||
capabilities: ['relay', 'ai_autofix'],
|
||||
limits: [],
|
||||
upgrade_reasons: [],
|
||||
},
|
||||
displayableCapabilities: ['Pulse Relay (Remote Access)', 'Patrol Auto-Fix'],
|
||||
}),
|
||||
).toMatchObject({
|
||||
title: 'Current plan: Pulse Pro+',
|
||||
body:
|
||||
'Pulse Pro+ is active on this instance. Root-cause analysis, safe remediation, and 90-day history are unlocked right now.',
|
||||
unlockedFeaturesLabel: 'Primary capabilities',
|
||||
unlockedFeatures: ['Pulse Alert Analysis', 'Patrol Auto-Fix', '90-day metric history'],
|
||||
});
|
||||
|
||||
expect(
|
||||
getSelfHostedCurrentPlanPresentation({
|
||||
entitlements: {
|
||||
|
|
@ -311,9 +332,9 @@ describe('licensePresentation', () => {
|
|||
'PDF/CSV Reporting',
|
||||
'Centralized Agent Profiles',
|
||||
],
|
||||
supplementalBadges: ['Grandfathered price', 'Grandfathered floor'],
|
||||
supplementalBadges: ['Grandfathered price'],
|
||||
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.',
|
||||
'This migrated v5 subscription keeps its existing recurring price and uncapped guest capacity until cancellation.',
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -534,6 +555,37 @@ describe('licensePresentation', () => {
|
|||
body: expect.stringContaining('already monitoring 23'),
|
||||
tone: expect.stringContaining('amber'),
|
||||
});
|
||||
expect(
|
||||
getMonitoredSystemContinuityNotice(
|
||||
{
|
||||
plan_limit: 10,
|
||||
grandfathered_floor: 23,
|
||||
effective_limit: 23,
|
||||
capture_pending: false,
|
||||
captured_at: 123,
|
||||
},
|
||||
{
|
||||
current_available: true,
|
||||
},
|
||||
{
|
||||
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,
|
||||
},
|
||||
{
|
||||
planVersion: 'v5_pro_monthly_grandfathered',
|
||||
isLifetime: false,
|
||||
subscriptionState: 'active',
|
||||
},
|
||||
),
|
||||
).toBeNull();
|
||||
expect(
|
||||
getMonitoredSystemContinuityNotice(
|
||||
{
|
||||
|
|
|
|||
|
|
@ -157,6 +157,15 @@ describe('selfHostedPlans', () => {
|
|||
expect(getSelfHostedPlanDefinitionForBillingTier('pro')).toBe(
|
||||
SELF_HOSTED_PLAN_BY_TIER.pro,
|
||||
);
|
||||
expect(getSelfHostedPlanDefinitionForBillingTier('pro_annual')).toBe(
|
||||
SELF_HOSTED_PLAN_BY_TIER.pro,
|
||||
);
|
||||
expect(getSelfHostedPlanDefinitionForBillingTier('pro_plus')).toBe(
|
||||
SELF_HOSTED_PLAN_BY_TIER.pro,
|
||||
);
|
||||
expect(getSelfHostedPlanDefinitionForBillingTier('lifetime')).toBe(
|
||||
SELF_HOSTED_PLAN_BY_TIER.pro,
|
||||
);
|
||||
expect(getSelfHostedPlanDefinitionForBillingTier('enterprise')).toBeNull();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
|
|
@ -35,6 +35,9 @@ const TIER_LABELS: Record<string, string> = {
|
|||
|
||||
const SELF_HOSTED_PLAN_LABELS: Record<string, string> = {
|
||||
pro: 'Pulse Pro',
|
||||
pro_annual: 'Pulse Pro Annual',
|
||||
pro_plus: 'Pulse Pro+',
|
||||
lifetime: 'Pulse Pro Lifetime',
|
||||
};
|
||||
|
||||
const FEATURE_MIN_TIER_LABELS: Record<string, string> = {
|
||||
|
|
@ -139,6 +142,49 @@ export const isUncappedGrandfatheredPlanVersion = (
|
|||
return isGrandfatheredRecurringV5PlanVersion(planVersion);
|
||||
};
|
||||
|
||||
const isActiveOrGraceSubscription = (subscriptionState?: string | null): boolean => {
|
||||
const normalized = (subscriptionState || '').trim().toLowerCase();
|
||||
return normalized === 'active' || normalized === 'grace';
|
||||
};
|
||||
|
||||
export const hasActiveUncappedSelfHostedContinuity = ({
|
||||
planVersion,
|
||||
isLifetime,
|
||||
subscriptionState,
|
||||
}: {
|
||||
planVersion?: string | null;
|
||||
isLifetime?: boolean | null;
|
||||
subscriptionState?: string | null;
|
||||
}): boolean => {
|
||||
if (isLifetime) {
|
||||
return true;
|
||||
}
|
||||
return (
|
||||
isActiveOrGraceSubscription(subscriptionState) &&
|
||||
isGrandfatheredRecurringV5PlanVersion(planVersion)
|
||||
);
|
||||
};
|
||||
|
||||
export const getDisplayableMonitoredSystemContinuity = ({
|
||||
continuity,
|
||||
planVersion,
|
||||
isLifetime,
|
||||
subscriptionState,
|
||||
}: {
|
||||
continuity?: MonitoredSystemContinuityStatus | null;
|
||||
planVersion?: string | null;
|
||||
isLifetime?: boolean | null;
|
||||
subscriptionState?: string | null;
|
||||
}): MonitoredSystemContinuityStatus | null => {
|
||||
if (!continuity) {
|
||||
return null;
|
||||
}
|
||||
if (hasActiveUncappedSelfHostedContinuity({ planVersion, isLifetime, subscriptionState })) {
|
||||
return null;
|
||||
}
|
||||
return continuity;
|
||||
};
|
||||
|
||||
export const getLicenseTierLabel = (tier?: string | null): string => {
|
||||
const normalized = (tier || '').trim().toLowerCase();
|
||||
if (!normalized) return 'Unknown';
|
||||
|
|
@ -204,10 +250,24 @@ export const getMonitoredSystemContinuityNotice = (
|
|||
continuity?: MonitoredSystemContinuityStatus | null,
|
||||
limit?: MonitoredSystemLimitUsageStatus | null,
|
||||
capacity?: MonitoredSystemCapacityStatus | null,
|
||||
context?: {
|
||||
planVersion?: string | null;
|
||||
isLifetime?: boolean | null;
|
||||
subscriptionState?: string | null;
|
||||
},
|
||||
): LicenseInlineNotice | null => {
|
||||
const displayContinuity = getDisplayableMonitoredSystemContinuity({
|
||||
continuity,
|
||||
planVersion: context?.planVersion,
|
||||
isLifetime: context?.isLifetime,
|
||||
subscriptionState: context?.subscriptionState,
|
||||
});
|
||||
if (!displayContinuity) {
|
||||
return null;
|
||||
}
|
||||
const resolvedCapacity = resolveMonitoredSystemCapacityStatus(capacity, limit);
|
||||
|
||||
if (continuity?.capture_pending) {
|
||||
if (displayContinuity.capture_pending) {
|
||||
if (!resolvedCapacity?.current_available) {
|
||||
return {
|
||||
tone: 'border-amber-200 dark:border-amber-900 bg-amber-50 dark:bg-amber-900 text-amber-900 dark:text-amber-100',
|
||||
|
|
@ -222,7 +282,7 @@ export const getMonitoredSystemContinuityNotice = (
|
|||
return {
|
||||
tone: 'border-amber-200 dark:border-amber-900 bg-amber-50 dark:bg-amber-900 text-amber-900 dark:text-amber-100',
|
||||
title: 'Migration continuity verification pending',
|
||||
body: `Pulse is still verifying the grandfathered monitored-system floor for this migrated v5 installation. The finite policy includes ${continuity.plan_limit}, while this installation is already monitoring ${resolvedCapacity.current}. Existing monitoring continues while additional monitored-system admissions pause until continuity capture finishes.`,
|
||||
body: `Pulse is still verifying the grandfathered monitored-system floor for this migrated v5 installation. The finite policy includes ${displayContinuity.plan_limit}, while this installation is already monitoring ${resolvedCapacity.current}. Existing monitoring continues while additional monitored-system admissions pause until continuity capture finishes.`,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -244,15 +304,14 @@ export const getMonitoredSystemContinuityNotice = (
|
|||
}
|
||||
|
||||
if (
|
||||
continuity &&
|
||||
typeof continuity.grandfathered_floor === 'number' &&
|
||||
continuity.grandfathered_floor > 0 &&
|
||||
continuity.effective_limit > continuity.plan_limit
|
||||
typeof displayContinuity.grandfathered_floor === 'number' &&
|
||||
displayContinuity.grandfathered_floor > 0 &&
|
||||
displayContinuity.effective_limit > displayContinuity.plan_limit
|
||||
) {
|
||||
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 monitored-system floor',
|
||||
body: `This migrated v5 installation keeps an effective monitored-system limit of ${continuity.effective_limit}. The current plan includes ${continuity.plan_limit}, and the observed legacy estate was grandfathered at ${continuity.grandfathered_floor}.`,
|
||||
body: `This migrated v5 installation keeps an effective monitored-system limit of ${displayContinuity.effective_limit}. The current plan includes ${displayContinuity.plan_limit}, and the observed legacy estate was grandfathered at ${displayContinuity.grandfathered_floor}.`,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -320,6 +379,22 @@ const getSelfHostedActivationHighlights = ({
|
|||
return highlights;
|
||||
};
|
||||
|
||||
const getSelfHostedActivePlanSummary = (
|
||||
planLabel: string,
|
||||
planDefinition: ReturnType<typeof getSelfHostedPlanDefinitionForBillingTier>,
|
||||
): string | null => {
|
||||
switch (planDefinition?.tier) {
|
||||
case 'community':
|
||||
return `${planLabel} is active on this instance. It includes self-hosted monitoring, 7-day metric history, Pulse Patrol (BYOK), and update alerts.`;
|
||||
case 'relay':
|
||||
return `${planLabel} is active on this instance. Remote access, mobile, push, and longer history are unlocked right now.`;
|
||||
case 'pro':
|
||||
return `${planLabel} is active on this instance. Root-cause analysis, safe remediation, and 90-day history are unlocked right now.`;
|
||||
default:
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
export const getSelfHostedPlanComparisonPresentation = ({
|
||||
entitlements,
|
||||
}: {
|
||||
|
|
@ -373,6 +448,11 @@ export const getSelfHostedCurrentPlanPresentation = ({
|
|||
const normalizedTier = (current.tier || '').trim().toLowerCase();
|
||||
const planLabel = getSelfHostedPlanLabel(current.tier);
|
||||
const planDefinition = getSelfHostedPlanDefinitionForBillingTier(current.tier);
|
||||
const hasUncappedContinuity = hasActiveUncappedSelfHostedContinuity({
|
||||
planVersion: current.plan_version,
|
||||
isLifetime: current.is_lifetime,
|
||||
subscriptionState: current.subscription_state,
|
||||
});
|
||||
const unlockedFeatures = getSelfHostedUnlockedFeatures({
|
||||
entitlements: current,
|
||||
displayableCapabilities,
|
||||
|
|
@ -386,19 +466,27 @@ export const getSelfHostedCurrentPlanPresentation = ({
|
|||
const supplementalBadges: string[] = [];
|
||||
const supplementalDetails: string[] = [];
|
||||
|
||||
if (isGrandfatheredRecurringV5PlanVersion(current.plan_version)) {
|
||||
if (
|
||||
isActiveOrGraceSubscription(current.subscription_state) &&
|
||||
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)) {
|
||||
} else if (hasUncappedContinuity && current.is_lifetime) {
|
||||
supplementalBadges.push('Grandfathered lifetime');
|
||||
supplementalDetails.push(
|
||||
'This migrated lifetime install keeps uncapped monitored-system and guest capacity continuity.',
|
||||
);
|
||||
}
|
||||
|
||||
const continuity = current.monitored_system_continuity;
|
||||
const continuity = getDisplayableMonitoredSystemContinuity({
|
||||
continuity: current.monitored_system_continuity,
|
||||
planVersion: current.plan_version,
|
||||
isLifetime: current.is_lifetime,
|
||||
subscriptionState: current.subscription_state,
|
||||
});
|
||||
if (continuity?.capture_pending) {
|
||||
supplementalBadges.push('Continuity pending');
|
||||
supplementalDetails.push(
|
||||
|
|
@ -436,7 +524,7 @@ export const getSelfHostedCurrentPlanPresentation = ({
|
|||
return {
|
||||
title: 'Current plan: Community',
|
||||
body:
|
||||
planDefinition?.entitlementSummary ||
|
||||
getSelfHostedActivePlanSummary('Community', planDefinition) ||
|
||||
'Community is active on this instance. Self-hosted monitoring, 7-day metric history, Pulse Patrol (BYOK), and update alerts are included here.',
|
||||
unlockedFeaturesLabel,
|
||||
unlockedFeatures,
|
||||
|
|
@ -450,7 +538,7 @@ export const getSelfHostedCurrentPlanPresentation = ({
|
|||
return {
|
||||
title: `Current plan: ${planLabel}`,
|
||||
body:
|
||||
planDefinition?.entitlementSummary ||
|
||||
getSelfHostedActivePlanSummary(planLabel, planDefinition) ||
|
||||
`${planLabel} is active on this instance. These capabilities are unlocked right now.`,
|
||||
unlockedFeaturesLabel,
|
||||
unlockedFeatures,
|
||||
|
|
|
|||
|
|
@ -167,6 +167,9 @@ export function getSelfHostedPlanDefinitionForBillingTier(
|
|||
case 'relay':
|
||||
return SELF_HOSTED_PLAN_BY_TIER.relay;
|
||||
case 'pro':
|
||||
case 'pro_annual':
|
||||
case 'pro_plus':
|
||||
case 'lifetime':
|
||||
return SELF_HOSTED_PLAN_BY_TIER.pro;
|
||||
default:
|
||||
return null;
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue