mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Retire self-hosted trial posture prompts
This commit is contained in:
@@ -1629,7 +1629,9 @@ fleet surfaces must not expose direct trial CTAs or depend on
|
||||
`POST /api/license/trial/start`, and the normal router must fail that path as
|
||||
`404` without mutating entitlements. The retired `/auth/trial-activate`
|
||||
self-hosted callback must also stay absent from lifecycle retry and backoff
|
||||
behavior.
|
||||
behavior. Lifecycle-adjacent setup and install surfaces must also treat
|
||||
`trial_eligible` and `trial_eligibility_reason` as retired compatibility
|
||||
fields, not as prompt state or setup transport state.
|
||||
That same shared `internal/api/` dependency also assumes session-carried OIDC
|
||||
refresh tokens stay fail-closed at rest: `session_store.go` may only persist
|
||||
or recover those tokens through encrypted-at-rest session payloads, and any
|
||||
|
||||
@@ -676,7 +676,11 @@ the canonical monitored-system blocked payload.
|
||||
`/auth/trial-activate` self-hosted return path must also stay absent from
|
||||
the ordinary router and settings UI; signed hosted entitlement leases may
|
||||
refresh cached hosted/cloud entitlement state, but they must not create a
|
||||
local Pro trial acquisition callback.
|
||||
local Pro trial acquisition callback. Entitlement payloads may retain
|
||||
`trial_eligible` and `trial_eligibility_reason` as compatibility fields,
|
||||
but ordinary self-hosted responses leave eligibility false and the reason
|
||||
empty; active trial state is represented by `subscription_state`,
|
||||
`trial_expires_at`, and `trial_days_remaining` only.
|
||||
29. Keep `/api/security/dev/reset-first-run` transport-backed and genuinely
|
||||
unauthenticated: when the dev reset route clears first-run auth it must
|
||||
also clear any env-backed auth state that feeds `/api/security/status`, so
|
||||
@@ -869,7 +873,7 @@ for upgrade links, trial CTAs, plan upsells, and paid-only navigation rather
|
||||
than as a billing entitlement change.
|
||||
That same contract split also makes the licensing boundary explicit:
|
||||
`/api/license/runtime-capabilities` is the public runtime feature contract,
|
||||
`/api/license/commercial-posture` is the non-billing upgrade/trial posture
|
||||
`/api/license/commercial-posture` is the non-billing upgrade posture
|
||||
contract for real customer workspaces, and `/api/license/entitlements`
|
||||
remains billing-only. New callers must extend one of those owned shapes
|
||||
instead of reviving a combined entitlement payload for mixed runtime,
|
||||
@@ -2947,7 +2951,8 @@ out of the router inventory and must not return the old hosted-signup or
|
||||
trial-rate-limit acquisition payloads from an ordinary self-hosted runtime;
|
||||
`frontend-modern/src/api/license.ts`, demo mode, and feature gates must not
|
||||
expose a start-trial client method or in-app CTA in the same slice as any
|
||||
handler change.
|
||||
handler change. Commercial migration state must travel through
|
||||
`commercial_migration`, not through trial-denial reason strings.
|
||||
That same shared commercial API boundary also owns hosted self-serve failure
|
||||
transport semantics. Hosted trial request and verification failures may render
|
||||
owned HTML pages, but they must preserve the originating Pulse instance and
|
||||
|
||||
@@ -241,7 +241,11 @@ Community limit enforcement.
|
||||
13. Add or change hosted billing-admin presentation through `frontend-modern/src/components/Settings/BillingAdminPanel.tsx`, `frontend-modern/src/components/Settings/BillingAdminOrganizationsTable.tsx`, and `frontend-modern/src/components/Settings/useBillingAdminPanelState.ts`
|
||||
14. Add or change shared commercial plan/usage presentation through `frontend-modern/src/components/Settings/CommercialBillingSections.tsx` and `frontend-modern/src/utils/commercialBillingModel.ts`
|
||||
15. Add or change organization billing and usage presentation through `frontend-modern/src/components/Settings/OrganizationBillingPanel.tsx`, `frontend-modern/src/components/Settings/OrganizationBillingLoadingState.tsx`, and `frontend-modern/src/components/Settings/useOrganizationBillingPanelState.ts`
|
||||
16. Add or change self-hosted Pro plan, trial, recovery, and entitlement actions through `frontend-modern/src/components/Settings/ProLicensePanel.tsx`, `frontend-modern/src/components/Settings/ProLicensePlanSection.tsx`, `frontend-modern/src/components/Settings/SelfHostedCommercialRecoverySection.tsx`, and `frontend-modern/src/components/Settings/useProLicensePanelState.ts`
|
||||
16. Add or change self-hosted Pro plan, recovery, and entitlement actions through `frontend-modern/src/components/Settings/ProLicensePanel.tsx`, `frontend-modern/src/components/Settings/ProLicensePlanSection.tsx`, `frontend-modern/src/components/Settings/SelfHostedCommercialRecoverySection.tsx`, and `frontend-modern/src/components/Settings/useProLicensePanelState.ts`
|
||||
Ordinary self-hosted trial acquisition is retired: these plan surfaces may
|
||||
show active historical `subscription_state=trial` entitlement state, but
|
||||
they must not turn `trial_eligible`, `trial_eligibility_reason`, or an
|
||||
expired trial marker into a default Pro CTA or banner.
|
||||
17. Add or change monitored-system ledger, disclosure, or admission-preview presentation through `frontend-modern/src/components/Settings/MonitoredSystemLedgerPanel.tsx`, `frontend-modern/src/components/Settings/MonitoredSystemAdmissionPreview.tsx`, `frontend-modern/src/components/Commercial/MonitoredSystemDefinitionDisclosure.tsx`, and `frontend-modern/src/utils/monitoredSystemPresentation.ts`
|
||||
18. Add or change paid relay settings and pairing presentation through `frontend-modern/src/components/Settings/RelaySettingsPanel.tsx`, `frontend-modern/src/components/Settings/RelayPairingSection.tsx`, and `frontend-modern/src/components/Settings/useRelaySettingsPanelState.ts`. The Dashboard shell must not carry a Relay onboarding card or equivalent blanket upsell — relay discovery stays inside its owning settings surface.
|
||||
Public demo and other read-only presentation policy states must suppress
|
||||
@@ -504,11 +508,14 @@ Community limit enforcement.
|
||||
|
||||
`frontend-modern/src/utils/pricingHandoff.ts` now routes pro-feature paywall
|
||||
keys (`ai_alerts`, `ai_autofix`, `relay`, `rbac`, `audit_logging`,
|
||||
`advanced_sso`, `agent_profiles`, `long_term_metrics`, `trial_expired`) to the
|
||||
`advanced_sso`, `agent_profiles`, `long_term_metrics`) to the
|
||||
self-hosted billing plan page instead of the Pulse Account purchase-start
|
||||
handoff. The purchase-start handoff requires a `PublicURL` and fails on local
|
||||
instances; routing these keys to the in-product billing plan keeps upgrades
|
||||
accessible from self-hosted environments.
|
||||
Retired trial-acquisition intents such as `trial_expired` must not be owned
|
||||
pricing handoff destinations; no ordinary self-hosted runtime path should emit
|
||||
them as an upgrade reason or plan-page CTA.
|
||||
The monitored-system app-shell warning CTA now follows that same commercial
|
||||
boundary by rendering only in hosted mode. Ordinary self-hosted installs must
|
||||
not see finite monitored-system pressure in the global app shell; when hosted
|
||||
@@ -567,7 +574,7 @@ endpoints are fully hidden (`404`) and which remain available only as a
|
||||
non-commercial public contract. `/api/license/runtime-capabilities` is the
|
||||
canonical public exception for feature truth and history retention.
|
||||
`/api/license/commercial-posture` is the canonical non-billing commercial
|
||||
contract for upgrade/trial posture in real customer workspaces, while
|
||||
contract for upgrade posture in real customer workspaces, while
|
||||
`/api/license/entitlements` remains billing-only. In public demo mode both
|
||||
commercial routes, plus `/auth/license-purchase-start`, must stay hidden and
|
||||
public browsers must not see licensed identity, plan labels, upgrade reasons,
|
||||
@@ -623,7 +630,7 @@ owner for the first `/api/license/commercial-posture` read, while
|
||||
other non-billing feature hooks may consume the resolved posture store but
|
||||
must not each trigger their own mount-time posture fetch. Billing-owned
|
||||
surfaces such as `frontend-modern/src/components/Settings/useProLicensePanelState.ts`
|
||||
may still force refresh through the same shared store when plan, trial, or
|
||||
may still force refresh through the same shared store when plan, activation, or
|
||||
recovery actions mutate commercial truth.
|
||||
Non-billing browser journeys must also stay off
|
||||
`/api/license/entitlements` entirely. Dashboard, infrastructure, alerts,
|
||||
|
||||
@@ -756,9 +756,9 @@ storage and recovery may inherit demo-safe `/api/license/runtime-capabilities`
|
||||
reads for capability and history-retention truth, but
|
||||
`/api/license/commercial-posture`, `/api/license/entitlements`, and
|
||||
`/auth/license-purchase-start` stay hidden and those surfaces must not expect
|
||||
licensed identity, upgrade prompts, trial urgency, checkout handoff state, or
|
||||
observed usage counts to remain present once the public-demo contract is
|
||||
applied.
|
||||
licensed identity, upgrade prompts, trial urgency or eligibility reasons,
|
||||
checkout handoff state, or observed usage counts to remain present once the
|
||||
public-demo contract is applied.
|
||||
That same runtime-safe exception now also redacts monitored-system capacity
|
||||
posture. Storage/recovery surfaces may keep demo-safe capability and retention
|
||||
truth from `/api/license/runtime-capabilities`, but they must not expect
|
||||
@@ -1887,7 +1887,9 @@ storage- or recovery-adjacent flows must not invoke or advertise the retired
|
||||
`POST /api/license/trial/start` route, and the normal self-hosted router must
|
||||
return `404` for that path without mutating entitlements. The retired
|
||||
`/auth/trial-activate` self-hosted callback must also stay absent from
|
||||
storage/recovery-local retry or backoff behavior.
|
||||
storage/recovery-local retry or backoff behavior. Retired
|
||||
`trial_eligible`/`trial_eligibility_reason` payload fields are compatibility
|
||||
only and must not become storage/recovery prompt, identity, or restore state.
|
||||
That same shared `internal/api/` dependency now also assumes adjacent
|
||||
commercial helper surfaces speak in monitored-system terms: recovery- or
|
||||
storage-adjacent API wiring may consume the canonical monitored-system ledger
|
||||
|
||||
@@ -114,7 +114,6 @@ const ProLicensePanelContent: Component = () => {
|
||||
purchaseActivationAction={state.purchaseActivationAction()}
|
||||
onPurchaseActivationActionClick={state.handlePurchaseActivationActionClick}
|
||||
purchaseActivationNotice={state.purchaseActivationNotice()}
|
||||
trialEnded={state.trialEnded()}
|
||||
/>
|
||||
|
||||
<SelfHostedCommercialRecoverySection
|
||||
|
||||
@@ -1,12 +1,10 @@
|
||||
import { Component, For, Show } from 'solid-js';
|
||||
import RefreshCw from 'lucide-solid/icons/refresh-cw';
|
||||
import { UpgradeLink } from '@/components/shared/UpgradeLink';
|
||||
import { getUpgradeActionDestination } from '@/stores/licenseCommercial';
|
||||
import { licenseEntitlementsLoadError } from '@/stores/licenseEntitlements';
|
||||
import {
|
||||
getLicenseStatusLoadingState,
|
||||
getNoActiveProLicenseState,
|
||||
getTrialEndedProLicenseNotice,
|
||||
} from '@/utils/licensePresentation';
|
||||
import { SELF_HOSTED_PRO_BILLING_PRESENTATION } from './selfHostedBillingPresentation';
|
||||
import type { UpgradeDestination } from '@/utils/upgradeNavigation';
|
||||
@@ -88,7 +86,6 @@ interface ProLicensePlanSectionProps {
|
||||
destination: UpgradeDestination;
|
||||
} | null;
|
||||
onPurchaseActivationActionClick: () => void;
|
||||
trialEnded: boolean;
|
||||
}
|
||||
|
||||
const formatDate = (value?: string | null) => {
|
||||
@@ -99,8 +96,6 @@ const formatDate = (value?: string | null) => {
|
||||
};
|
||||
|
||||
export const ProLicensePlanSection: Component<ProLicensePlanSectionProps> = (props) => {
|
||||
const trialEndedNotice = props.trialEnded ? getTrialEndedProLicenseNotice() : null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<Show when={props.activationSuccessSummary}>
|
||||
@@ -321,18 +316,6 @@ export const ProLicensePlanSection: Component<ProLicensePlanSectionProps> = (pro
|
||||
</div>
|
||||
)}
|
||||
</Show>
|
||||
<Show when={props.trialEnded && !licenseEntitlementsLoadError() && trialEndedNotice}>
|
||||
<div class={`mb-4 rounded-md border p-3 text-sm ${trialEndedNotice?.tone ?? ''}`}>
|
||||
<p class="font-medium">{trialEndedNotice?.title}</p>
|
||||
<p class="text-xs mt-1 opacity-90">{trialEndedNotice?.body}</p>
|
||||
<UpgradeLink
|
||||
class="inline-flex items-center gap-1 mt-2 text-xs font-medium hover:underline"
|
||||
destination={getUpgradeActionDestination('trial_expired')}
|
||||
>
|
||||
{trialEndedNotice?.actionLabel}
|
||||
</UpgradeLink>
|
||||
</div>
|
||||
</Show>
|
||||
<Show when={licenseEntitlementsLoadError()}>
|
||||
<div class="rounded-md border border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900 p-3 text-sm text-amber-800 dark:text-amber-200">
|
||||
<p class="font-medium">Could not load license status</p>
|
||||
|
||||
@@ -118,7 +118,7 @@ describe('ProLicensePanel', () => {
|
||||
subscription_state: 'expired',
|
||||
upgrade_reasons: [],
|
||||
tier: 'free',
|
||||
trial_eligible: true,
|
||||
trial_eligible: false,
|
||||
};
|
||||
|
||||
loadRuntimeLicenseStatusMock.mockReset();
|
||||
@@ -252,7 +252,7 @@ describe('ProLicensePanel', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('hides start trial action and shows trial-ended banner when trial was already used', async () => {
|
||||
it('does not surface a trial-ended banner for retired self-hosted trial state', async () => {
|
||||
mockEntitlements = {
|
||||
capabilities: [],
|
||||
limits: [],
|
||||
@@ -268,7 +268,7 @@ describe('ProLicensePanel', () => {
|
||||
expect(
|
||||
screen.queryByRole('button', { name: /start 14-day pro trial/i }),
|
||||
).not.toBeInTheDocument();
|
||||
expect(screen.getByText('Your Pro trial has ended')).toBeInTheDocument();
|
||||
expect(screen.queryByText('Your Pro trial has ended')).not.toBeInTheDocument();
|
||||
});
|
||||
|
||||
it('renders trial countdown from entitlements payload', async () => {
|
||||
@@ -1014,7 +1014,6 @@ describe('ProLicensePanel', () => {
|
||||
upgrade_reasons: [],
|
||||
tier: 'free',
|
||||
trial_eligible: false,
|
||||
trial_eligibility_reason: 'commercial_migration_pending',
|
||||
commercial_migration: {
|
||||
source: 'v5_license',
|
||||
state: 'pending',
|
||||
@@ -1040,7 +1039,6 @@ describe('ProLicensePanel', () => {
|
||||
upgrade_reasons: [],
|
||||
tier: 'free',
|
||||
trial_eligible: false,
|
||||
trial_eligibility_reason: 'commercial_migration_pending',
|
||||
commercial_migration: {
|
||||
source: 'v5_license',
|
||||
state: 'pending',
|
||||
@@ -1064,7 +1062,6 @@ describe('ProLicensePanel', () => {
|
||||
upgrade_reasons: [],
|
||||
tier: 'free',
|
||||
trial_eligible: false,
|
||||
trial_eligibility_reason: 'commercial_migration_failed',
|
||||
commercial_migration: {
|
||||
source: 'v5_license',
|
||||
state: 'failed',
|
||||
@@ -1117,7 +1114,7 @@ describe('ProLicensePanel', () => {
|
||||
expect(proLicensePanelStateSource).toContain("'A license or activation key is required'");
|
||||
expect(proLicensePlanSectionSource).toContain('getLicenseStatusLoadingState');
|
||||
expect(proLicensePlanSectionSource).toContain('getNoActiveProLicenseState');
|
||||
expect(proLicensePlanSectionSource).toContain('getTrialEndedProLicenseNotice');
|
||||
expect(proLicensePlanSectionSource).not.toContain('getTrialEndedProLicenseNotice');
|
||||
expect(proLicensePlanSectionSource).toContain('currentPlanSummary.title');
|
||||
expect(proLicensePlanSectionSource).toContain('currentPlanSummary.supplementalBadges');
|
||||
expect(proLicensePlanSectionSource).toContain('props.activationSuccessSummary');
|
||||
@@ -1132,9 +1129,8 @@ describe('ProLicensePanel', () => {
|
||||
expect(proLicensePlanSectionSource).not.toContain('MonitoredSystemDefinitionDisclosure');
|
||||
expect(proLicensePlanSectionSource).not.toContain('trialStartTitle');
|
||||
expect(proLicensePlanSectionSource).not.toContain('trialStartIdleActionLabel');
|
||||
expect(proLicensePlanSectionSource).toContain(
|
||||
'const trialEndedNotice = props.trialEnded ? getTrialEndedProLicenseNotice() : null;',
|
||||
);
|
||||
expect(proLicensePlanSectionSource).not.toContain('trialEndedNotice');
|
||||
expect(proLicensePlanSectionSource).not.toContain('props.trialEnded');
|
||||
expect(proLicensePlanSectionSource).toContain('planSelectionPrompt');
|
||||
expect(proLicensePlanSectionSource).not.toContain(
|
||||
"resolveSelfHostedPurchaseStartDestination('self_hosted_plan')",
|
||||
|
||||
@@ -427,12 +427,6 @@ export function useProLicensePanelState() {
|
||||
: undefined;
|
||||
});
|
||||
|
||||
const trialEnded = createMemo(
|
||||
() =>
|
||||
subscriptionState() === 'expired' &&
|
||||
entitlements()?.trial_eligibility_reason === 'already_used',
|
||||
);
|
||||
|
||||
const purchaseActivationNotice = createMemo(() => {
|
||||
if (purchaseActivationResult().trim().toLowerCase() === 'activated') {
|
||||
return null;
|
||||
@@ -668,6 +662,5 @@ export function useProLicensePanelState() {
|
||||
showCountingRulesByDefault,
|
||||
showRecoveryByDefault,
|
||||
statusPresentation,
|
||||
trialEnded,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2505,7 +2505,7 @@ describe('frontend resource type boundaries', () => {
|
||||
expect(proLicensePanelStateSource).toContain('getCommercialMigrationNotice');
|
||||
expect(proLicensePlanSectionSource).toContain('getLicenseStatusLoadingState');
|
||||
expect(proLicensePlanSectionSource).toContain('getNoActiveProLicenseState');
|
||||
expect(proLicensePlanSectionSource).toContain('getTrialEndedProLicenseNotice');
|
||||
expect(proLicensePlanSectionSource).not.toContain('getTrialEndedProLicenseNotice');
|
||||
expect(proLicensePlanSectionSource).not.toContain('getInactiveProUpsellNotice');
|
||||
expect(proLicensePanelSource).not.toContain('Loading license status...');
|
||||
expect(proLicensePanelSource).not.toContain('No Pro license is active.');
|
||||
@@ -2519,7 +2519,7 @@ describe('frontend resource type boundaries', () => {
|
||||
);
|
||||
expect(licensePresentationSource).toContain('export const getLicenseStatusLoadingState');
|
||||
expect(licensePresentationSource).toContain('export const getNoActiveProLicenseState');
|
||||
expect(licensePresentationSource).toContain('export const getTrialEndedProLicenseNotice');
|
||||
expect(licensePresentationSource).not.toContain('export const getTrialEndedProLicenseNotice');
|
||||
expect(licensePresentationSource).toContain('export const getInactiveProUpsellNotice');
|
||||
expect(licensePresentationSource).toContain(
|
||||
'export const getOrganizationBillingLicenseStatusLabel',
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
getOrganizationBillingLicenseStatusLabel,
|
||||
getInactiveProUpsellNotice,
|
||||
getPurchaseActivationNotice,
|
||||
getTrialEndedProLicenseNotice,
|
||||
isDisplayableLicenseFeature,
|
||||
isGrandfatheredRecurringV5PlanVersion,
|
||||
isUncappedGrandfatheredPlanVersion,
|
||||
@@ -50,12 +49,6 @@ describe('licensePresentation', () => {
|
||||
expect(getNoActiveProLicenseState()).toEqual({
|
||||
text: 'No Pro license is active.',
|
||||
});
|
||||
expect(getTrialEndedProLicenseNotice()).toEqual({
|
||||
tone: 'border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900 text-red-900 dark:text-red-100',
|
||||
title: 'Your Pro trial has ended',
|
||||
body: 'Choose a paid plan to keep root-cause analysis, safe remediation workflows, and 90-day history on this instance.',
|
||||
actionLabel: 'View Pro plans',
|
||||
});
|
||||
expect(getInactiveProUpsellNotice()).toEqual({
|
||||
tone: 'border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900 text-amber-800 dark:text-amber-200',
|
||||
title: 'Pro options available',
|
||||
|
||||
@@ -46,6 +46,12 @@ describe('pricingHandoff', () => {
|
||||
expect(getUpgradeFallbackDestination('rbac')).toBe(SELF_HOSTED_PRO_BILLING_PLAN_HREF);
|
||||
});
|
||||
|
||||
it('does not keep retired trial-expired as an owned in-product billing intent', () => {
|
||||
expect(getPricingRouteDestination('?feature=trial_expired')).toBe(
|
||||
getSelfHostedPurchaseStartUrl('trial_expired', new URLSearchParams('feature=trial_expired')),
|
||||
);
|
||||
});
|
||||
|
||||
it('routes unknown feature upgrades to Pulse Account purchase start', () => {
|
||||
expect(getUpgradeFallbackDestination('unknown_pro_feature')).toBe(
|
||||
getSelfHostedPurchaseStartUrl('unknown_pro_feature'),
|
||||
|
||||
@@ -630,7 +630,6 @@ export const getCommercialMigrationNotice = (
|
||||
if (!migration?.state) return null;
|
||||
|
||||
const actionText = getCommercialMigrationActionText(migration.recommended_action);
|
||||
const blockedText = 'A new Pro trial stays blocked until this is resolved.';
|
||||
|
||||
if (migration.state === 'pending') {
|
||||
let body =
|
||||
@@ -651,7 +650,7 @@ export const getCommercialMigrationNotice = (
|
||||
return {
|
||||
tone: 'border-amber-200 dark:border-amber-900 bg-amber-50 dark:bg-amber-900 text-amber-900 dark:text-amber-100',
|
||||
title: 'v5 license migration pending',
|
||||
body: `${body} ${actionText} ${blockedText}`,
|
||||
body: `${body} ${actionText}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -680,7 +679,7 @@ export const getCommercialMigrationNotice = (
|
||||
return {
|
||||
tone: 'border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900 text-red-900 dark:text-red-100',
|
||||
title: 'v5 license migration needs attention',
|
||||
body: `${body} ${actionText} ${blockedText}`,
|
||||
body: `${body} ${actionText}`,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -780,13 +779,6 @@ export const getNoActiveProLicenseState = (): LicenseLoadingStateCopy => ({
|
||||
text: 'No Pro license is active.',
|
||||
});
|
||||
|
||||
export const getTrialEndedProLicenseNotice = (): LicenseActionNotice => ({
|
||||
tone: 'border-red-200 dark:border-red-900 bg-red-50 dark:bg-red-900 text-red-900 dark:text-red-100',
|
||||
title: 'Your Pro trial has ended',
|
||||
body: 'Choose a paid plan to keep root-cause analysis, safe remediation workflows, and 90-day history on this instance.',
|
||||
actionLabel: 'View Pro plans',
|
||||
});
|
||||
|
||||
export const getInactiveProUpsellNotice = (): LicenseActionNotice => ({
|
||||
tone: 'border-amber-200 dark:border-amber-800 bg-amber-50 dark:bg-amber-900 text-amber-800 dark:text-amber-200',
|
||||
title: 'Pro options available',
|
||||
|
||||
@@ -63,7 +63,6 @@ const IN_PRODUCT_PRICING_DESTINATIONS: Record<string, string> = {
|
||||
advanced_sso: SELF_HOSTED_PRO_BILLING_PLAN_HREF,
|
||||
agent_profiles: SELF_HOSTED_PRO_BILLING_PLAN_HREF,
|
||||
long_term_metrics: SELF_HOSTED_PRO_BILLING_PLAN_HREF,
|
||||
trial_expired: SELF_HOSTED_PRO_BILLING_PLAN_HREF,
|
||||
};
|
||||
|
||||
const INTERNAL_HREF_BASE = 'https://pulse.invalid';
|
||||
|
||||
@@ -5327,7 +5327,7 @@ func TestContract_SelfHostedCommunityEntitlementsJSONSnapshot(t *testing.T) {
|
||||
"valid":false,
|
||||
"is_lifetime":false,
|
||||
"days_remaining":0,
|
||||
"trial_eligible":true,
|
||||
"trial_eligible":false,
|
||||
"max_history_days":7,
|
||||
"overflow_days_remaining":14,
|
||||
"legacy_connections":{"proxmox_nodes":0,"docker_hosts":0,"kubernetes_clusters":0},
|
||||
|
||||
@@ -548,7 +548,7 @@ func TestHandleRuntimeCapabilities_HostedCommunityEvaluatorStateStripsLegacyComm
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntitlementHandler_TrialEligibility_FreshOrgAllowed(t *testing.T) {
|
||||
func TestEntitlementHandler_SelfHostedTrialEligibilityRetiredForFreshOrg(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
mtp := config.NewMultiTenantPersistence(baseDir)
|
||||
h := NewLicenseHandlers(mtp, false)
|
||||
@@ -569,8 +569,8 @@ func TestEntitlementHandler_TrialEligibility_FreshOrgAllowed(t *testing.T) {
|
||||
if payload.SubscriptionState != string(license.SubStateActive) {
|
||||
t.Fatalf("subscription_state=%q, want %q", payload.SubscriptionState, license.SubStateActive)
|
||||
}
|
||||
if !payload.TrialEligible {
|
||||
t.Fatalf("trial_eligible=%v, want true", payload.TrialEligible)
|
||||
if payload.TrialEligible {
|
||||
t.Fatalf("trial_eligible=%v, want false", payload.TrialEligible)
|
||||
}
|
||||
if payload.TrialEligibilityReason != "" {
|
||||
t.Fatalf("trial_eligibility_reason=%q, want empty", payload.TrialEligibilityReason)
|
||||
@@ -622,8 +622,8 @@ func TestEntitlementHandler_OverflowOnlyBillingStateReportsActiveCommunity(t *te
|
||||
if payload.SubscriptionState != string(license.SubStateActive) {
|
||||
t.Fatalf("subscription_state=%q, want %q", payload.SubscriptionState, license.SubStateActive)
|
||||
}
|
||||
if !payload.TrialEligible {
|
||||
t.Fatalf("trial_eligible=%v, want true", payload.TrialEligible)
|
||||
if payload.TrialEligible {
|
||||
t.Fatalf("trial_eligible=%v, want false", payload.TrialEligible)
|
||||
}
|
||||
if payload.MonitoredSystemCapacity == nil || payload.MonitoredSystemCapacity.Limit != 0 || payload.MonitoredSystemCapacity.BlocksNewSystems {
|
||||
t.Fatalf("expected uncapped monitored_system_capacity, got %+v", payload.MonitoredSystemCapacity)
|
||||
@@ -709,7 +709,7 @@ func TestEntitlementHandler_DevModeIncludesMultiTenantWhenRuntimeEnabled(t *test
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntitlementHandler_TrialEligibility_AlreadyUsedDenied(t *testing.T) {
|
||||
func TestEntitlementHandler_ExpiredTrialStateDoesNotExposeStartReason(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
mtp := config.NewMultiTenantPersistence(baseDir)
|
||||
orgID := "default"
|
||||
@@ -746,12 +746,12 @@ func TestEntitlementHandler_TrialEligibility_AlreadyUsedDenied(t *testing.T) {
|
||||
if payload.TrialEligible {
|
||||
t.Fatalf("trial_eligible=%v, want false", payload.TrialEligible)
|
||||
}
|
||||
if payload.TrialEligibilityReason != "already_used" {
|
||||
t.Fatalf("trial_eligibility_reason=%q, want %q", payload.TrialEligibilityReason, "already_used")
|
||||
if payload.TrialEligibilityReason != "" {
|
||||
t.Fatalf("trial_eligibility_reason=%q, want empty", payload.TrialEligibilityReason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestEntitlementHandler_CommercialMigrationBlocksTrialEligibility(t *testing.T) {
|
||||
func TestEntitlementHandler_CommercialMigrationDoesNotExposeTrialStartReason(t *testing.T) {
|
||||
baseDir := t.TempDir()
|
||||
mtp := config.NewMultiTenantPersistence(baseDir)
|
||||
orgID := "default"
|
||||
@@ -795,8 +795,8 @@ func TestEntitlementHandler_CommercialMigrationBlocksTrialEligibility(t *testing
|
||||
if payload.TrialEligible {
|
||||
t.Fatalf("trial_eligible=%v, want false", payload.TrialEligible)
|
||||
}
|
||||
if payload.TrialEligibilityReason != "commercial_migration_pending" {
|
||||
t.Fatalf("trial_eligibility_reason=%q, want %q", payload.TrialEligibilityReason, "commercial_migration_pending")
|
||||
if payload.TrialEligibilityReason != "" {
|
||||
t.Fatalf("trial_eligibility_reason=%q, want empty", payload.TrialEligibilityReason)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1556,8 +1556,8 @@ func TestHandleClearLicense_ClearsActiveTrialButPreservesTrialUsedMarker(t *test
|
||||
if payload.TrialEligible {
|
||||
t.Fatalf("payload.trial_eligible=%v, want false", payload.TrialEligible)
|
||||
}
|
||||
if payload.TrialEligibilityReason != "already_used" {
|
||||
t.Fatalf("payload.trial_eligibility_reason=%q, want %q", payload.TrialEligibilityReason, "already_used")
|
||||
if payload.TrialEligibilityReason != "" {
|
||||
t.Fatalf("payload.trial_eligibility_reason=%q, want empty", payload.TrialEligibilityReason)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,8 +35,6 @@ type licenseFeaturesResponse = pkglicensing.LicenseFeaturesResponse
|
||||
type activateLicenseRequestModel = pkglicensing.ActivateLicenseRequest
|
||||
type activateLicenseResponseModel = pkglicensing.ActivateLicenseResponse
|
||||
type activationStateModel = pkglicensing.ActivationState
|
||||
type trialStartDecisionModel = pkglicensing.TrialStartDecision
|
||||
type trialStartDenialReasonModel = pkglicensing.TrialStartDenialReason
|
||||
type billingStoreModel = pkglicensing.BillingStore
|
||||
type billingState = pkglicensing.BillingState
|
||||
type subscriptionState = pkglicensing.SubscriptionState
|
||||
@@ -92,7 +90,6 @@ const (
|
||||
activationKeyPrefixValue = pkglicensing.ActivationKeyPrefix
|
||||
|
||||
// Conversion event type constants for backend-emitted events.
|
||||
conversionEventTrialStarted = pkglicensing.EventTrialStarted
|
||||
conversionEventLicenseActivated = pkglicensing.EventLicenseActivated
|
||||
conversionEventLicenseActivationFailed = pkglicensing.EventLicenseActivationFailed
|
||||
conversionEventCheckoutStarted = pkglicensing.EventCheckoutStarted
|
||||
@@ -152,14 +149,6 @@ func buildTrialBillingStateWithPlanFromLicensing(now time.Time, capabilities []s
|
||||
return pkglicensing.BuildTrialBillingStateWithPlan(now, capabilities, planVersion, duration)
|
||||
}
|
||||
|
||||
func evaluateTrialStartEligibilityFromLicensing(hasActiveLicense bool, existing *billingState) trialStartDecisionModel {
|
||||
return pkglicensing.EvaluateTrialStartEligibility(hasActiveLicense, existing)
|
||||
}
|
||||
|
||||
func trialStartErrorFromLicensing(reason trialStartDenialReasonModel) (code, message string, includeOrgID bool) {
|
||||
return pkglicensing.TrialStartError(reason)
|
||||
}
|
||||
|
||||
func newLicensePersistenceFromLicensing(configDir string) (*licensePersistence, error) {
|
||||
return pkglicensing.NewPersistence(configDir)
|
||||
}
|
||||
|
||||
@@ -315,8 +315,8 @@ func TestGetTenantComponents_PersistsCommercialMigrationState_WhenAutoExchangeFa
|
||||
if payload.TrialEligible {
|
||||
t.Fatalf("trial_eligible=%v, want false", payload.TrialEligible)
|
||||
}
|
||||
if payload.TrialEligibilityReason != "commercial_migration_pending" {
|
||||
t.Fatalf("trial_eligibility_reason=%q, want %q", payload.TrialEligibilityReason, "commercial_migration_pending")
|
||||
if payload.TrialEligibilityReason != "" {
|
||||
t.Fatalf("trial_eligibility_reason=%q, want empty", payload.TrialEligibilityReason)
|
||||
}
|
||||
|
||||
handlers.StopAllBackgroundLoops()
|
||||
|
||||
@@ -19,7 +19,7 @@ type EntitlementPayload = entitlementPayloadModel
|
||||
type RuntimeCapabilitiesPayload = runtimeCapabilitiesPayloadModel
|
||||
|
||||
// CommercialPosturePayload is the canonical non-billing commercial response
|
||||
// for upgrade/trial posture and monitored-system migration guidance.
|
||||
// for upgrade posture and monitored-system migration guidance.
|
||||
type CommercialPosturePayload = commercialPosturePayloadModel
|
||||
|
||||
// LimitStatus represents a quantitative limit with current usage state.
|
||||
@@ -72,13 +72,12 @@ func (h *LicenseHandlers) buildCommercialEntitlementPayload(
|
||||
if existing != nil {
|
||||
payload.CommercialMigration = cloneCommercialMigrationStatusFromLicensing(existing.CommercialMigration)
|
||||
}
|
||||
payload.TrialEligible, payload.TrialEligibilityReason = h.trialStartEligibility(ctx, svc, existing)
|
||||
payload.HostedMode = h != nil && h.hostedMode
|
||||
return payload, nil
|
||||
}
|
||||
|
||||
// HandleEntitlements returns the normalized entitlement payload for the current tenant.
|
||||
// This is the commercial entitlement endpoint for billing, trial, and upgrade presentation.
|
||||
// This is the commercial entitlement endpoint for billing, activation, and upgrade presentation.
|
||||
func (h *LicenseHandlers) HandleEntitlements(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
writeErrorResponse(w, http.StatusMethodNotAllowed, "method_not_allowed", "Method not allowed", nil)
|
||||
@@ -417,30 +416,3 @@ func (h *LicenseHandlers) billingStateForContext(ctx context.Context) *billingSt
|
||||
}
|
||||
return existing
|
||||
}
|
||||
|
||||
func (h *LicenseHandlers) trialStartEligibility(ctx context.Context, svc *licenseService, existing *billingState) (eligible bool, reason string) {
|
||||
if h == nil || h.mtPersistence == nil {
|
||||
return false, "unavailable"
|
||||
}
|
||||
|
||||
orgID := GetOrgID(ctx)
|
||||
if orgID == "" {
|
||||
orgID = "default"
|
||||
}
|
||||
|
||||
if existing == nil {
|
||||
billingStore := config.NewFileBillingStore(h.mtPersistence.BaseDataDir())
|
||||
loaded, err := billingStore.GetBillingState(orgID)
|
||||
if err != nil {
|
||||
return false, "unavailable"
|
||||
}
|
||||
existing = loaded
|
||||
}
|
||||
|
||||
hasActiveLicense := svc != nil && svc.Current() != nil && svc.IsValid()
|
||||
decision := evaluateTrialStartEligibilityFromLicensing(hasActiveLicense, existing)
|
||||
if decision.Allowed {
|
||||
return true, ""
|
||||
}
|
||||
return false, string(decision.Reason)
|
||||
}
|
||||
|
||||
@@ -57,10 +57,12 @@ type EntitlementPayload struct {
|
||||
// GracePeriodEnd is the RFC3339 grace period end timestamp when available.
|
||||
GracePeriodEnd *string `json:"grace_period_end,omitempty"`
|
||||
|
||||
// TrialEligible indicates whether this org can start a self-serve trial right now.
|
||||
// TrialEligible is retained for compatibility with retired self-hosted trial clients.
|
||||
// New self-hosted payloads leave it false; active trial state is exposed through
|
||||
// SubscriptionState, TrialExpiresAt, and TrialDaysRemaining.
|
||||
TrialEligible bool `json:"trial_eligible"`
|
||||
|
||||
// TrialEligibilityReason is set when trial start is denied.
|
||||
// TrialEligibilityReason is retained for compatibility with retired self-hosted trial clients.
|
||||
TrialEligibilityReason string `json:"trial_eligibility_reason,omitempty"`
|
||||
|
||||
// MaxHistoryDays is the maximum metrics history retention in days for the current tier.
|
||||
@@ -95,7 +97,7 @@ type EntitlementPayload struct {
|
||||
}
|
||||
|
||||
// CommercialPosturePayload is the canonical non-billing commercial contract
|
||||
// for upgrade messaging, trial posture, and monitored-system migration copy.
|
||||
// for upgrade messaging and monitored-system migration copy.
|
||||
// It intentionally excludes billing identity, grandfathered plan terms, and
|
||||
// other full-entitlement details that belong only to billing surfaces.
|
||||
type CommercialPosturePayload struct {
|
||||
@@ -114,10 +116,10 @@ type CommercialPosturePayload struct {
|
||||
// TrialDaysRemaining is the number of whole or partial days remaining in trial.
|
||||
TrialDaysRemaining *int `json:"trial_days_remaining,omitempty"`
|
||||
|
||||
// TrialEligible indicates whether this org can start a self-serve trial right now.
|
||||
// TrialEligible is retained for compatibility with retired self-hosted trial clients.
|
||||
TrialEligible bool `json:"trial_eligible"`
|
||||
|
||||
// TrialEligibilityReason is set when trial start is denied.
|
||||
// TrialEligibilityReason is retained for compatibility with retired self-hosted trial clients.
|
||||
TrialEligibilityReason string `json:"trial_eligibility_reason,omitempty"`
|
||||
|
||||
// OverflowDaysRemaining is set when the onboarding overflow (+1 host) is active.
|
||||
|
||||
@@ -86,6 +86,12 @@ func TestBuildEntitlementPayload_FreeTier(t *testing.T) {
|
||||
if len(payload.UpgradeReasons) == 0 {
|
||||
t.Fatalf("expected upgrade reasons for free tier")
|
||||
}
|
||||
if payload.TrialEligible {
|
||||
t.Fatal("expected retired self-hosted trial eligibility to stay false")
|
||||
}
|
||||
if payload.TrialEligibilityReason != "" {
|
||||
t.Fatalf("TrialEligibilityReason=%q, want empty", payload.TrialEligibilityReason)
|
||||
}
|
||||
for _, reason := range payload.UpgradeReasons {
|
||||
if reason.ActionURL == "" {
|
||||
t.Fatalf("expected action_url for reason %q", reason.Key)
|
||||
|
||||
@@ -7,65 +7,6 @@ import (
|
||||
|
||||
const DefaultTrialDuration = 14 * 24 * time.Hour
|
||||
|
||||
type TrialStartDenialReason string
|
||||
|
||||
const (
|
||||
TrialStartAllowed TrialStartDenialReason = ""
|
||||
TrialStartDeniedLicense TrialStartDenialReason = "license_active"
|
||||
TrialStartDeniedAlreadyUsed TrialStartDenialReason = "already_used"
|
||||
TrialStartDeniedSubscription TrialStartDenialReason = "subscription_active"
|
||||
TrialStartDeniedMigrationPending TrialStartDenialReason = "commercial_migration_pending"
|
||||
TrialStartDeniedMigrationFailed TrialStartDenialReason = "commercial_migration_failed"
|
||||
)
|
||||
|
||||
type TrialStartDecision struct {
|
||||
Allowed bool
|
||||
Reason TrialStartDenialReason
|
||||
}
|
||||
|
||||
func EvaluateTrialStartEligibility(hasActiveLicense bool, existing *BillingState) TrialStartDecision {
|
||||
if hasActiveLicense {
|
||||
return TrialStartDecision{Allowed: false, Reason: TrialStartDeniedLicense}
|
||||
}
|
||||
if existing == nil {
|
||||
return TrialStartDecision{Allowed: true, Reason: TrialStartAllowed}
|
||||
}
|
||||
if existing.CommercialMigration != nil {
|
||||
switch existing.CommercialMigration.State {
|
||||
case CommercialMigrationStatePending:
|
||||
return TrialStartDecision{Allowed: false, Reason: TrialStartDeniedMigrationPending}
|
||||
case CommercialMigrationStateFailed:
|
||||
return TrialStartDecision{Allowed: false, Reason: TrialStartDeniedMigrationFailed}
|
||||
}
|
||||
}
|
||||
if existing.TrialStartedAt != nil {
|
||||
return TrialStartDecision{Allowed: false, Reason: TrialStartDeniedAlreadyUsed}
|
||||
}
|
||||
switch existing.SubscriptionState {
|
||||
case SubStateActive, SubStateGrace, SubStateSuspended:
|
||||
return TrialStartDecision{Allowed: false, Reason: TrialStartDeniedSubscription}
|
||||
default:
|
||||
return TrialStartDecision{Allowed: true, Reason: TrialStartAllowed}
|
||||
}
|
||||
}
|
||||
|
||||
func TrialStartError(reason TrialStartDenialReason) (code, message string, includeOrgID bool) {
|
||||
switch reason {
|
||||
case TrialStartDeniedLicense:
|
||||
return "trial_not_available", "Trial cannot be started while a license is active", false
|
||||
case TrialStartDeniedAlreadyUsed:
|
||||
return "trial_already_used", "Trial has already been used for this organization", true
|
||||
case TrialStartDeniedSubscription:
|
||||
return "trial_not_available", "Trial cannot be started while a subscription is active", true
|
||||
case TrialStartDeniedMigrationPending:
|
||||
return "trial_not_available", "Trial cannot be started while a paid v5 license migration is pending", false
|
||||
case TrialStartDeniedMigrationFailed:
|
||||
return "trial_not_available", "Trial cannot be started until the paid v5 license migration is resolved", false
|
||||
default:
|
||||
return "", "", false
|
||||
}
|
||||
}
|
||||
|
||||
func TrialWindow(now time.Time, duration time.Duration) (startedAt, endsAt int64) {
|
||||
if duration <= 0 {
|
||||
duration = DefaultTrialDuration
|
||||
|
||||
@@ -5,169 +5,6 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestEvaluateTrialStartEligibility(t *testing.T) {
|
||||
now := time.Unix(1700000000, 0)
|
||||
started := now.Add(-24 * time.Hour).Unix()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
hasActiveLicense bool
|
||||
existing *BillingState
|
||||
wantAllowed bool
|
||||
wantReason TrialStartDenialReason
|
||||
}{
|
||||
{
|
||||
name: "denied when license active",
|
||||
hasActiveLicense: true,
|
||||
existing: nil,
|
||||
wantAllowed: false,
|
||||
wantReason: TrialStartDeniedLicense,
|
||||
},
|
||||
{
|
||||
name: "allowed with no state",
|
||||
hasActiveLicense: false,
|
||||
existing: nil,
|
||||
wantAllowed: true,
|
||||
wantReason: TrialStartAllowed,
|
||||
},
|
||||
{
|
||||
name: "denied when trial already used",
|
||||
hasActiveLicense: false,
|
||||
existing: &BillingState{TrialStartedAt: &started},
|
||||
wantAllowed: false,
|
||||
wantReason: TrialStartDeniedAlreadyUsed,
|
||||
},
|
||||
{
|
||||
name: "denied on active subscription",
|
||||
hasActiveLicense: false,
|
||||
existing: &BillingState{SubscriptionState: SubStateActive},
|
||||
wantAllowed: false,
|
||||
wantReason: TrialStartDeniedSubscription,
|
||||
},
|
||||
{
|
||||
name: "denied on grace subscription",
|
||||
hasActiveLicense: false,
|
||||
existing: &BillingState{SubscriptionState: SubStateGrace},
|
||||
wantAllowed: false,
|
||||
wantReason: TrialStartDeniedSubscription,
|
||||
},
|
||||
{
|
||||
name: "denied on suspended subscription",
|
||||
hasActiveLicense: false,
|
||||
existing: &BillingState{SubscriptionState: SubStateSuspended},
|
||||
wantAllowed: false,
|
||||
wantReason: TrialStartDeniedSubscription,
|
||||
},
|
||||
{
|
||||
name: "denied when v5 migration is pending",
|
||||
hasActiveLicense: false,
|
||||
existing: &BillingState{CommercialMigration: &CommercialMigrationStatus{
|
||||
Source: CommercialMigrationSourceV5License,
|
||||
State: CommercialMigrationStatePending,
|
||||
}},
|
||||
wantAllowed: false,
|
||||
wantReason: TrialStartDeniedMigrationPending,
|
||||
},
|
||||
{
|
||||
name: "denied when v5 migration failed",
|
||||
hasActiveLicense: false,
|
||||
existing: &BillingState{CommercialMigration: &CommercialMigrationStatus{
|
||||
Source: CommercialMigrationSourceV5License,
|
||||
State: CommercialMigrationStateFailed,
|
||||
}},
|
||||
wantAllowed: false,
|
||||
wantReason: TrialStartDeniedMigrationFailed,
|
||||
},
|
||||
{
|
||||
name: "allowed on expired subscription",
|
||||
hasActiveLicense: false,
|
||||
existing: &BillingState{SubscriptionState: SubStateExpired},
|
||||
wantAllowed: true,
|
||||
wantReason: TrialStartAllowed,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
got := EvaluateTrialStartEligibility(tt.hasActiveLicense, tt.existing)
|
||||
if got.Allowed != tt.wantAllowed {
|
||||
t.Fatalf("allowed=%t, want %t", got.Allowed, tt.wantAllowed)
|
||||
}
|
||||
if got.Reason != tt.wantReason {
|
||||
t.Fatalf("reason=%q, want %q", got.Reason, tt.wantReason)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrialStartError(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
reason TrialStartDenialReason
|
||||
wantCode string
|
||||
wantMessage string
|
||||
wantIncludeOrgID bool
|
||||
}{
|
||||
{
|
||||
name: "license active",
|
||||
reason: TrialStartDeniedLicense,
|
||||
wantCode: "trial_not_available",
|
||||
wantMessage: "Trial cannot be started while a license is active",
|
||||
wantIncludeOrgID: false,
|
||||
},
|
||||
{
|
||||
name: "already used",
|
||||
reason: TrialStartDeniedAlreadyUsed,
|
||||
wantCode: "trial_already_used",
|
||||
wantMessage: "Trial has already been used for this organization",
|
||||
wantIncludeOrgID: true,
|
||||
},
|
||||
{
|
||||
name: "subscription active",
|
||||
reason: TrialStartDeniedSubscription,
|
||||
wantCode: "trial_not_available",
|
||||
wantMessage: "Trial cannot be started while a subscription is active",
|
||||
wantIncludeOrgID: true,
|
||||
},
|
||||
{
|
||||
name: "migration pending",
|
||||
reason: TrialStartDeniedMigrationPending,
|
||||
wantCode: "trial_not_available",
|
||||
wantMessage: "Trial cannot be started while a paid v5 license migration is pending",
|
||||
wantIncludeOrgID: false,
|
||||
},
|
||||
{
|
||||
name: "migration failed",
|
||||
reason: TrialStartDeniedMigrationFailed,
|
||||
wantCode: "trial_not_available",
|
||||
wantMessage: "Trial cannot be started until the paid v5 license migration is resolved",
|
||||
wantIncludeOrgID: false,
|
||||
},
|
||||
{
|
||||
name: "unknown",
|
||||
reason: TrialStartAllowed,
|
||||
wantCode: "",
|
||||
wantMessage: "",
|
||||
wantIncludeOrgID: false,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
code, message, includeOrgID := TrialStartError(tt.reason)
|
||||
if code != tt.wantCode {
|
||||
t.Fatalf("code=%q, want %q", code, tt.wantCode)
|
||||
}
|
||||
if message != tt.wantMessage {
|
||||
t.Fatalf("message=%q, want %q", message, tt.wantMessage)
|
||||
}
|
||||
if includeOrgID != tt.wantIncludeOrgID {
|
||||
t.Fatalf("includeOrgID=%t, want %t", includeOrgID, tt.wantIncludeOrgID)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestTrialWindow(t *testing.T) {
|
||||
now := time.Unix(1700000000, 0)
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ type EntitlementPayload = {
|
||||
type ExpectedCopy = {
|
||||
title: RegExp;
|
||||
bodyFragments: RegExp[];
|
||||
trialReason: string;
|
||||
};
|
||||
|
||||
const expectedState = process.env.PULSE_E2E_EXPECT_COMMERCIAL_MIGRATION_STATE || '';
|
||||
@@ -98,8 +97,7 @@ function expectedCopyFor(state: string, reason: string, action: string): Expecte
|
||||
|
||||
return {
|
||||
title: /v5 license migration pending/i,
|
||||
bodyFragments: [reasonFragment, actionFragment, /new pro trial stays blocked/i],
|
||||
trialReason: 'commercial_migration_pending',
|
||||
bodyFragments: [reasonFragment, actionFragment],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -118,8 +116,7 @@ function expectedCopyFor(state: string, reason: string, action: string): Expecte
|
||||
|
||||
return {
|
||||
title: /v5 license migration needs attention/i,
|
||||
bodyFragments: [reasonFragment, actionFragment, /new pro trial stays blocked/i],
|
||||
trialReason: 'commercial_migration_failed',
|
||||
bodyFragments: [reasonFragment, actionFragment],
|
||||
};
|
||||
}
|
||||
|
||||
@@ -173,7 +170,7 @@ test.describe.serial('v5 commercial migration notice', () => {
|
||||
expect(entitlements.commercial_migration?.reason).toBe(expectedReason);
|
||||
expect(entitlements.commercial_migration?.recommended_action).toBe(expectedAction);
|
||||
expect(entitlements.trial_eligible).toBe(false);
|
||||
expect(entitlements.trial_eligibility_reason).toBe(expectedCopy.trialReason);
|
||||
expect(entitlements.trial_eligibility_reason || '').toBe('');
|
||||
});
|
||||
|
||||
test('Pro settings renders the expected migration state', async ({ page }) => {
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { expect, test } from '@playwright/test';
|
||||
import { ensureAuthenticated } from './helpers';
|
||||
|
||||
const FREE_TRIAL_ELIGIBLE_ENTITLEMENTS = {
|
||||
const FREE_SELF_HOSTED_ENTITLEMENTS = {
|
||||
capabilities: [],
|
||||
limits: [],
|
||||
subscription_state: 'active',
|
||||
upgrade_reasons: [],
|
||||
tier: 'free',
|
||||
trial_eligible: true,
|
||||
trial_eligible: false,
|
||||
};
|
||||
|
||||
const SELF_HOSTED_SECURITY_STATUS = {
|
||||
@@ -72,7 +72,7 @@ test.describe.serial('Self-hosted paid prompt visibility', () => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(FREE_TRIAL_ELIGIBLE_ENTITLEMENTS),
|
||||
body: JSON.stringify(FREE_SELF_HOSTED_ENTITLEMENTS),
|
||||
});
|
||||
});
|
||||
|
||||
@@ -80,7 +80,7 @@ test.describe.serial('Self-hosted paid prompt visibility', () => {
|
||||
await route.fulfill({
|
||||
status: 200,
|
||||
contentType: 'application/json',
|
||||
body: JSON.stringify(FREE_TRIAL_ELIGIBLE_ENTITLEMENTS),
|
||||
body: JSON.stringify(FREE_SELF_HOSTED_ENTITLEMENTS),
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@ const INACTIVE_ENTITLEMENTS = {
|
||||
subscription_state: 'expired',
|
||||
upgrade_reasons: [],
|
||||
tier: 'free',
|
||||
trial_eligible: true,
|
||||
trial_eligible: false,
|
||||
hosted_mode: false,
|
||||
valid: false,
|
||||
};
|
||||
@@ -56,7 +56,7 @@ const INACTIVE_COMMERCIAL_POSTURE = {
|
||||
subscription_state: 'expired',
|
||||
upgrade_reasons: [],
|
||||
tier: 'free',
|
||||
trial_eligible: true,
|
||||
trial_eligible: false,
|
||||
monitored_system_capacity: null,
|
||||
monitored_system_continuity: null,
|
||||
has_migration_gap: false,
|
||||
|
||||
Reference in New Issue
Block a user