fix(portal): confirm applied provider plan on refreshed main

Carry the checked checkout-return race correction onto the current main snapshot. Await the refreshed licence identity before announcing the paid plan; ignore overtaken plan reads after restart.

Contract-Neutral: Restores the existing provider-hosted Plan confirmation without changing plan, entitlement, route, price, or role contracts.
Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-24 03:05:07 +01:00
5 changed files with 146 additions and 34 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
{
"source_hash": "02b2a130bad03a89d6b916726206d5941bd0b23de5a2475f534aaf69419d7aba",
"source_hash": "33ddecf9ec76e0a22e0facf6971b8f0825600e56007db5e7f1f4e2cbe1dd2621",
"build_inputs": [
"package.json",
"tsconfig.json",
+39 -14
View File
@@ -2826,6 +2826,19 @@
return setTimeout(fn, ms);
};
var view = { loading: false, error: "", plan: null, cycle: "monthly", busy: "", notice: "" };
var planEpoch = 0;
var applying = null;
function isApplied(plan, target) {
return !!target.plan_version && plan.plan_version === target.plan_version && (!target.license_id || plan.license_id === target.license_id) && (!target.expires_at || plan.expires_at === target.expires_at);
}
function finishApplying(plan) {
applying = null;
planEpoch += 1;
view.plan = plan;
view.error = "";
view.notice = "Your " + providerPlanName(plan.plan_version) + " plan is active.";
render();
}
function account() {
var bootstrap = deps.store.getBootstrap();
if (bootstrap.provider_hosted_mode !== true || !bootstrap.authenticated) return null;
@@ -2841,47 +2854,59 @@
async function load() {
var current = account();
if (!current) return;
var epoch = planEpoch;
view.loading = true;
render();
try {
view.plan = await deps.api.fetchPlan(current.id);
var plan = await deps.api.fetchPlan(current.id);
if (epoch !== planEpoch) return;
if (applying) {
if (isApplied(plan, applying)) finishApplying(plan);
return;
}
view.plan = plan;
view.error = "";
} catch (error) {
view.error = error instanceof Error && error.message ? error.message : "Your plan could not be loaded.";
if (epoch === planEpoch) {
view.error = error instanceof Error && error.message ? error.message : "Your plan could not be loaded.";
}
} finally {
view.loading = false;
render();
}
}
function waitForAppliedPlan(previousPlan, attempt) {
function waitForAppliedPlan(target, attempt) {
if (applying !== target) return;
var current = account();
if (!current) return;
deps.api.fetchPlan(current.id).then(function(plan) {
if (plan.plan_version !== previousPlan) {
view.plan = plan;
view.error = "";
view.notice = "Your " + providerPlanName(plan.plan_version) + " plan is active.";
render();
if (applying !== target) return;
if (isApplied(plan, target)) {
finishApplying(plan);
return;
}
throw new Error("plan not applied yet");
}).catch(function() {
if (applying !== target) return;
if (attempt + 1 >= RESTART_POLL_ATTEMPTS) {
view.notice = "Your plan should be active now. Open this page again if it still shows the old plan.";
applying = null;
planEpoch += 1;
view.notice = "The updated plan could not be confirmed yet. Open this page again or use the apply button.";
render();
return;
}
later(function() {
waitForAppliedPlan(previousPlan, attempt + 1);
waitForAppliedPlan(target, attempt + 1);
}, RESTART_POLL_MS);
});
}
function applyRestart() {
var previousPlan = view.plan ? view.plan.plan_version : "";
function applyRestart(target) {
applying = target;
planEpoch += 1;
view.notice = "Your plan is being applied. This takes a few seconds.";
render();
later(function() {
waitForAppliedPlan(previousPlan, 0);
waitForAppliedPlan(target, 0);
}, RESTART_SETTLE_MS);
}
async function refresh(manual) {
@@ -2892,7 +2917,7 @@
try {
var result = await deps.api.refreshLicense(current.id);
if (result.restart_scheduled) {
applyRestart();
applyRestart(result);
return true;
}
if (manual) {
+2 -2
View File
@@ -1,5 +1,5 @@
import type { PortalBootstrapData, PortalAccessMember } from './types';
import type { ProviderPlanAPI, ProviderPlanState } from './provider_plan';
import type { ProviderPlanAPI, ProviderPlanRefreshResult, ProviderPlanState } from './provider_plan';
interface PortalAPIContext {
getBootstrap(): PortalBootstrapData;
@@ -242,7 +242,7 @@ export function createPortalAPI(context: PortalAPIContext): PortalAPI {
}, 'Billing is unavailable right now.');
},
refreshLicense: function(accountID: string) {
return request<{ status?: string; changed?: boolean; restart_scheduled?: boolean }>(accountURL(accountID, '/provider-msp/license/refresh'), {
return request<ProviderPlanRefreshResult>(accountURL(accountID, '/provider-msp/license/refresh'), {
method: 'POST',
}, 'The licence could not be refreshed right now.');
},
@@ -200,7 +200,7 @@ describe('installProviderPlan', () => {
it('applies a purchase after checkout returns and shows the new plan once the platform restarts', async () => {
api.refreshLicense = vi.fn()
.mockResolvedValueOnce({ status: 'no_paid_subscription' })
.mockResolvedValueOnce({ status: 'active', changed: true, restart_scheduled: true });
.mockResolvedValueOnce({ status: 'active', changed: true, restart_scheduled: true, plan_version: 'msp_solo' });
const { store, controller } = install(providerBootstrap(), '?provider_msp_checkout=complete');
await controller.load();
expect(store.getShellState().activeSection).toBe('billing');
@@ -225,11 +225,60 @@ describe('installProviderPlan', () => {
expect(document.getElementById(PROVIDER_PLAN_ROOT_ID)!.innerHTML).toContain('Manage billing');
});
it('does not announce the old plan when refresh beats the initial plan request', async () => {
let resolveInitial!: (plan: ProviderPlanState) => void;
api.fetchPlan = vi.fn()
.mockImplementationOnce(() => new Promise<ProviderPlanState>((resolve) => { resolveInitial = resolve; }))
.mockResolvedValueOnce(evaluationPlan())
.mockResolvedValueOnce(evaluationPlan({ plan_version: 'msp_solo', evaluation: false, workspace_limit: 3 }));
api.refreshLicense = vi.fn().mockResolvedValue({
status: 'active', changed: true, restart_scheduled: true, plan_version: 'msp_solo',
});
const { controller } = install(providerBootstrap(), '?provider_msp_checkout=complete');
const initialLoad = controller.load();
await flush();
expect(controller.view().notice).toContain('being applied');
timers.shift()!();
await flush();
expect(controller.view().notice).not.toContain('plan is active');
expect(timers).toHaveLength(1);
timers.shift()!();
await flush();
expect(controller.view().notice).toBe('Your Solo plan is active.');
resolveInitial(evaluationPlan());
await initialLoad;
expect(controller.view().plan?.plan_version).toBe('msp_solo');
});
it('waits for the renewed licence date when the plan name is unchanged', async () => {
const original = evaluationPlan({ plan_version: 'msp_solo', evaluation: false, expires_at: '2026-10-10T00:00:00Z' });
const renewed = evaluationPlan({ plan_version: 'msp_solo', evaluation: false, expires_at: '2026-11-10T00:00:00Z' });
let served = original;
api.fetchPlan = vi.fn().mockImplementation(() => Promise.resolve(served));
api.refreshLicense = vi.fn().mockResolvedValue({
status: 'active', changed: true, restart_scheduled: true,
plan_version: 'msp_solo', expires_at: renewed.expires_at,
});
const { controller } = install(providerBootstrap(), '?provider_msp_checkout=billing');
await controller.load();
await flush();
timers.shift()!();
await flush();
expect(controller.view().notice).not.toContain('plan is active');
served = renewed;
timers.shift()!();
await flush();
expect(controller.view().plan?.expires_at).toBe(renewed.expires_at);
expect(controller.view().notice).toBe('Your Solo plan is active.');
});
it('applies a plan changed in Manage billing when the provider comes back', async () => {
api.fetchPlan = vi.fn().mockResolvedValue(evaluationPlan({ plan_version: 'msp_solo', evaluation: false, workspace_limit: 3 }));
api.refreshLicense = vi.fn()
.mockResolvedValueOnce({ status: 'active', changed: false })
.mockResolvedValueOnce({ status: 'active', changed: true, restart_scheduled: true });
.mockResolvedValueOnce({ status: 'active', changed: true, restart_scheduled: true, plan_version: 'msp_starter' });
const { store, controller } = install(providerBootstrap(), '?provider_msp_checkout=billing');
await controller.load();
expect(store.getShellState().activeSection).toBe('billing');
@@ -198,7 +198,16 @@ export interface ProviderPlanAPI {
fetchPlan(accountID: string): Promise<ProviderPlanState>;
startCheckout(accountID: string, planVersion: string, billingCycle: string): Promise<{ url?: string }>;
openBillingPortal(accountID: string): Promise<{ url?: string }>;
refreshLicense(accountID: string): Promise<{ status?: string; changed?: boolean; restart_scheduled?: boolean }>;
refreshLicense(accountID: string): Promise<ProviderPlanRefreshResult>;
}
export interface ProviderPlanRefreshResult {
status?: string;
changed?: boolean;
restart_scheduled?: boolean;
plan_version?: string;
license_id?: string;
expires_at?: string;
}
export interface ProviderPlanDeps {
@@ -241,6 +250,23 @@ export function installProviderPlan(deps: ProviderPlanDeps): ProviderPlanControl
var navigate = deps.navigate || function(url: string) { window.location.assign(url); };
var later = deps.setTimeoutFn || function(fn: () => void, ms: number) { return setTimeout(fn, ms); };
var view: ProviderPlanView = { loading: false, error: '', plan: null, cycle: 'monthly', busy: '', notice: '' };
var planEpoch = 0;
var applying: ProviderPlanRefreshResult | null = null;
function isApplied(plan: ProviderPlanState, target: ProviderPlanRefreshResult): boolean {
return !!target.plan_version && plan.plan_version === target.plan_version &&
(!target.license_id || plan.license_id === target.license_id) &&
(!target.expires_at || plan.expires_at === target.expires_at);
}
function finishApplying(plan: ProviderPlanState) {
applying = null;
planEpoch += 1; // Ignore plan requests started before the restart completed.
view.plan = plan;
view.error = '';
view.notice = 'Your ' + providerPlanName(plan.plan_version) + ' plan is active.';
render();
}
function account(): PortalAccountSummary | null {
var bootstrap = deps.store.getBootstrap();
@@ -259,46 +285,58 @@ export function installProviderPlan(deps: ProviderPlanDeps): ProviderPlanControl
async function load() {
var current = account();
if (!current) return;
var epoch = planEpoch;
view.loading = true;
render();
try {
view.plan = await deps.api.fetchPlan(current.id);
var plan = await deps.api.fetchPlan(current.id);
if (epoch !== planEpoch) return;
if (applying) {
if (isApplied(plan, applying)) finishApplying(plan);
return;
}
view.plan = plan;
view.error = '';
} catch (error) {
view.error = error instanceof Error && error.message ? error.message : 'Your plan could not be loaded.';
if (epoch === planEpoch) {
view.error = error instanceof Error && error.message ? error.message : 'Your plan could not be loaded.';
}
} finally {
view.loading = false;
render();
}
}
function waitForAppliedPlan(previousPlan: string, attempt: number) {
function waitForAppliedPlan(target: ProviderPlanRefreshResult, attempt: number) {
if (applying !== target) return;
var current = account();
if (!current) return;
deps.api.fetchPlan(current.id).then(function(plan) {
if (plan.plan_version !== previousPlan) {
view.plan = plan;
view.error = '';
view.notice = 'Your ' + providerPlanName(plan.plan_version) + ' plan is active.';
render();
if (applying !== target) return;
if (isApplied(plan, target)) {
finishApplying(plan);
return;
}
throw new Error('plan not applied yet');
}).catch(function() {
if (applying !== target) return;
if (attempt + 1 >= RESTART_POLL_ATTEMPTS) {
view.notice = 'Your plan should be active now. Open this page again if it still shows the old plan.';
applying = null;
planEpoch += 1;
view.notice = 'The updated plan could not be confirmed yet. Open this page again or use the apply button.';
render();
return;
}
later(function() { waitForAppliedPlan(previousPlan, attempt + 1); }, RESTART_POLL_MS);
later(function() { waitForAppliedPlan(target, attempt + 1); }, RESTART_POLL_MS);
});
}
function applyRestart() {
var previousPlan = view.plan ? view.plan.plan_version : '';
function applyRestart(target: ProviderPlanRefreshResult) {
applying = target;
planEpoch += 1;
view.notice = 'Your plan is being applied. This takes a few seconds.';
render();
later(function() { waitForAppliedPlan(previousPlan, 0); }, RESTART_SETTLE_MS);
later(function() { waitForAppliedPlan(target, 0); }, RESTART_SETTLE_MS);
}
async function refresh(manual: boolean): Promise<boolean> {
@@ -309,7 +347,7 @@ export function installProviderPlan(deps: ProviderPlanDeps): ProviderPlanControl
try {
var result = await deps.api.refreshLicense(current.id);
if (result.restart_scheduled) {
applyRestart();
applyRestart(result);
return true;
}
if (manual) {