Skip to content
Open
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
26 changes: 20 additions & 6 deletions frontend/src/__tests__/recommendations.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3553,7 +3553,10 @@ describe('Issue #111: per-bucket Payment seed from per-account service override'

await loadRecommendations();
(document.getElementById('bulk-purchase-btn') as HTMLButtonElement).click();
await Promise.resolve(); await Promise.resolve(); await Promise.resolve();
// fetchOverridesForAccounts is a separate async function (one extra
// microtask boundary vs the previous inline Promise.all); four ticks
// are needed for openFanOutModal to finish populating the DOM.
await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); await Promise.resolve();

// Change the multi-account 1yr bucket's bucket-level Payment dropdown.
const bucketSelects = Array.from(
Expand Down Expand Up @@ -3853,8 +3856,10 @@ describe('Issue #132: bulk-buy collapses SP plan types into one bucket', () => {

await loadRecommendations();
(document.getElementById('bulk-purchase-btn') as HTMLButtonElement).click();
// openFanOutModal is async (issue #111 prefetch); wait a tick.
await Promise.resolve(); await Promise.resolve(); await Promise.resolve();
// fetchOverridesForAccounts is a separate async function (one extra
// microtask boundary vs the previous inline Promise.all); four ticks
// are needed for openFanOutModal to finish populating the DOM.
await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); await Promise.resolve();

const { getFanOutBuckets } = await import('../recommendations');
const buckets = getFanOutBuckets();
Expand Down Expand Up @@ -3917,7 +3922,10 @@ describe('Issue #132: bulk-buy collapses SP plan types into one bucket', () => {

await loadRecommendations();
(document.getElementById('bulk-purchase-btn') as HTMLButtonElement).click();
await Promise.resolve(); await Promise.resolve(); await Promise.resolve();
// fetchOverridesForAccounts is a separate async function (one extra
// microtask boundary vs the previous inline Promise.all); four ticks
// are needed for openFanOutModal to finish populating the DOM.
await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); await Promise.resolve();

// The fan-out modal renders one section per bucket; the SP section
// title carries the combined plan-type label.
Expand Down Expand Up @@ -3946,7 +3954,10 @@ describe('Issue #132: bulk-buy collapses SP plan types into one bucket', () => {

await loadRecommendations();
(document.getElementById('bulk-purchase-btn') as HTMLButtonElement).click();
await Promise.resolve(); await Promise.resolve(); await Promise.resolve();
// fetchOverridesForAccounts is a separate async function (one extra
// microtask boundary vs the previous inline Promise.all); four ticks
// are needed for openFanOutModal to finish populating the DOM.
await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); await Promise.resolve();

// The SP bucket section must contain a <details> element with the
// plan-type breakdown (issue #249).
Expand Down Expand Up @@ -3993,7 +4004,10 @@ describe('Issue #132: bulk-buy collapses SP plan types into one bucket', () => {

await loadRecommendations();
(document.getElementById('bulk-purchase-btn') as HTMLButtonElement).click();
await Promise.resolve(); await Promise.resolve(); await Promise.resolve();
// fetchOverridesForAccounts is a separate async function (one extra
// microtask boundary vs the previous inline Promise.all); four ticks
// are needed for openFanOutModal to finish populating the DOM.
await Promise.resolve(); await Promise.resolve(); await Promise.resolve(); await Promise.resolve();

const spSections = Array.from(document.querySelectorAll('.fanout-bucket')).filter(
(s) => s.querySelector('h4')?.textContent?.includes('Savings Plans'),
Expand Down
23 changes: 23 additions & 0 deletions frontend/src/lib/overrides.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import * as api from '../api';
import type { AccountServiceOverride } from '../api/accounts';

/**
* Fetches AccountServiceOverride arrays for a set of account IDs in parallel.
* Returns a Map keyed by account ID. Per-account fetch errors are silently
* swallowed so callers can always fall back to their default seed.
*/
export async function fetchOverridesForAccounts(
ids: Iterable<string>,
): Promise<Map<string, AccountServiceOverride[]>> {
const out = new Map<string, AccountServiceOverride[]>();
await Promise.all(
Array.from(ids).map(async (id) => {
try {
out.set(id, await api.listAccountServiceOverrides(id));
} catch {
// Silent fallback -- callers handle missing entries gracefully.
}
}),
);
return out;
}
26 changes: 3 additions & 23 deletions frontend/src/recommendations.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ function canActOnRecommendations(): boolean {
return canAccess('execute', 'purchases') || canAccess('create', 'plans');
}
import { parseNumericFilter, applyColumnFilters as applyColumnFiltersLib } from './lib/column-filters';
import { fetchOverridesForAccounts } from './lib/overrides';
// Re-export the shared primitives so existing consumers that import from
// recommendations.ts keep working without import-path churn (issue #166).
export { parseNumericFilter } from './lib/column-filters';
Expand Down Expand Up @@ -4130,18 +4131,7 @@ async function openFanOutModal(
if (r.cloud_account_id) allAccountIDs.add(r.cloud_account_id);
}
}
const overridesByAccount = new Map<string, AccountServiceOverride[]>();
await Promise.all(
Array.from(allAccountIDs).map(async (id) => {
try {
const list = await api.listAccountServiceOverrides(id);
overridesByAccount.set(id, list);
} catch {
// Silent fallback to toolbar seed — a network blip shouldn't
// block the user from purchasing.
}
}),
);
const overridesByAccount = await fetchOverridesForAccounts(allAccountIDs);

const buckets: FanOutBucket[] = bucketEntries
.filter(([_key, recs]) => recs.length > 0)
Expand Down Expand Up @@ -4891,17 +4881,7 @@ export async function openPurchaseModal(recommendations: LocalRecommendation[]):
for (const r of currentPurchaseRecommendations) {
if (r.cloud_account_id) accountIDs.add(r.cloud_account_id);
}
const overridesByAccount = new Map<string, AccountServiceOverride[]>();
await Promise.all(
Array.from(accountIDs).map(async (id) => {
try {
const list = await api.listAccountServiceOverrides(id);
overridesByAccount.set(id, list);
} catch {
// Silent fallback to rec-own / paymentOptionsFor[0] seed.
}
}),
);
const overridesByAccount = await fetchOverridesForAccounts(accountIDs);

// Compute seed per rec and mutate currentPurchaseRecommendations in
// place so the in-flight modal state matches what the dropdowns
Expand Down
Loading