diff --git a/frontend/src/__tests__/api-insurance.test.ts b/frontend/src/__tests__/api-insurance.test.ts
index bf0b27ca..cf11625e 100644
--- a/frontend/src/__tests__/api-insurance.test.ts
+++ b/frontend/src/__tests__/api-insurance.test.ts
@@ -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() }));
@@ -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');
+});
diff --git a/frontend/src/__tests__/recommendation-archera-comparison.test.ts b/frontend/src/__tests__/recommendation-archera-comparison.test.ts
new file mode 100644
index 00000000..4d69b745
--- /dev/null
+++ b/frontend/src/__tests__/recommendation-archera-comparison.test.ts
@@ -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: '
', 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 => { await Promise.resolve(); await Promise.resolve(); };
+const action = (): HTMLButtonElement => document.querySelector('[data-action="archera"]')!;
+
+beforeEach(() => {
+ document.body.innerHTML = '';
+ 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('
'), { 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('
{
+ 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('
'));
+ 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('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']);
+});
diff --git a/frontend/src/api/insurance.ts b/frontend/src/api/insurance.ts
index a54cd71a..cd88424d 100644
--- a/frontend/src/api/insurance.ts
+++ b/frontend/src/api/insurance.ts
@@ -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 {
@@ -14,3 +14,7 @@ export async function getInsuranceStatus(): Promise {
export async function getInsuranceComparison(): Promise {
return apiRequest('/insurance/comparison');
}
+
+export async function getRecommendationInsuranceComparison(id: string): Promise {
+ return apiRequest(`/insurance/comparison?recommendation_id=${encodeURIComponent(id)}`);
+}
diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts
index f3ad8bc6..9a0a8903 100644
--- a/frontend/src/api/types.ts
+++ b/frontend/src/api/types.ts
@@ -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;
+}
diff --git a/frontend/src/recommendation-archera-comparison.ts b/frontend/src/recommendation-archera-comparison.ts
new file mode 100644
index 00000000..5d5eb7cb
--- /dev/null
+++ b/frontend/src/recommendation-archera-comparison.ts
@@ -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();
+
+const FAILURES: Record = {
+ 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(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 {
+ 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 {
+ if (!canAccess('view', 'recommendations')) return;
+ try {
+ if (!(await getInsuranceStatus()).configured) return;
+ } catch {
+ return;
+ }
+ for (const row of root.querySelectorAll('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);
+ }
+}
diff --git a/frontend/src/recommendations.ts b/frontend/src/recommendations.ts
index 6f44b1be..bd1b9709 100644
--- a/frontend/src/recommendations.ts
+++ b/frontend/src/recommendations.ts
@@ -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
@@ -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.
diff --git a/frontend/tests-e2e/recommendation-archera.spec.ts b/frontend/tests-e2e/recommendation-archera.spec.ts
new file mode 100644
index 00000000..cf78a29f
--- /dev/null
+++ b/frontend/tests-e2e/recommendation-archera.spec.ts
@@ -0,0 +1,66 @@
+import { test, expect } from '@playwright/test';
+import { mockApi, seedAuth, RECS } from './fixtures/recs';
+
+test('readonly stored row opens native Archera figures without selecting or purchasing', async ({ page }) => {
+ const errors: string[] = [];
+ page.on('pageerror', error => errors.push(error.message));
+ await seedAuth(page);
+ const { calls } = await mockApi(page);
+ await page.route('**/api/auth/me', route => route.fulfill({ json: { id: 'viewer', groups: [], email: 'viewer@example.com' } }));
+ await page.route('**/api/auth/me/permissions', route => route.fulfill({ json: {
+ permissions: [{ action: 'view', resource: 'recommendations' }], is_admin: false,
+ } }));
+ await page.route(/\/api\/recommendations(?:\?.*)?$/, route => route.fulfill({ json: {
+ recommendations: [{ ...RECS[0], id: 'stored/id', service: 'savings-plans-ec2instance', resource_type: 'm7i' }],
+ summary: {}, regions: ['us-east-1'],
+ } }));
+ await page.route('**/api/insurance/status', route => route.fulfill({ json: { configured: true, missing: [] } }));
+ const comparisonCalls: string[] = [];
+ const figures = {
+ offer_id: '
', 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' },
+ };
+ await page.route('**/api/insurance/comparison?*', route => {
+ comparisonCalls.push(route.request().url());
+ return route.fulfill({ json: {
+ 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: 'alternative', status: 'vendor_alternative', reason: '',
+ native_identity_status: 'unavailable', figures: { ...figures, native_duration_seconds: null,
+ native_plan_type: null, native_instance_family: null, lease_attached: true, upfront_one_time_cost: '2400' } }],
+ non_gating_disclosure: 'Purchases do not depend on Archera.', sponsorship_disclosure: 'Sponsored by Archera.',
+ } });
+ });
+ await page.goto('/opportunities');
+ const button = page.getByRole('button', { name: 'Archera alternatives' });
+ await expect(button).toBeVisible();
+ expect(comparisonCalls).toEqual([]);
+ await expect(page.getByRole('checkbox', { name: 'Select recommendation', exact: true })).toHaveCount(0);
+ await button.click();
+ const dialog = page.getByRole('dialog', { name: 'Archera recommendation alternatives' });
+ await expect(dialog).toContainText('0.500000000000000001');
+ await expect(dialog).toContainText('one_year_gris');
+ await expect(dialog).toContainText('2400');
+ await expect(dialog).toContainText('native product identity is unavailable');
+ await expect(dialog).toContainText('Currency: unknown. Comparable cost difference: unavailable.');
+ await expect(dialog).toContainText('Original provider precision is unproven');
+ await expect(dialog).toContainText('Sponsored by Archera.');
+ await expect(dialog.locator('img')).toHaveCount(0);
+ expect(new URL(comparisonCalls[0]!).searchParams.get('recommendation_id')).toBe('stored/id');
+ expect(comparisonCalls).toHaveLength(1);
+ expect(calls.filter(call => call.method !== 'GET')).toEqual([]);
+ expect(await page.evaluate(() => (window as unknown as { __archeraXss?: number }).__archeraXss)).toBeUndefined();
+ await page.setViewportSize({ width: 390, height: 844 });
+ expect(await dialog.evaluate(element => element.scrollWidth <= element.clientWidth)).toBe(true);
+ await page.keyboard.press('Escape');
+ await expect(dialog).toHaveCount(0);
+ await expect(button).toBeFocused();
+ expect(errors).toEqual([]);
+});
diff --git a/internal/api/handler_insurance_recommendation_contract_test.go b/internal/api/handler_insurance_recommendation_contract_test.go
new file mode 100644
index 00000000..ab545004
--- /dev/null
+++ b/internal/api/handler_insurance_recommendation_contract_test.go
@@ -0,0 +1,222 @@
+package api
+
+import (
+ "context"
+ "encoding/json"
+ "fmt"
+ "io"
+ "net/http"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/stretchr/testify/mock"
+ "github.com/stretchr/testify/require"
+
+ "github.com/LeanerCloud/cloud-commitments-platform/internal/archera"
+ "github.com/LeanerCloud/cloud-commitments-platform/internal/config"
+ "github.com/LeanerCloud/cloud-commitments-platform/internal/secrets"
+)
+
+// The vendor here is synthetic transport behind the real archera.Provider and insurance.Client; it is not
+// a live Archera organization.
+
+const contractOrg = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa"
+
+type contractSecret struct {
+ secrets.Resolver
+ calls int
+}
+
+func (s *contractSecret) GetSecret(context.Context, string) (string, error) {
+ s.calls++
+ return insTestKey, nil
+}
+
+type contractVendor struct {
+ t *testing.T
+ scenario string
+ calls []*url.URL
+}
+
+func contractItem(id, account, payment string) string {
+ return fmt.Sprintf(`{"id":%q,"plan_id":%q,"offer_id":%q,"account_id":%s,"selected_quantity":7,"selected_commitment":0.5,"is_selected":true,"contract_term":"one_year_gris","payment_option":%q,"contract_spec":{"commitment_type":"x"},"offer":{"id":%q,"type":"savings_plan","provider":"aws","provider_offering_id":"provider-offer-a","offering_id":"cat","duration_seconds":31536000,"region":"us-east-1","az":null,"instance_type":null,"instance_family":"m5","product_description":null,"tenancy":null,"plan_type":"EC2Instance","is_multi_az":null,"is_flexible":null,"payment_option":%q}}`, id, insTestPlan, recOffer, account, payment, recOffer, payment)
+}
+
+func contractUnrelatedID(i int) string { return fmt.Sprintf("00000000-0000-4000-8000-%012d", i) }
+
+func contractOfferRow(current bool, payment, offer string) string {
+ return fmt.Sprintf(`{"is_current":%t,"offer_id":%q,"selected_amount":0.5,"commitment_type":"savings_plan","contract_term":"one_year_gris","payment_option":%q,"offer":{"provider":"aws","region":"us-east-1"},"discount_rate":0.1,"commitment_upfront_cost":1200,"commitment_financials_monthly_rate":{},"delta_vs_current":{"monthly_net_savings":5,"upfront_cost":1,"discount_rate":0.1}}`, current, offer, payment)
+}
+
+func contractComparison(scenario string) string {
+ id := recItem
+ if scenario == "comparison_missing" {
+ id = "bbbbbbbb-bbbb-4bbb-8bbb-bbbbbbbbbbbb"
+ }
+ currentPay, candPay := "no_upfront", "partial_upfront"
+ if scenario == "partial" {
+ currentPay, candPay = "partial_upfront", "all_upfront"
+ }
+ cand := contractOfferRow(false, candPay, "faadf00d-ffff-4aba-8ada-abcdefabcdef")
+ row := `{"line_item_id":"` + id + `","current":` + contractOfferRow(true, currentPay, recOffer) + `,"candidates":[` + cand + `]}`
+ unrelated := `{"line_item_id":"` + contractUnrelatedID(1) + `","current":` + strings.Replace(contractOfferRow(true, currentPay, recOffer), `"commitment_upfront_cost":1200`, `"commitment_upfront_cost":99999999`, 1) + `,"candidates":[]}`
+ return `{"current_totals":{"commitment_financials_monthly_rate":{},"commitment_upfront_cost":987654321},"hypothetical_totals":[],"data":[` + unrelated + `,` + row + `]}`
+}
+
+func (v *contractVendor) RoundTrip(req *http.Request) (*http.Response, error) {
+ v.calls = append(v.calls, req.URL)
+ require.Equal(v.t, http.MethodGet, req.Method)
+ require.Equal(v.t, "https", req.URL.Scheme)
+ require.Equal(v.t, "api.archera.ai", req.URL.Host)
+ require.Equal(v.t, insTestKey, req.Header.Get("x-api-key"))
+ prefix := "/v1/org/" + contractOrg + "/commitment-plans/" + insTestPlan
+ q := req.URL.Query()
+ headers := http.Header{}
+ status, body := http.StatusOK, ""
+ switch req.URL.Path {
+ case prefix + "/line-items":
+ page := q.Get("page")
+ require.Equal(v.t, "100", q.Get("page_size"))
+ require.Contains(v.t, []string{"1", "2"}, page)
+ headers.Set("X-Pagination", `{"total":101,"total_pages":2,"page":`+page+`,"first_page":1,"last_page":2}`)
+ if page == "1" {
+ rows := make([]string, 0, 100)
+ for i := 1; i <= 100; i++ {
+ rows = append(rows, contractItem(contractUnrelatedID(i), `"unrelated-source"`, "No Upfront"))
+ }
+ body = "[" + strings.Join(rows, ",") + "]"
+ break
+ }
+ account := `"Source-account-a"`
+ if v.scenario == "source_case" {
+ account = `"source-account-a"`
+ }
+ payment := "No Upfront"
+ if v.scenario == "partial" {
+ payment = "Partial Upfront"
+ }
+ body = "[" + contractItem(recItem, account, payment) + "]"
+ if v.scenario == "late_failure" {
+ status, body = http.StatusInternalServerError, `{"message":"`+recSpace+`"}`
+ }
+ case prefix + "/comparison":
+ body = contractComparison(v.scenario)
+ default:
+ v.t.Fatalf("unexpected vendor path: %s", req.URL.Path)
+ }
+ return &http.Response{StatusCode: status, Header: headers, Body: io.NopCloser(strings.NewReader(body)), Request: req}, nil
+}
+
+func contractRig(t *testing.T, scenario string, rows []config.RecommendationRecord) (*Handler, *recStore, *contractSecret, *contractVendor) {
+ t.Helper()
+ store := &recStore{results: [][]config.RecommendationRecord{rows}}
+ resolver := &contractSecret{}
+ vendor := &contractVendor{t: t, scenario: scenario}
+ provider := archera.NewProvider(archera.Settings{OrgID: contractOrg, PlanID: insTestPlan, KeySecretRef: insTestRef}, resolver, &http.Client{Transport: vendor})
+ return &Handler{config: store, insurance: provider}, store, resolver, vendor
+}
+
+func contractGet(h *Handler) (any, error) {
+ return (&recRig{h: h}).getID("stored-a")
+}
+
+func TestInsuranceRecommendationRealClientReadsPageTwoAndScopesComparison(t *testing.T) {
+ h, store, resolver, vendor := contractRig(t, "complete", one())
+ got, err := contractGet(h)
+ require.NoError(t, err)
+ dto := got.(*archera.RecommendationComparisonDTO)
+ require.Equal(t, archera.ResourceAssociated, dto.Status)
+ require.Equal(t, "stored-a", dto.RecommendationID)
+ require.Nil(t, dto.Currency)
+ require.Nil(t, dto.ComparableDelta)
+ require.Len(t, dto.Candidates, 1)
+
+ require.Len(t, vendor.calls, 3)
+ prefix := "/v1/org/" + contractOrg + "/commitment-plans/" + insTestPlan
+ for i, page := range []string{"1", "2"} {
+ require.Equal(t, prefix+"/line-items", vendor.calls[i].Path)
+ require.Equal(t, page, vendor.calls[i].Query().Get("page"))
+ }
+ cq := vendor.calls[2].Query()
+ require.Equal(t, prefix+"/comparison", vendor.calls[2].Path)
+ require.Equal(t, []string{recItem}, cq["line_item_ids"])
+ require.ElementsMatch(t, []string{"no_upfront", "partial_upfront", "all_upfront"}, cq["payment_options"])
+ require.Equal(t, 1, resolver.calls)
+ require.Equal(t, 2, store.calls, "stored row read before and after the vendor fetch")
+
+ body, err := json.Marshal(dto)
+ require.NoError(t, err)
+ for _, private := range []string{"Source-account-a", "unrelated-source", contractOrg, insTestPlan, "99999999", "987654321", contractUnrelatedID(1)} {
+ require.NotContains(t, string(body), private)
+ }
+}
+
+func TestInsuranceRecommendationRealClientPartialUpfrontRecommendationMatches(t *testing.T) {
+ row := recRecord()
+ row.Payment = "partial-upfront"
+ h, _, _, vendor := contractRig(t, "partial", []config.RecommendationRecord{row})
+ got, err := contractGet(h)
+ require.NoError(t, err)
+ require.Equal(t, archera.ResourceAssociated, got.(*archera.RecommendationComparisonDTO).Status)
+ require.Len(t, vendor.calls, 3)
+}
+
+func TestInsuranceRecommendationRealClientRefusesPartialOrUnprovenData(t *testing.T) {
+ for _, scenario := range []string{"late_failure", "comparison_missing", "source_case"} {
+ t.Run(scenario, func(t *testing.T) {
+ h, _, _, vendor := contractRig(t, scenario, one())
+ got, err := contractGet(h)
+ if scenario == "late_failure" {
+ require.Nil(t, got)
+ ce := requireCode(t, err, 502)
+ require.NotContains(t, ce.message, recSpace)
+ require.Len(t, vendor.calls, 2, "no comparison call after a failed page")
+ return
+ }
+ require.NoError(t, err)
+ dto := got.(*archera.RecommendationComparisonDTO)
+ require.Equal(t, archera.MatchUnavailable, dto.Status)
+ require.Nil(t, dto.Current)
+ require.Empty(t, dto.Candidates)
+ })
+ }
+}
+
+func TestInsuranceRecommendationRealClientRowChangedAfterFetchIs409(t *testing.T) {
+ h, store, _, vendor := contractRig(t, "complete", one())
+ changed := recRecord()
+ changed.Details = []byte(strings.Replace(string(changed.Details), "0.5", "0.6", 1))
+ store.results = append(store.results, []config.RecommendationRecord{changed})
+ got, err := contractGet(h)
+ require.Nil(t, got)
+ requireCode(t, err, 409)
+ require.Len(t, vendor.calls, 3)
+}
+
+func TestInsuranceRecommendationRealClientStoreFailuresMakeNoVendorCalls(t *testing.T) {
+ for name, rows := range map[string][]config.RecommendationRecord{"duplicate_store": append(one(), recRecord()), "missing": nil} {
+ t.Run(name, func(t *testing.T) {
+ h, _, resolver, vendor := contractRig(t, "complete", rows)
+ _, err := contractGet(h)
+ require.Error(t, err)
+ require.Zero(t, resolver.calls)
+ require.Empty(t, vendor.calls)
+ })
+ }
+}
+
+func TestInsuranceRecommendationRealClientUnauthorizedMakesNoVendorCalls(t *testing.T) {
+ for _, scoped := range []bool{false, true} {
+ h, store, resolver, vendor := contractRig(t, "complete", one())
+ m := new(MockAuthService)
+ m.On("HasPermissionAPI", mock.Anything, "u1", "view", "recommendations").Return(scoped, nil)
+ m.On("GetAllowedAccountsAPI", mock.Anything, "u1").Return([]string{"acct-1"}, nil)
+ h.auth = m
+ _, err := (&recRig{h: h}).get(sessionPrincipalCtx(), "recommendation_id=stored-a", map[string]string{"recommendation_id": "stored-a"})
+ require.Error(t, err)
+ require.Zero(t, store.calls)
+ require.Zero(t, resolver.calls)
+ require.Empty(t, vendor.calls)
+ }
+}