Implemented the feature to identify and update estimates based on the ACR value, consumption start date, and ramp-up

This commit is contained in:
2026-10-20 15:55:58 -03:00
parent 71d550dfb1
commit 60daabfe91
31 changed files with 2800 additions and 43 deletions

View File

@@ -292,7 +292,7 @@ test("full-page debug CSS defines responsive bounds and readable semantic colors
test("scoped debug styles are packaged after the unchanged main stylesheet", () => {
const root = path.join(__dirname, "..");
const css = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css", "src/features/consumption/plan-detail.css", "src/features/ratecard/styles.css", "src/features/opportunities/customer-table-search.css", "src/features/opportunities/calendar.css"]
.concat("src/features/manage-time/styles.css")
.concat("src/features/manage-time/styles.css", "src/features/products/ramp-projection.css")
.map(file => fs.readFileSync(path.join(root, file), "utf8")).join("\n");
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(css)};`));
assert.ok(!composeScript("background").includes("opportunities-extension-debug-tabs"));

View File

@@ -2,11 +2,12 @@
const listeners = [];
const storage = new Map();
const internalSrPreview = new URLSearchParams(location.search).has("internal-sr");
const projectionPreview = new URLSearchParams(location.search).has("projection");
const opportunities = Array.from({ length: 25 }, (_, index) => ({
OptyNumber: `TEST${index + 1}`,
OptyId: 1000 + index,
Name: `Synthetic opportunity ${index + 1} - Database migration`,
CustomerAccount: { PartyUniqueName: "Example public sector customer (Example City, BR)", PartyId: internalSrPreview ? 789 : "test-party", PartyNumber: "test-account", Region_c_localizedValue: "LAD", PrimaryAddress: { CountryCode: "BR" } },
CustomerAccount: { PartyUniqueName: "Example public sector customer (Example City, BR)", PartyId: internalSrPreview || projectionPreview ? 789 : "test-party", PartyNumber: "test-account", Region_c_localizedValue: "LAD", PrimaryAddress: { CountryCode: "BR" } },
Owner: { PersonFirstName: "Example", PersonLastName: "Owner" },
PrimaryRevenue: { RevnAmountCurcyCode: "USD", RevnAmount: "25000", WinProb: 60 },
EffectiveDate: index === 24 ? null : `2026-11-${String(index < 8 ? 15 : 1 + index).padStart(2, "0")}T00:00:00Z`,
@@ -15,6 +16,7 @@
StatusCode: ["OPEN", "CLOSED", "LOST", "WON", "WON_PENDING"][index % 5]
}));
const products = [{ ProdGroupName: "Migrate databases to cloud", WorkloadName_c: "Example workload", RevnAmountCurcyCode: "USD", RevnAmount: "25000", TypeCode: "NEWCOMMIT", StatusCode: "OPEN", WinProb: 60, EffectiveDate: "2026-11-15", ConsumptionStartDate_c: "2026-11-15", RampMonths_c: 3 }];
if (projectionPreview) products[0] = { ...products[0], RevnId: 123, RampMonths_c: 2 };
const trend = Array.from({ length: 30 }, (_, index) => ({
fyDate: `2026-09-${String((index % 28) + 1).padStart(2, "0")}`,
fiscalTime: [`${String((index % 28) + 1).padStart(2, "0")}-Sep-26`],
@@ -26,10 +28,17 @@
accountSummary: { label: "Account-Ti-Authorization", value: "synthetic-accp" },
accountPlans: { label: "G2m-Authorization", value: "synthetic-ocicons" }
};
if (projectionPreview) authorizations.workloadActions = { label: "Wwb-Provider-Authorization", value: "synthetic-wwb" };
const runtime = {
onMessage: { addListener: listener => listeners.push(listener) },
async sendMessage(message) {
const operation = message.type.split(".").at(-1);
if (operation === "requestWorkloadForecastUpdate") return { ok: true, result: null, debugRequests: [] };
if (operation === "requestWorkloads") return { ok: true, workloads: [{ id: 123, name: "Example workload",
adjustedACR: 50000, consumptionStartDate: "2026-11-15T00:00:00", forecast: Array.from({ length: 12 }, (_, index) => ({
month: (5 + index) % 12 + 1, year: index < 7 ? 2026 : 2027,
adjustedConsumptionAmount: index < 5 ? 0 : index === 5 ? 1000 : index === 6 ? 3030.30 : 4500
})) }], debugRequests: [] };
if (operation === "requestTimeEntries") return { ok: true, items: [], debugRequests: [] };
if (operation === "requestLookupTaskType") return { ok: true, items: [{ lookUpCode_c: "SCTA_SERVICE_CONFIG", lookUpMeaning_c: "Config", tags_c: "SERVICE" }, { lookUpCode_c: "SCTA_SERVICE_STANDARD", lookUpMeaning_c: "Standard", tags_c: "SERVICE" }, { lookUpCode_c: "SCTA_NON_SERVICE_TRAINING", lookUpMeaning_c: "Training", tags_c: "NON_SERVICE" }], debugRequests: [] };
if (operation === "requestServiceRequestExpand") return { ok: true, items: [...message.srNumbers, "non-service-sr"].map(SrNumber => ({ SrNumber, SRActivities_c: "Account Plan;Customer Research, Assessment and Recommendation;Success Plan" })), debugRequests: [] };

View File

@@ -34,7 +34,7 @@ test("failed results are never cached and identity changes discard previous resp
h.user(""); await h.cache.send(msg, send); await h.cache.send(msg, send); assert.equal(calls, 5);
});
test("writes and token requests always execute; successful saves and Refresh all invalidate queries", async () => {
for (const name of ["requestTimeEntriesSave", "requestTimeEntriesDelete", "requestMgmtAction", "requestPostServiceRequest", "requestPostInternalServiceRequest", "invalidateProductsCache"]) {
for (const name of ["requestTimeEntriesSave", "requestTimeEntriesDelete", "requestMgmtAction", "requestPostServiceRequest", "requestPostInternalServiceRequest", "requestWorkloadForecastUpdate", "invalidateProductsCache"]) {
const h = setup(); let queries = 0, writes = 0;
const query = message("requestActionFindLookup");
const queryTransport = async () => { queries++; return { ok: true }; };
@@ -47,6 +47,42 @@ test("writes and token requests always execute; successful saves and Refresh all
for (let i = 0; i < 2; i++) await h.cache.send(message("requestSrTokenCatalog"), async () => { calls++; return { ok: true }; });
assert.equal(calls, 2);
});
test("reopening workloads reuses account cache across token renewals and keeps original persistent expiry", async () => {
const h = setup(); let calls = 0;
h.hours(1);
const transport = async () => { calls++; return { ok: true, workloads: [{ id: 1, forecast: [] }], cachedAt: 50000, fromCache: true }; };
const msg = message("requestWorkloads", { customerId: 123, principal: "a@example.invalid", authorization: { value: "old-token" } });
const first = await h.cache.send(msg, transport);
first.workloads[0].forecast.push({ month: 1 });
const second = await h.cache.send({ ...msg, customerId: "123", cacheMaxAgeMs: 3600000, authorization: { value: "new-token" } }, transport);
assert.equal(calls, 1);
assert.equal(second.fromCache, true);
assert.equal(second.cachedAt, 50000);
assert.equal(second.workloads[0].forecast.length, 0);
h.advance(3550000);
await h.cache.send(msg, transport);
assert.equal(calls, 2, "expires from persistent fetchedAt rather than modal opening time");
await h.cache.send({ ...msg, customerId: 456 }, transport);
assert.equal(calls, 3);
h.user("b@example.invalid");
await h.cache.send({ ...msg, principal: "b@example.invalid" }, transport);
assert.equal(calls, 4);
});
test("workload cache survives failed updates but invalidates after success and Refresh all", async () => {
const h = setup(); let calls = 0;
const msg = message("requestWorkloads", { customerId: 123 });
const transport = async () => { calls++; return { ok: true, workloads: [] }; };
await h.cache.send(msg, transport);
await h.cache.send(message("requestWorkloadForecastUpdate"), async () => ({ ok: false }));
await h.cache.send(msg, transport);
assert.equal(calls, 1);
await h.cache.send(message("requestWorkloadForecastUpdate"), async () => ({ ok: true }));
await h.cache.send(msg, transport);
assert.equal(calls, 2);
await h.cache.send(message("invalidateProductsCache"), async () => ({ ok: true }));
await h.cache.send(msg, transport);
assert.equal(calls, 3);
});
test("concurrent queries share work but old responses cannot repopulate an invalidated cache", async () => {
const h = setup(); let resolve, calls = 0;
const msg = message("requestLookupTypePlatform");

View File

@@ -132,6 +132,32 @@ test("product labels support multiple digits without changing their tooltip", ()
assert.equal(count.attributes["aria-label"], "12 products for opportunity TEST-1");
});
test("opportunity projection alert is last and aggregates only divergent products", () => {
const h = setup();
const alert = () => findClass(h.cell("name"), "opportunities-extension-opportunity-projection-alert");
const product = amount => ({ RampMonths_c: 0, workload: { adjustedACR: 1200, consumptionStartDate: "2026-10-01",
forecast: [{ year: 2026, month: 10, adjustedConsumptionAmount: amount }] } });
assert.equal(alert(), undefined);
h.app.productsContentState.opptyProductsByNumber.set("TEST-1", { items: [product(90), product(100), product(110), {}] });
h.app.opportunitiesTable.renderOpportunitiesTable();
assert.equal(alert().title, "Consumption forecast differs in 2 products. View products.");
const actions = findClass(h.cell("name"), "opportunities-extension-cell-actions");
assert.equal(actions.children.at(-1), alert());
assert.equal(alert().tag, "button");
assert.equal(alert().attributes["aria-expanded"], "false");
alert().click();
assert.equal(h.rows()[1].className, "opportunities-extension-products-detail-row");
assert.equal(alert().attributes["aria-expanded"], "true");
assert.equal(walk(h.rows()[1]).filter(node => node.attributes["aria-haspopup"] === "dialog").length, 2);
assert.deepEqual(h.summaries, ["TEST-1"]);
alert().click();
assert.equal(h.rows()[1].className, "opportunities-extension-products-detail-row", "clicking again never collapses the list");
assert.deepEqual(h.summaries, ["TEST-1"], "already expanded list is not reloaded");
h.app.productsContentState.opptyProductsByNumber.set("TEST-1", { items: [product(100), {}] });
h.app.opportunitiesTable.renderOpportunitiesTable();
assert.equal(alert(), undefined);
});
test("opportunity badges share height, typography and alignment including the Action plus button", () => {
const css = fs.readFileSync(path.join(__dirname, "../src/features/opportunities/styles.css"), "utf8");
const shared = css.match(/\.opportunities-extension-cell-actions > \.opportunities-extension-products-count,\s*\.opportunities-extension-cell-actions > \.opportunities-extension-products-ramp,\s*\.opportunities-extension-cell-actions > \.opportunities-extension-crm-sr-badge\s*\{([^}]+)\}/);
@@ -254,6 +280,6 @@ test("table styles reserve compact widths, prioritize Name/Customer and keep nes
assert.ok(remaining > 0);
assert.ok(remaining / 2 >= 12 + 84, "three customer controls fit after the left inset");
}
const allCss = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css", "src/features/consumption/plan-detail.css", "src/features/ratecard/styles.css", "src/features/opportunities/customer-table-search.css", "src/features/opportunities/calendar.css", "src/features/manage-time/styles.css"].map(read).join("\n");
const allCss = ["src/content/styles/redwood.css", "src/features/debug/styles.css", "src/features/opportunities/styles.css", "src/features/install-base/styles.css", "src/features/service-requests/styles.css", "src/features/consumption/plan-detail.css", "src/features/ratecard/styles.css", "src/features/opportunities/customer-table-search.css", "src/features/opportunities/calendar.css", "src/features/manage-time/styles.css", "src/features/products/ramp-projection.css"].map(read).join("\n");
assert.ok(composeScript("content").includes(`const redwoodStyles = ${JSON.stringify(allCss)};`));
});

View File

@@ -0,0 +1,209 @@
const assert = require("node:assert/strict");
const { test } = require("node:test");
const { loadApplication } = require("./support/application.cjs");
const model = loadApplication("content").rampProjection;
const product = (workload = {}, overrides = {}) => ({ RampMonths_c: 2, RevnAmount: 1,
ConsumptionStartDate_c: "2000-01-01", workload: { adjustedACR: 50000,
consumptionStartDate: "2026-11-01T00:00:00", forecast: [{ month: 11, year: 2026, adjustedConsumptionAmount: 1515.15 }],
...workload }, ...overrides });
test("ramp formula agrees with all twelve workbook ramps and preserves the annual total", () => {
// rampas!E6:E17 and the stabilized values in P6:P17, annual value F2 = 50000.
const first = [4166.666666666667, 2173.913043478261, 1515.151515151515, 1190.4761904761904, 1000,
877.1929824561404, 793.6507936507936, 735.2941176470588, 694.4444444444445, 666.6666666666667,
649.3506493506494, 641.025641025641];
const stable = [4166.666666666667, 4347.826086956522, 4545.454545454545, 4761.9047619047615,
5000, 5263.1578947368425, 5555.555555555556, 5882.35294117647, 6250, 6666.666666666667,
7142.857142857143, 7692.307692307692];
for (let ramp = 0; ramp <= 11; ramp++) {
assert.ok(Math.abs(model.monthlyAmount(50000, ramp, 1) - first[ramp]) < 1e-9);
assert.ok(Math.abs(model.monthlyAmount(50000, ramp, 12) - stable[ramp]) < 1e-9);
const months = Array.from({ length: 12 }, (_, i) => model.monthlyAmount(50000, ramp, i + 1));
assert.ok(Math.abs(months.reduce((sum, value) => sum + value, 0) - 50000) < 1e-9);
assert.equal(model.monthlyAmount(50000, ramp, 13), months[11], "no annual restart");
assert.equal(model.monthlyAmount(50000, ramp, 0), 0);
}
});
test("comparison uses adjustedACR, product RampMonths_c, calendar months and cent precision", () => {
const result = model.compare(product({ rampMonths: 0, actualRampMonths: 11, forecast: [
{ month: 1, year: 2027, adjustedConsumptionAmount: 4500 },
{ month: 11, year: 2026, adjustedConsumptionAmount: "1515.151515" },
{ month: 12, year: 2026, adjustedConsumptionAmount: 3200 },
{ month: 10, year: 2026, adjustedConsumptionAmount: 0 }
] }));
assert.equal(result.status, "divergent");
assert.equal(result.divergentCount, 2);
assert.deepEqual(Array.from(result.rows, row => [row.key, row.calculated, row.difference]), [
["2026-10", 0, 0], ["2026-11", 1515.15, 0], ["2026-12", 3030.30, 169.70], ["2027-01", 4545.45, -45.45]
]);
assert.equal(model.compare(product()).status, "matched");
});
test("first month is prorated inclusively without timezone shifts, including leap years", () => {
for (const [start, month, year, expected, days, total] of [
["2026-11-25T00:00:00-0300", 11, 2026, 303.03, 6, 30],
["2026-11-30T00:00:00Z", 11, 2026, 50.51, 1, 30],
["2028-02-29T00:00:00+1400", 2, 2028, 52.25, 1, 29],
["2027-02-28", 2, 2027, 54.11, 1, 28],
["2026-12-01", 12, 2026, 1515.15, 31, 31]
]) {
const result = model.compare(product({ consumptionStartDate: start,
forecast: [{ month, year, adjustedConsumptionAmount: expected }] }));
assert.equal(result.rows[0].calculated, expected, start);
assert.equal(result.status, "matched", start);
assert.equal(result.firstMonthDays, days);
assert.equal(result.firstMonthTotalDays, total);
}
});
test("missing or invalid inputs never create false mismatches or silently default to zero", () => {
for (const value of [null, "", "garbage", -1, Infinity]) assert.equal(model.compare(product({ adjustedACR: value })).status, "unavailable");
for (const value of [null, "", "garbage", -1, 12, 1.5]) assert.equal(model.compare(product({}, { RampMonths_c: value })).status, "unavailable");
for (const value of [null, "", "2026-02-30", "2026-13-01"]) assert.equal(model.compare(product({ consumptionStartDate: value })).status, "unavailable");
assert.equal(model.compare({}).status, "unavailable");
for (const forecast of [[], null, [{ year: 2026, month: 0 }], [{ year: 2026, month: 11 }, { year: 2026, month: 11 }]]) {
assert.equal(model.compare(product({ forecast })).status, "unavailable");
}
assert.equal(model.compare(product({ adjustedACR: 0, forecast: [{ year: 2026, month: 11, adjustedConsumptionAmount: 0 }] }, { RampMonths_c: "0" })).status, "matched");
const missing = model.compare(product({ forecast: [{ year: 2026, month: 11, projectedConsumptionAmount: 500 }] }));
assert.equal(missing.status, "unavailable");
assert.equal(missing.rows[0].actual, null);
assert.equal(missing.rows[0].difference, null);
});
test("calculation is read-only and keeps missing monthly values visible beside real differences", () => {
const input = product({ forecast: [{ year: 2026, month: 11 }, { year: 2026, month: 12, adjustedConsumptionAmount: 0 }] });
const before = JSON.stringify(input);
const result = model.compare(input);
assert.equal(JSON.stringify(input), before);
assert.equal(result.status, "divergent");
assert.equal(result.divergentCount, 1);
assert.equal(result.missingCount, 1);
assert.ok(result.reason);
});
function dom() {
const document = { activeElement: null };
class Element {
constructor(tag) { this.tag = tag; this.children = []; this.attributes = {}; this.dataset = {}; this.listeners = {}; this.classList = { toggle() {} }; this.inert = false; this.isConnected = true; }
set textContent(value) { this.text = String(value); this.children = []; }
get textContent() { return (this.text || "") + this.children.map(child => child.textContent).join(""); }
append(...children) { for (const child of children) { child.parent = this; this.children.push(child); } }
setAttribute(name, value) { this.attributes[name] = value; }
addEventListener(name, fn) { this.listeners[name] = fn; }
focus() { document.activeElement = this; }
remove() { this.parent.children = this.parent.children.filter(child => child !== this); this.isConnected = false; }
click() { this.listeners.click?.({ preventDefault() {}, stopPropagation() {} }); }
}
const host = new Element("main"), content = new Element("div");
host.append(content);
document.createElement = tag => new Element(tag);
document.createElementNS = (_, tag) => new Element(tag);
document.getElementById = () => host;
return { document, host, content };
}
const walk = root => [root, ...root.children.flatMap(walk)];
test("Update estimates prevents duplicate clicks, reports errors, retries and closes on success", async () => {
const { document, host } = dom();
const app = loadApplication("content", { document, Intl, Date });
const input = product({ forecast: [{ year: 2026, month: 11, adjustedConsumptionAmount: 0 }] });
const notifications = [];
app.opportunitiesLinks.showCopyConfirmation = (...args) => notifications.push(args);
let reject, calls = 0;
app.workloadForecastUpdate.save = () => { calls++; return new Promise((_, fail) => { reject = fail; }); };
app.rampProjectionView.open(input, { OptyNumber: "TEST" });
const modal = host.children[1];
const button = walk(modal).find(node => node.textContent === "Update estimates");
const event = { preventDefault() {}, stopPropagation() {} };
button.listeners.click(event);
assert.equal(calls, 0, "opening confirmation must not save");
const confirmation = () => walk(modal).find(node => node.attributes.role === "alertdialog");
assert.ok(confirmation().textContent.includes("The estimates will be updated directly in the SPA"));
const cancel = walk(confirmation()).find(node => node.textContent === "Cancel");
assert.equal(document.activeElement, cancel);
cancel.click();
assert.equal(confirmation(), undefined);
assert.equal(document.activeElement, button);
assert.equal(calls, 0, "cancelling must not save");
button.click();
const pending = walk(confirmation()).find(node => node.textContent === "Confirm update").listeners.click(event);
button.click();
assert.equal(confirmation(), undefined);
assert.equal(calls, 1);
assert.equal(button.disabled, true);
assert.equal(app.rampProjectionView.close(), true);
assert.equal(host.children.length, 2, "cannot dismiss while saving");
reject(new Error("Forbidden"));
await pending;
assert.equal(button.disabled, false);
assert.deepEqual(notifications, [["Forbidden", true]]);
app.workloadForecastUpdate.save = async () => { calls++; };
button.click();
await walk(confirmation()).find(node => node.textContent === "Confirm update").listeners.click(event);
assert.equal(host.children.length, 1);
assert.deepEqual(notifications[1], ["Workload estimates updated successfully."]);
});
test("single-product opportunity alert opens the comparison directly with opportunity context", () => {
const { document, host, content } = dom();
const app = loadApplication("content", { document, Intl, Date });
const opportunity = { OptyNumber: "ONLY", CustomerAccount: { PartyUniqueName: "Single customer" } };
const input = product({ forecast: [{ year: 2026, month: 11, adjustedConsumptionAmount: 0 }] });
let expansions = 0;
app.productsDetails.expandProducts = () => { expansions++; };
const alert = app.rampProjectionView.createOpportunityIndicator({ items: [input] }, opportunity, false);
content.append(alert);
assert.equal(alert.attributes["aria-haspopup"], "dialog");
assert.equal(alert.attributes["aria-expanded"], undefined);
assert.match(alert.title, /View comparison/);
alert.click();
assert.equal(expansions, 0);
assert.equal(host.children[1].attributes.role, "dialog");
assert.ok(host.children[1].textContent.includes("ONLY"));
assert.ok(host.children[1].textContent.includes("Single customer"));
app.rampProjectionView.close();
assert.equal(document.activeElement, alert);
const multiple = app.rampProjectionView.createOpportunityIndicator({ items: [input, product()] }, opportunity, false);
assert.equal(multiple.attributes["aria-haspopup"], undefined);
multiple.click();
assert.equal(expansions, 1, "two products still expand even when only one is divergent");
assert.equal(host.children.length, 1);
});
test("product column opens accessible read-only comparison and restores focus/background on close", () => {
const { document, host, content } = dom();
const app = loadApplication("content", { document, Intl, Date });
const input = product({ consumptionStartDate: "2026-11-25", forecast: [{ year: 2026, month: 11, adjustedConsumptionAmount: 1000 }] },
{ RevnId: 1, ProdGroupName: "<b>Product</b>", RevnAmountCurcyCode: "USD" });
app.productsContentState.opptyProductsByNumber.set("TEST", { items: [input] });
const row = app.productsDetails.createProductsDetailRow({ OptyNumber: "TEST", CustomerAccount: { PartyUniqueName: "Example" } }, 10);
const alert = walk(row).find(node => node.attributes["aria-haspopup"] === "dialog");
assert.ok(walk(row).some(node => node.tag === "th" && node.textContent === "Projection"));
assert.ok(alert);
assert.equal(alert.children[0].tag, "svg");
content.append(row);
alert.click();
const modal = host.children[1];
assert.equal(modal.attributes.role, "dialog");
assert.equal(content.inert, true);
const elements = walk(modal);
for (const text of ["Forecast", "Calculated", "Difference", "11/2026 (Prorated)", "$303.03", "$696.97"]) {
assert.ok(elements.some(node => node.textContent === text), text);
}
assert.ok(elements.some(node => node.textContent === "<b>Product</b>"));
assert.equal(elements.some(node => node.tag === "b"), false, "names use safe text nodes");
const closeButton = elements.find(node => node.title === "Close consumption projection");
assert.equal(document.activeElement, closeButton);
let prevented = false;
modal.listeners.keydown({ key: "Tab", shiftKey: true, preventDefault() { prevented = true; } });
assert.equal(prevented, true);
assert.equal(document.activeElement.textContent, "Update estimates");
modal.listeners.keydown({ key: "Escape", preventDefault() {}, stopPropagation() {} });
assert.equal(host.children.length, 1);
assert.equal(content.inert, false);
assert.equal(document.activeElement, alert);
assert.equal(app.rampProjectionView.createIndicator(product(), {}).textContent, "OK");
assert.equal(app.rampProjectionView.createIndicator({}, {}).textContent, "-");
});

View File

@@ -0,0 +1,102 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const { loadBackground, response, plain } = require("./support/characterization.cjs");
const { loadApplication } = require("./support/application.cjs");
const type = "opportunitiesExtension.requestWorkloadForecastUpdate";
const payload = [{ year: 2026, month: 11, amount: 303.03, updatedBy: "test@example.invalid" }];
const message = { type, workloadId: "1234", authorization: { value: "synthetic-wwb" }, payload };
for (const family of ["chrome", "firefox"]) {
test(`${family}: forecast PATCH uses dynamic workload, JSON, WWB authorization and invalidates cache on 204`, async () => {
const calls = [];
const storage = { "opportunitiesExtension.workloadsCache.v1": { old: { items: [{ id: 1234 }] } } };
const h = loadBackground({ family, storage, fetch: async (url, options) => {
calls.push({ url, options });
return new Response(null, { status: 204 });
} });
const result = await h.dispatch(message);
assert.equal(result.ok, true);
assert.equal(calls.length, 1);
assert.ok(calls[0].url.endsWith("/workloads/1234/forecast"));
assert.equal(calls[0].options.method, "PATCH");
assert.equal(calls[0].options.credentials, "include");
assert.equal(calls[0].options.headers["wwb-provider-authorization"], "synthetic-wwb");
assert.equal(calls[0].options.headers["Content-Type"], "application/json");
assert.deepEqual(JSON.parse(calls[0].options.body), payload);
assert.deepEqual(storage["opportunitiesExtension.workloadsCache.v1"], {});
assert.equal(result.debugRequests[0].name, "requestWorkloadForecastUpdate");
assert.equal(result.debugRequests[0].httpStatus, 204);
});
test(`${family}: denied PATCH preserves cache, returns debug and is not automatically retried`, async () => {
let calls = 0;
const storage = { "opportunitiesExtension.workloadsCache.v1": { old: { items: [{ id: 1234 }] } } };
const before = JSON.stringify(storage);
const h = loadBackground({ family, storage, fetch: async () => { calls++; return response({ error: "Denied" }, 403); } });
const result = await h.dispatch(message);
assert.equal(result.ok, false);
assert.equal(result.httpStatus, 403);
assert.equal(calls, 1);
assert.equal(JSON.stringify(storage), before);
assert.equal(result.debugRequests[0].httpStatus, 403);
});
}
test("forecast update rejects invalid identity, invalid amounts and duplicate months before fetch", async () => {
let calls = 0;
const h = loadBackground({ fetch: async () => { calls++; return response({}); } });
for (const change of [{ workloadId: "../other" }, { authorization: {} }, { payload: [] },
{ payload: [{ ...payload[0], amount: -1 }] }, { payload: [{ ...payload[0], month: 13 }] },
{ payload: [{ ...payload[0], updatedBy: "" }] }, { payload: [payload[0], payload[0]] }]) {
assert.equal((await h.dispatch({ ...message, ...change })).ok, false);
}
assert.equal(calls, 0);
});
function setup() {
const app = loadApplication("content");
const item = { RevnId: 1234, RampMonths_c: 2, workload: { id: 1234, adjustedACR: 50000,
consumptionStartDate: "2026-11-25", forecast: [
{ year: 2026, month: 10, adjustedConsumptionAmount: 0 },
{ year: 2026, month: 11, adjustedConsumptionAmount: 100 },
{ year: 2026, month: 12, adjustedConsumptionAmount: 100 }
] } };
app.currentUser.ensure = async () => {};
app.currentUser.getUserId = () => "test@example.invalid";
app.authContentSession.requestConsumerTokenServiceOnce = async () => ({ workloadActions: message.authorization });
app.debugContentLog.appendBackgroundRequestLog = () => {};
app.productsContentJobs.queueProductsTableRender = () => {};
app.productsContentState.opptyProductsByNumber.set("ONE", { items: [item] });
app.productsContentState.opptyProductsByNumber.set("TWO", { items: [{ ...item }] });
app.productsContentState.workloadsByCustomer.set("99", { status: "ready", workloads: [item.workload], byId: new Map([["1234", item.workload]]) });
return { app, item };
}
test("content constructs calculated months and updates both joins only after successful PATCH", async () => {
const { app, item } = setup();
let release, sent;
app.platformContentMessages.sendRuntimeMessage = value => { sent = value; return new Promise(resolve => { release = resolve; }); };
const saving = app.workloadForecastUpdate.save(item);
await new Promise(resolve => setImmediate(resolve));
assert.equal(item.workload.forecast[1].adjustedConsumptionAmount, 100);
assert.equal(sent.type, type);
assert.equal(sent.workloadId, "1234");
assert.deepEqual(plain(sent.payload), [payload[0], { year: 2026, month: 12, amount: 3030.30, updatedBy: "test@example.invalid" }]);
await assert.rejects(app.workloadForecastUpdate.save(item), /already being updated/);
release({ ok: true });
await saving;
assert.equal(app.rampProjection.compare(item).status, "matched");
assert.equal(app.productsContentState.workloadsByCustomer.get("99").byId.get("1234").forecast[1].adjustedConsumptionAmount, 303.03);
for (const record of app.productsContentState.opptyProductsByNumber.values()) assert.equal(app.rampProjection.compare(record.items[0]).status, "matched");
});
test("failed forecast update leaves values unchanged and allows a deliberate retry", async () => {
const { app, item } = setup();
const before = JSON.stringify(item);
app.platformContentMessages.sendRuntimeMessage = async () => ({ ok: false, error: "Forbidden" });
await assert.rejects(app.workloadForecastUpdate.save(item), /Forbidden/);
assert.equal(JSON.stringify(item), before);
app.platformContentMessages.sendRuntimeMessage = async () => ({ ok: true });
await app.workloadForecastUpdate.save(item);
assert.equal(app.rampProjection.compare(item).status, "matched");
});

237
tests/workloads.test.cjs Normal file
View File

@@ -0,0 +1,237 @@
const test = require("node:test");
const assert = require("node:assert/strict");
const { loadBackground, plain, response } = require("./support/characterization.cjs");
const { loadApplication } = require("./support/application.cjs");
const TYPE = "opportunitiesExtension.requestWorkloads";
const CACHE = "opportunitiesExtension.workloadsCache.v1";
const flush = () => new Promise(resolve => setImmediate(resolve));
const request = (extra = {}) => ({ type: TYPE, customerId: "123", principal: "user@example.invalid",
authorization: { value: "synthetic-wwb-token" }, cacheMaxAgeMs: 3600000, ...extra });
const page = (ids, extra = {}) => ({ workloads: ids.map(id => ({ id, name: `Workload ${id}`, forecast: [] })), hasNext: false, ...extra });
for (const family of ["chrome", "firefox"]) {
test(`${family}: workloads paginate with response offset/limit and reuse persistent per-account cache`, async () => {
const calls = [], storage = {};
const h = loadBackground({ family, storage, fetch: async (url, options) => {
calls.push({ url: new URL(url), options });
const offset = Number(new URL(url).searchParams.get("offset"));
return response(offset === 0 ? page([11, 12], { hasNext: true, offset: 0, limit: 2 })
: offset === 2 ? page([13], { hasNext: true, offset: 2, limit: 1 }) : page([14]));
} });
const result = await h.dispatch(request());
assert.equal(result.ok, true);
assert.deepEqual(result.workloads.map(item => item.id), [11, 12, 13, 14]);
assert.deepEqual(calls.map(call => call.url.searchParams.get("offset")), ["0", "2", "3"]);
for (const { url, options } of calls) {
assert.equal(url.origin, "https://spa.oracle.com");
assert.ok(url.pathname.endsWith("/customers/123/workloads"));
assert.equal(url.searchParams.get("workloadStatus"), "Pipeline,Implementation,Live");
assert.equal(url.searchParams.get("opportunityForecastType"), "All");
assert.equal(url.searchParams.get("includeForEstimates"), "ALL");
assert.equal(url.searchParams.get("financialYear"), "ROLLING_4_QTRS");
assert.equal(options.method, "GET");
assert.equal(options.credentials, "include");
assert.equal(options.headers["wwb-provider-authorization"], "synthetic-wwb-token");
}
assert.equal(result.debugRequests.length, 3);
assert.ok(result.debugRequests.every(entry => entry.name === "requestWorkloads"));
assert.equal(result.fromCache, false);
assert.equal((await h.dispatch(request())).fromCache, true);
assert.equal(calls.length, 3);
assert.doesNotMatch(JSON.stringify(storage), /synthetic-wwb-token|debugRequests|requestHeaders/);
const restarted = loadBackground({ family, storage });
const cached = await restarted.dispatch(request());
assert.equal(cached.fromCache, true);
assert.deepEqual(cached.workloads, result.workloads);
assert.equal(cached.cachedAt, result.cachedAt);
h.advance(3600001);
assert.equal((await h.dispatch(request())).fromCache, false);
assert.equal(calls.length, 6);
assert.equal((await h.dispatch(request({ customerId: "456" }))).fromCache, false);
assert.equal((await h.dispatch(request({ principal: "other@example.invalid" }))).fromCache, false);
assert.equal(calls.length, 12);
});
test(`${family}: workload cache coalesces flights and Refresh all prevents late cache restoration`, async () => {
const pending = [], storage = {};
const h = loadBackground({ family, storage, fetch: () => new Promise(resolve => pending.push(resolve)) });
const old = h.dispatch(request()), shared = h.dispatch(request());
await flush();
assert.equal(pending.length, 1);
assert.equal((await h.dispatch({ type: "opportunitiesExtension.invalidateProductsCache" })).ok, true);
const fresh = h.dispatch(request());
await flush();
assert.equal(pending.length, 2);
pending[1](response(page([22])));
await fresh;
pending[0](response(page([11])));
assert.deepEqual(await old, await shared);
const cached = await h.dispatch(request());
assert.equal(cached.fromCache, true);
assert.deepEqual(cached.workloads.map(item => item.id), [22]);
assert.equal(pending.length, 2);
});
test(`${family}: successful empty workloads are cached; storage failures do not lose results`, async () => {
let calls = 0;
const h = loadBackground({ family, fetch: async () => { calls++; return response(page([])); } });
assert.deepEqual((await h.dispatch(request())).workloads, []);
assert.equal((await h.dispatch(request())).fromCache, true);
assert.equal(calls, 1);
const unavailable = loadBackground({ family, storageError: true, fetch: async () => response(page([10])) });
assert.equal((await unavailable.dispatch(request())).ok, true);
});
}
test("workload failures and stalled pagination are not cached as complete results", async () => {
for (const bad of [response({}, 403), response({ workloads: [] }), response(page([], { hasNext: true })),
response(page([11], { hasNext: true, offset: 0, limit: 0 }))]) {
let calls = 0;
const h = loadBackground({ fetch: async () => ++calls === 1 ? bad : response(page([12])) });
const failed = await h.dispatch(request());
assert.equal(failed.ok, false);
assert.equal(failed.debugRequests.length, 1);
assert.equal((await h.dispatch(request())).ok, true);
assert.equal(calls, 2);
}
let calls = 0;
const repeated = loadBackground({ fetch: async () => { calls++; return response(page([11], { hasNext: true })); } });
assert.match((await repeated.dispatch(request())).error, /did not advance/);
assert.equal(calls, 2);
let attempt = 0;
const partial = loadBackground({ fetch: async () => ++attempt === 1
? response(page([11], { hasNext: true })) : response({}, 500) });
const failure = await partial.dispatch(request());
assert.equal(failure.ok, false);
assert.equal(failure.debugRequests.length, 2);
assert.equal(Object.keys(partial.storage[CACHE] || {}).length, 0);
});
test("workload validation rejects unsafe IDs or missing WWB tokens before requesting", async () => {
const h = loadBackground();
for (const extra of [{ customerId: "../123" }, { customerId: "" }, { authorization: {} }]) {
const result = await h.dispatch(request(extra));
assert.equal(result.ok, false);
assert.deepEqual(result.debugRequests, []);
}
});
test("workloads without a known principal are never persisted or reused across users", async () => {
const storage = {};
let calls = 0;
const h = loadBackground({ storage, fetch: async () => { calls++; return response(page([11])); } });
await h.dispatch(request({ principal: "" }));
await h.dispatch(request({ principal: "" }));
assert.equal(calls, 2);
assert.equal(storage[CACHE], undefined);
});
function content() {
let listener;
const app = loadApplication("content", {
document: { getElementById: () => null }, window: { requestAnimationFrame() {} },
chrome: { runtime: { onMessage: { addListener(fn) { listener = fn; } } } }
});
const calls = [], logs = [];
let tokens = 0;
app.currentUser.getUserId = () => "user@example.invalid";
app.authContentSession.requestConsumerTokenServiceOnce = async () => { tokens++; return { workloadActions: { value: "wwb" } }; };
app.debugContentLog.appendBackgroundRequestLog = entries => logs.push(...(entries || []));
app.platformContentMessages.sendRuntimeMessage = message => {
if (message.type !== TYPE) return Promise.resolve({ ok: true });
return new Promise(resolve => calls.push({ message: plain(message), resolve }));
};
app.productsContentJobs.ensureProductsBackgroundMessageHandler();
const start = opportunities => {
app.productsContentJobs.requestOpptyProductsInBackground(opportunities, "fusion-token");
return app.productsContentState.currentProductsRequestId;
};
const progress = (id, number, items, fromCache = false) => listener({ type: "opportunitiesExtension.opptyProductsProgress",
requestId: id, optyNumber: number, items, fromCache, state: "running" });
return { app, calls, logs, start, progress, tokens: () => tokens };
}
const opportunity = (OptyNumber, PartyId) => ({ OptyNumber, CustomerAccount: { PartyId } });
for (const productsFirst of [true, false]) {
test(`accounts deduplicate independently of filters; products-first=${productsFirst} joins by account and RevnId`, async () => {
const h = content();
h.app.preferencesState.opptyProductsCacheHours = 12;
h.app.opportunitiesFilterState.tableSearch = "hides all opportunities";
const id = h.start([opportunity("A", 123), opportunity("B", "123"), opportunity("C", 456),
{ OptyNumber: "NO_ACCOUNT", PartyId: 999, Owner: { PartyId: 888 } }]);
await flush();
assert.deepEqual(h.calls.map(call => call.message.customerId), ["123", "456"]);
assert.deepEqual(plain(h.app.productsContentState.workloadCustomerIds), ["123", "456"]);
assert.equal(h.tokens(), 1);
assert.equal(h.calls[0].message.authorization.value, "wwb");
assert.equal(h.calls[0].message.cacheMaxAgeMs, 12 * 3600000);
assert.equal(h.calls[0].message.principal, "user@example.invalid");
const original = { RevnId: 11, Name: "Original product" };
function products() {
h.progress(id, "A", [original, { RevnId: "999" }]);
h.progress(id, "B", [{ RevnId: "12" }], true);
h.progress(id, "C", [{ RevnId: 11 }, { RevnId: 22 }]);
}
if (productsFirst) products();
h.calls[0].resolve({ ok: true, ...page([11, 12]), cachedAt: 100, fromCache: true, debugRequests: [{ name: "requestWorkloads" }] });
h.calls[1].resolve({ ok: true, ...page([22]), cachedAt: 200 });
await flush();
if (!productsFirst) products();
const results = h.app.productsContentState.opptyProductsByNumber;
assert.equal(results.get("A").items[0].workload.id, 11);
assert.equal(results.get("A").items[0].Name, "Original product");
assert.equal(original.workload, undefined, "do not mutate raw product responses or their cache");
assert.equal(results.get("A").items[1].workload, null);
assert.equal(results.get("B").items[0].workload.id, 12);
assert.equal(results.get("B").fromCache, true);
assert.equal(results.get("C").items[0].workload, null, "never join a workload from another account");
assert.equal(results.get("C").items[1].workload.id, 22);
assert.equal(h.app.productsContentState.workloadsByCustomer.get("123").fromCache, true);
assert.equal(h.logs.length, 1);
});
}
test("workload queue limits concurrency to four and ignores a superseded Reference", async () => {
const h = content();
h.start(Array.from({ length: 9 }, (_, i) => opportunity(`A${i}`, i + 1)));
await flush();
assert.equal(h.calls.length, 4);
h.app.opportunitiesQueryState.periodRequestVersion++;
const id = h.start([opportunity("NEW", 100)]);
await flush();
assert.equal(h.calls.length, 5);
for (const call of h.calls.slice(0, 4)) call.resolve({ ok: true, ...page([11]) });
await flush();
assert.equal(h.calls.length, 5, "do not continue accounts from the old Reference");
assert.deepEqual([...h.app.productsContentState.workloadsByCustomer.keys()], ["100"]);
h.calls[4].resolve({ ok: true, ...page([22]) });
await flush();
h.progress(id, "NEW", [{ RevnId: 22 }]);
assert.equal(h.app.productsContentState.opptyProductsByNumber.get("NEW").items[0].workload.id, 22);
});
test("one failed account does not block other accounts or discard products", async () => {
const h = content();
const id = h.start([opportunity("A", 123), opportunity("B", 456)]);
await flush();
h.progress(id, "A", [{ RevnId: 11 }]);
h.calls[0].resolve({ ok: false, error: "Synthetic failure", debugRequests: [{ name: "requestWorkloads", httpStatus: 403 }] });
h.calls[1].resolve({ ok: true, ...page([22]) });
await flush();
assert.equal(h.app.productsContentState.workloadsByCustomer.get("123").status, "error");
assert.equal(h.app.productsContentState.workloadsByCustomer.get("456").status, "ready");
assert.equal(h.app.productsContentState.opptyProductsByNumber.get("A").items.length, 1);
assert.equal(h.logs[0].httpStatus, 403);
});
test("no accounts skips authentication, and failed authentication settles workload states", async () => {
const h = content();
h.start([]);
await flush();
assert.equal(h.tokens(), 0);
h.app.authContentSession.requestConsumerTokenServiceOnce = async () => { throw new Error("Session unavailable"); };
h.start([opportunity("A", 123)]);
await flush();
assert.equal(h.calls.length, 0);
assert.equal(h.app.productsContentState.workloadsByCustomer.get("123").error, "Session unavailable");
});