Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion frontend/src/__tests__/api-insurance.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { getInsuranceComparison, getInsuranceStatus } from '../api/insurance';
import { getInsuranceComparison, getInsuranceStatus, getRecommendationInsuranceComparison } from '../api/insurance';
import { apiRequest } from '../api/client';

jest.mock('../api/client', () => ({ apiRequest: jest.fn() }));
Expand All @@ -16,3 +16,8 @@ describe('insurance api', () => {
expect(apiRequest).toHaveBeenCalledWith('/insurance/comparison');
});
});

test('recommendation comparison encodes the exact stored ID', async () => {
await getRecommendationInsuranceComparison('stored/id?&');
expect(apiRequest).toHaveBeenLastCalledWith('/insurance/comparison?recommendation_id=stored%2Fid%3F%26');
});
153 changes: 153 additions & 0 deletions frontend/src/__tests__/recommendation-archera-comparison.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
import { mountRecommendationArchera } from '../recommendation-archera-comparison';
import { getInsuranceStatus, getRecommendationInsuranceComparison } from '../api/insurance';
import type { RecommendationArcheraComparison, RecommendationArcheraOffer } from '../api/types';
import { canAccess } from '../permissions';

jest.mock('../api/insurance', () => ({
getInsuranceStatus: jest.fn(), getRecommendationInsuranceComparison: jest.fn(),
}));
jest.mock('../permissions', () => ({ canAccess: jest.fn() }));

const figures: RecommendationArcheraOffer = {
offer_id: '<img src=x onerror=alert(1)>', provider: 'aws', commitment_type: 'savings_plan',
hourly_selected_commitment: '0.500000000000000001', native_duration_seconds: '31536000',
native_plan_type: 'EC2Instance', native_instance_family: 'm7i', effective_contract_term: 'one_year_gris',
payment_option: 'no_upfront', lease_attached: false, upfront_one_time_cost: '1200',
monthly: {
monthly_730h_commitment_cost_total: '110', monthly_730h_cloud_provider_cost: '100',
monthly_730h_archera_premium: '10', monthly_730h_gross_savings: null,
monthly_730h_net_savings: '20', monthly_730h_covered_on_demand_cost: '130',
},
};
const result: RecommendationArcheraComparison = {
recommendation_id: 'stored/id', status: 'resource_associated', reason: '',
amount_provenance: 'stored_decimal_only', economic_status: 'unavailable_source_precision',
fetched_at: '2026-10-15T12:00:00Z', currency: null, comparable_delta: null,
currency_note: 'Vendor currency unavailable', premium_included: true,
current: figures, candidates: [
{ offer_id: 'same', status: 'vendor_alternative', reason: '', native_identity_status: 'unavailable',
figures: { ...figures, native_duration_seconds: null, native_plan_type: null, native_instance_family: null } },
{ offer_id: 'same', status: 'unavailable', reason: 'amount_unproven', native_identity_status: 'unavailable', figures: null },
],
non_gating_disclosure: 'Purchases do not depend on Archera.',
sponsorship_disclosure: 'Sponsored by Archera.',
};
const flush = async (): Promise<void> => { await Promise.resolve(); await Promise.resolve(); };
const action = (): HTMLButtonElement => document.querySelector<HTMLButtonElement>('[data-action="archera"]')!;

beforeEach(() => {
document.body.innerHTML = '<table><tbody><tr class="recommendation-row" data-rec-id="stored/id"><td></td></tr></tbody></table>';
jest.clearAllMocks();
(canAccess as jest.Mock).mockReturnValue(true);
(getInsuranceStatus as jest.Mock).mockResolvedValue({ configured: true, missing: [] });
(getRecommendationInsuranceComparison as jest.Mock).mockResolvedValue(result);
});

test('explicit row action uses the stored ID without comparison on mount or repeated mount', async () => {
await mountRecommendationArchera(document.body);
await mountRecommendationArchera(document.body);
expect(document.querySelectorAll('[data-action="archera"]')).toHaveLength(1);
expect(getRecommendationInsuranceComparison).not.toHaveBeenCalled();
action().click();
await flush();
expect(getRecommendationInsuranceComparison).toHaveBeenCalledWith('stored/id');
expect(document.querySelector('[role="dialog"]')).not.toBeNull();
});

test.each(['permission', 'unconfigured', 'status_error'])('%s does not expose the action or fetch comparison', async mode => {
if (mode === 'permission') (canAccess as jest.Mock).mockReturnValue(false);
if (mode === 'unconfigured') (getInsuranceStatus as jest.Mock).mockResolvedValue({ configured: false });
if (mode === 'status_error') (getInsuranceStatus as jest.Mock).mockRejectedValue(new Error('offline'));
await mountRecommendationArchera(document.body);
expect(document.querySelector('button')).toBeNull();
expect(getRecommendationInsuranceComparison).not.toHaveBeenCalled();
if (mode === 'permission') expect(getInsuranceStatus).not.toHaveBeenCalled();
});

test('loading deduplicates across rerenders and closing never reopens the response', async () => {
let resolve!: (value: RecommendationArcheraComparison) => void;
(getRecommendationInsuranceComparison as jest.Mock).mockReturnValue(new Promise(r => { resolve = r; }));
await mountRecommendationArchera(document.body);
action().click();
expect(action().disabled).toBe(true);
expect(document.body.textContent).toContain('Loading Archera alternatives');
const row = document.querySelector('tr')!;
row.querySelector('button')!.remove();
await mountRecommendationArchera(document.body);
action().click();
expect(getRecommendationInsuranceComparison).toHaveBeenCalledTimes(1);
(document.querySelector('[role="dialog"] button') as HTMLButtonElement).click();
resolve(result);
await flush();
expect(document.querySelector('[role="dialog"]')).toBeNull();
});

test('native exact figures, null economics, lease distinctions and disclosures remain text', async () => {
await mountRecommendationArchera(document.body);
action().click();
await flush();
const dialog = document.querySelector('[role="dialog"]')!;
expect(dialog.querySelector('img')).toBeNull();
for (const text of [figures.offer_id, '0.500000000000000001', '31536000', 'one_year_gris',
'1200', '110', 'Unknown', 'Original provider precision is unproven', 'Currency: unknown',
'Comparable cost difference: unavailable', result.non_gating_disclosure, result.sponsorship_disclosure]) {
expect(dialog.textContent).toContain(text);
}
expect(dialog.textContent).not.toMatch(/\$|USD/);
expect(dialog.querySelectorAll('h3')).toHaveLength(2);
expect(dialog.textContent).toContain('Alternative 2: unavailable. amount unproven');
expect(dialog.textContent).toContain('native product identity is unavailable');
});

test.each([[400, 'invalid'], [403, 'not authorised'], [404, 'no longer exists'], [409, 'changed or is ambiguous'],
[422, 'cannot be compared'], [502, 'unavailable or returned'], [503, 'not available'], [504, 'did not respond']])(
'status %i shows a fixed message and never the response text', async (status, text) => {
(getRecommendationInsuranceComparison as jest.Mock).mockRejectedValue(Object.assign(new Error('<img src=bad>'), { status }));
await mountRecommendationArchera(document.body);
action().click();
await flush();
const dialog = document.querySelector('[role="dialog"]')!;
expect(dialog.textContent).toContain(text);
expect(dialog.textContent).not.toContain('<img');
expect(dialog.querySelectorAll('h3')).toHaveLength(0);
expect(action().disabled).toBe(false);
});

test.each(['unmatched', 'error', 'wrong_id'])('%s never presents unrelated figures', async mode => {
if (mode === 'unmatched') (getRecommendationInsuranceComparison as jest.Mock).mockResolvedValue({
...result, status: 'unavailable', reason: 'identity_unproven', current: null, candidates: [],
});
if (mode === 'error') (getRecommendationInsuranceComparison as jest.Mock).mockRejectedValue(new Error('<img src=bad>'));
if (mode === 'wrong_id') (getRecommendationInsuranceComparison as jest.Mock).mockResolvedValue({ ...result, recommendation_id: 'another' });
await mountRecommendationArchera(document.body);
action().click();
await flush();
const dialog = document.querySelector('[role="dialog"]')!;
expect(dialog.querySelectorAll('h3')).toHaveLength(0);
expect(dialog.querySelector('img')).toBeNull();
expect(action().disabled).toBe(false);
if (mode === 'unmatched') expect(dialog.textContent).toContain('identity unproven');
if (mode === 'error') expect(dialog.textContent).toContain('Archera alternatives unavailable.');
if (mode === 'wrong_id') expect(dialog.textContent).toContain('changed or is ambiguous');
});

test('closing while loading restores focus without allowing a duplicate request', async () => {
let resolve!: (value: RecommendationArcheraComparison) => void;
(getRecommendationInsuranceComparison as jest.Mock).mockReturnValue(new Promise(r => { resolve = r; }));
await mountRecommendationArchera(document.body);
const button = action();
button.focus();
button.click();
(document.querySelector('[role="dialog"] button') as HTMLButtonElement).click();
expect(document.activeElement).toBe(button);
button.click();
expect(getRecommendationInsuranceComparison).toHaveBeenCalledTimes(1);
resolve(result);
await flush();
});

test('the module imports no purchase or plan API', () => {
const source = jest.requireActual<typeof import('fs')>('fs').readFileSync(require.resolve('../recommendation-archera-comparison'), 'utf8');
const imports = [...source.matchAll(/from '([^']+)'/g)].map(m => m[1]);
expect(imports).toEqual(['./api/insurance', './api/types', './permissions', './modal']);
});
6 changes: 5 additions & 1 deletion frontend/src/api/insurance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
*/

import { apiRequest } from './client';
import type { ArcheraComparison, InsuranceStatus } from './types';
import type { ArcheraComparison, InsuranceStatus, RecommendationArcheraComparison } from './types';

/** Reports whether the comparison is configured. Makes no vendor call. */
export async function getInsuranceStatus(): Promise<InsuranceStatus> {
Expand All @@ -14,3 +14,7 @@ export async function getInsuranceStatus(): Promise<InsuranceStatus> {
export async function getInsuranceComparison(): Promise<ArcheraComparison> {
return apiRequest<ArcheraComparison>('/insurance/comparison');
}

export async function getRecommendationInsuranceComparison(id: string): Promise<RecommendationArcheraComparison> {
return apiRequest<RecommendationArcheraComparison>(`/insurance/comparison?recommendation_id=${encodeURIComponent(id)}`);
}
38 changes: 38 additions & 0 deletions frontend/src/api/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1056,3 +1056,41 @@ export interface ArcheraComparison {
non_gating_disclosure: string;
sponsorship_disclosure: string;
}

export interface RecommendationArcheraOffer {
offer_id: string;
provider: string;
commitment_type: string;
hourly_selected_commitment: string | null;
native_duration_seconds: string | null;
native_plan_type: string | null;
native_instance_family: string | null;
effective_contract_term: string | null;
payment_option: string | null;
lease_attached: boolean;
monthly: ArcheraFinancials;
upfront_one_time_cost: string | null;
}

export interface RecommendationArcheraComparison {
recommendation_id: string;
status: string;
reason: string;
amount_provenance: string;
economic_status: string;
fetched_at: string;
currency: string | null;
currency_note: string;
comparable_delta: string | null;
premium_included: boolean;
current: RecommendationArcheraOffer | null;
candidates: Array<{
offer_id: string;
status: string;
reason: string;
native_identity_status: string;
figures: RecommendationArcheraOffer | null;
}>;
non_gating_disclosure: string;
sponsorship_disclosure: string;
}
130 changes: 130 additions & 0 deletions frontend/src/recommendation-archera-comparison.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import { getInsuranceStatus, getRecommendationInsuranceComparison } from './api/insurance';
import type { RecommendationArcheraComparison, RecommendationArcheraOffer } from './api/types';
import { canAccess } from './permissions';
import { openModal, closeModal } from './modal';

const pending = new Set<string>();

const FAILURES: Record<number, string> = {
400: 'The recommendation request was invalid.',
403: 'You are not authorised to view Archera alternatives.',
404: 'This recommendation no longer exists. Refresh the list.',
409: 'The recommendation changed or is ambiguous. Refresh the list and try again.',
422: 'This recommendation type cannot be compared with Archera.',
502: 'Archera is unavailable or returned an unusable response.',
503: 'The comparison service is not available right now.',
504: 'Archera did not respond in time.',
};

function failureText(error: unknown): string {
const status = (error as { status?: number } | null)?.status;
return (status !== undefined && FAILURES[status]) || 'Archera alternatives unavailable.';
}

function node<K extends keyof HTMLElementTagNameMap>(tag: K, text?: string): HTMLElementTagNameMap[K] {
const element = document.createElement(tag);
if (text !== undefined) element.textContent = text;
return element;
}

function offerView(offer: RecommendationArcheraOffer, title: string): HTMLElement {
const section = node('section');
section.append(node('h3', title));
const values: Array<[string, string | null]> = [
['Offer', offer.offer_id],
['Provider / product', `${offer.provider} / ${offer.commitment_type}`],
['Selected hourly commitment', offer.hourly_selected_commitment],
['Native duration (seconds)', offer.native_duration_seconds],
['Native plan type', offer.native_plan_type],
['Native instance family', offer.native_instance_family],
['Effective contract / lease term', offer.effective_contract_term],
['Payment', offer.payment_option],
['Lease attached', offer.lease_attached ? 'Yes' : 'No'],
['Monthly commitment cost (730 hours)', offer.monthly.monthly_730h_commitment_cost_total],
['Monthly provider cost (730 hours)', offer.monthly.monthly_730h_cloud_provider_cost],
['Monthly Archera premium (730 hours)', offer.monthly.monthly_730h_archera_premium],
['Monthly gross savings (730 hours)', offer.monthly.monthly_730h_gross_savings],
['Monthly net savings (730 hours)', offer.monthly.monthly_730h_net_savings],
['Monthly covered on-demand cost (730 hours)', offer.monthly.monthly_730h_covered_on_demand_cost],
['Upfront cost (one-time)', offer.upfront_one_time_cost],
];
const list = node('dl');
for (const [label, value] of values) list.append(node('dt', label), node('dd', value ?? 'Unknown'));
section.append(list);
return section;
}

function resultView(result: RecommendationArcheraComparison): HTMLElement {
const root = node('div');
root.append(node('p', result.status === 'resource_associated'
? 'Resource association found. Comparable economics remain unavailable.'
: `Association unavailable: ${result.reason.replace(/_/g, ' ')}`));
root.append(node('p', `Fetched at ${result.fetched_at}`));
root.append(node('p', result.currency_note));
root.append(node('p', 'Currency: unknown. Comparable cost difference: unavailable.'));
root.append(node('p', 'The stored hourly amount has already passed through floating-point conversion. Original provider precision is unproven; these are native vendor figures, not an equivalent-cost quote.'));
root.append(node('p', `Premium included: ${result.premium_included ? 'Yes' : 'No'}`));
root.append(node('p', result.non_gating_disclosure), node('p', result.sponsorship_disclosure));
if (result.current) root.append(offerView(result.current, 'Current vendor offer'));
for (const [index, candidate] of result.candidates.entries()) {
root.append(node('p', `Alternative ${index + 1}: ${candidate.status.replace(/_/g, ' ')}. ${candidate.reason.replace(/_/g, ' ')}`));
root.append(node('p', 'Alternative native product identity is unavailable; current-offer dimensions are not inherited.'));
if (candidate.figures) root.append(offerView(candidate.figures, `Vendor alternative ${index + 1}`));
}
return root;
}

async function compare(id: string, button: HTMLButtonElement): Promise<void> {
if (pending.has(id) || !canAccess('view', 'recommendations')) return;
pending.add(id);
const modal = node('div');
modal.className = 'modal hidden';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-label', 'Archera recommendation alternatives');
const panel = node('div');
panel.className = 'modal-content modal-wide';
const close = node('button', 'Close');
close.type = 'button';
close.className = 'btn';
close.addEventListener('click', () => closeModal(modal));
const content = node('div', 'Loading Archera alternatives');
content.setAttribute('aria-live', 'polite');
panel.append(node('h2', 'Archera recommendation alternatives'), close, content);
modal.append(panel);
document.body.append(modal);
openModal(modal, { onClose: () => { button.disabled = false; modal.remove(); } });
button.disabled = true;
try {
const result = await getRecommendationInsuranceComparison(id);
if (result.recommendation_id !== id) throw Object.assign(new Error('mismatched recommendation'), { status: 409 });
content.replaceChildren(resultView(result));
} catch (error) {
content.textContent = failureText(error);
} finally {
pending.delete(id);
button.disabled = false;
}
}

export async function mountRecommendationArchera(root: HTMLElement): Promise<void> {
if (!canAccess('view', 'recommendations')) return;
try {
if (!(await getInsuranceStatus()).configured) return;
} catch {
return;
}
for (const row of root.querySelectorAll<HTMLTableRowElement>('tr.recommendation-row[data-rec-id]')) {
const id = row.dataset['recId'];
if (!id || row.querySelector('[data-action="archera"]')) continue;
const button = node('button', 'Archera alternatives');
button.type = 'button';
button.className = 'btn btn-small';
button.dataset['action'] = 'archera';
button.addEventListener('click', event => {
event.stopPropagation();
void compare(id, button);
});
row.cells[0]?.append(button);
}
}
2 changes: 2 additions & 0 deletions frontend/src/recommendations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ import type { RecommendationsResponse, LocalRecommendation, RecommendationsSumma
import { openModal } from './modal';
import { showSkeletonRows, teardownSkeleton } from './lib/skeleton';
import { canAccess } from './permissions';
import { mountRecommendationArchera } from './recommendation-archera-comparison';

// Issue #869: true when the current session can take any action on
// recommendations (purchase or plan). Readonly/viewer sessions have neither
Expand Down Expand Up @@ -5020,6 +5021,7 @@ function renderRecommendationsList(loadedRecs: LocalRecommendation[]): void {
renderRecommendationsList(loadedRecs);
});
});
void mountRecommendationArchera(container);
}

// Resolve a priced override, valid own payment, or priced legacy fallback.
Expand Down
Loading
Loading