Badge indicator showing whether an opportunity has an associated Service Request
This commit is contained in:
17
README.md
17
README.md
@@ -17,6 +17,23 @@ Browser extension that adds the "Opportunities Extension" shortcut to the permit
|
||||
- `dist/chromium/`: output for Google Chrome and Microsoft Edge.
|
||||
- `dist/firefox/`: output for Firefox.
|
||||
|
||||
## CRM Service Request Badges
|
||||
|
||||
Once per opening of the opportunities modal, `resquestCrmServiceRequest` fetches all pages of
|
||||
`CRMServiceRequest` records with `SVC_PENDING_DELIVERY` and `ORA_SVC_MY_SR_TEAM_SRS`,
|
||||
using the existing general tokenrelay Bearer. It uses the Fusion `searchResources/latest`
|
||||
query endpoint, independently of the opportunity query version. Calls appear in
|
||||
the Service Requests debug category with masked credentials.
|
||||
|
||||
Records are linked by `OpportunityName_Id_c.OptyNumber`. A green SR badge indicates
|
||||
one or more matches; its tooltip lists each SR number, title, reported hours, owner,
|
||||
email and contact flags. A red badge indicates no match within that query's scope.
|
||||
A failed lookup shows `SR ?`, not a false no-match result, and opportunities still load.
|
||||
Badges follow Ramp Months and do not wait for product downloads. SR results remain
|
||||
in memory only. Reference changes share the in-flight lookup and reuse its result
|
||||
(including failures) without another request. Closing resets this state; reopening
|
||||
starts a fresh lookup, and responses from previous openings are ignored.
|
||||
|
||||
## Build
|
||||
|
||||
Use Node.js:
|
||||
|
||||
155
dist/chromium/content.js
vendored
155
dist/chromium/content.js
vendored
File diff suppressed because one or more lines are too long
155
dist/firefox/content.js
vendored
155
dist/firefox/content.js
vendored
File diff suppressed because one or more lines are too long
@@ -29,6 +29,8 @@ module.exports = {
|
||||
"src/platform/content-messages.js",
|
||||
"src/features/opportunities/controller.js",
|
||||
"src/services/fusion/opportunities.js",
|
||||
"src/services/fusion/crm-service-requests.js",
|
||||
"src/features/opportunities/crm-service-requests.js",
|
||||
"src/features/opportunities/fiscal-periods.js",
|
||||
"src/features/opportunities/table.js",
|
||||
"src/features/opportunities/dashboard.js",
|
||||
|
||||
@@ -104,6 +104,8 @@ function createContentApplication(overrides = {}) {
|
||||
requestTokenRelayOnce: (...args) => authContentSession.requestTokenRelayOnce(...args),
|
||||
resetCustomerAndOwnerFiltersForPeriod: (...args) => opportunitiesFilters.resetCustomerAndOwnerFiltersForPeriod(...args),
|
||||
resetOpportunitiesTable: (...args) => opportunitiesController.resetOpportunitiesTable(...args),
|
||||
loadCrmServiceRequests: (...args) => opportunityCrmServiceRequests.load(...args),
|
||||
resetCrmServiceRequests: () => opportunityCrmServiceRequests.reset(),
|
||||
restoreFilterPreferences: (...args) => preferencesStorage.restoreFilterPreferences(...args),
|
||||
saveFilterPreferences: (...args) => preferencesStorage.saveFilterPreferences(...args),
|
||||
toggleCustomerFilterPanel: (...args) => opportunitiesFilters.toggleCustomerFilterPanel(...args),
|
||||
@@ -176,6 +178,8 @@ function createContentApplication(overrides = {}) {
|
||||
productsContentState,
|
||||
renderProductsProgress: (...args) => productsContentJobs.renderProductsProgress(...args),
|
||||
requestOpportunitiesPage: (...args) => fusionOpportunities.requestOpportunitiesPage(...args),
|
||||
loadCrmServiceRequests: (...args) => opportunityCrmServiceRequests.load(...args),
|
||||
resetCrmServiceRequests: () => opportunityCrmServiceRequests.reset(),
|
||||
requestOpptyProductsInBackground: (...args) => productsContentJobs.requestOpptyProductsInBackground(...args),
|
||||
requestTokenRelayOnce: (...args) => authContentSession.requestTokenRelayOnce(...args),
|
||||
setOpportunitiesTableState: (...args) => opportunitiesTable.setOpportunitiesTableState(...args)
|
||||
@@ -193,6 +197,18 @@ function createContentApplication(overrides = {}) {
|
||||
environment
|
||||
});
|
||||
const opportunitiesFiscalPeriods = createOpportunitiesFiscalPeriods();
|
||||
const fusionCrmServiceRequests = createFusionCrmServiceRequests({
|
||||
isCurrentPeriodRequest: (...args) => opportunitiesController.isCurrentPeriodRequest(...args),
|
||||
logRequestStart: (...args) => debugContentLog.logRequestStart(...args),
|
||||
logRequestSuccess: (...args) => debugContentLog.logRequestSuccess(...args),
|
||||
logRequestFailure: (...args) => debugContentLog.logRequestFailure(...args),
|
||||
readResponseBodyForDebug: (...args) => debugContentLog.readResponseBodyForDebug(...args),
|
||||
maskToken: (...args) => debugContentLog.maskToken(...args), environment
|
||||
});
|
||||
const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({
|
||||
requestCrmServiceRequest: (...args) => fusionCrmServiceRequests.requestCrmServiceRequest(...args),
|
||||
isCurrentPeriodRequest: (...args) => opportunitiesController.isCurrentPeriodRequest(...args), environment
|
||||
});
|
||||
const opportunitiesTable = createOpportunitiesTable({
|
||||
ACCOUNT_DETAIL_URL: contracts.ACCOUNT_DETAIL_URL,
|
||||
OPPORTUNITIES_COLUMNS: columns,
|
||||
@@ -205,6 +221,7 @@ function createContentApplication(overrides = {}) {
|
||||
createDetailLink: (...args) => opportunitiesLinks.createDetailLink(...args),
|
||||
createProductsCountButton: (...args) => productsDetails.createProductsCountButton(...args),
|
||||
createProductsRampBadge: (...args) => productsDetails.createProductsRampBadge(...args),
|
||||
createCrmServiceRequestBadge: (...args) => opportunityCrmServiceRequests.createBadge(...args),
|
||||
createProductsDetailRow: (...args) => productsDetails.createProductsDetailRow(...args),
|
||||
createStageBadge: (...args) => opportunitiesLinks.createStageBadge(...args),
|
||||
createStatusBadge: (...args) => opportunitiesLinks.createStatusBadge(...args),
|
||||
@@ -468,6 +485,7 @@ function createContentApplication(overrides = {}) {
|
||||
}
|
||||
return {
|
||||
start, contracts,
|
||||
fusionCrmServiceRequests, opportunityCrmServiceRequests,
|
||||
authSessionState,
|
||||
authSalesCommonState,
|
||||
opportunitiesQueryState,
|
||||
|
||||
@@ -51,6 +51,8 @@ function createContentModal({
|
||||
renderStageFilterButtons,
|
||||
renderStatusFilterButtons,
|
||||
requestOpportunitiesForPeriod,
|
||||
loadCrmServiceRequests,
|
||||
resetCrmServiceRequests,
|
||||
requestTokenRelayOnce,
|
||||
resetCustomerAndOwnerFiltersForPeriod,
|
||||
resetOpportunitiesTable,
|
||||
@@ -73,7 +75,8 @@ function createContentModal({
|
||||
existingModal.querySelector("select").focus();
|
||||
document.documentElement.classList.add("opportunities-extension-scroll-lock");
|
||||
ensureEscapeKeyHandler();
|
||||
requestTokenRelayOnce().catch(() => {});
|
||||
requestTokenRelayOnce().then(token => loadCrmServiceRequests(token, opportunitiesQueryState.periodRequestVersion))
|
||||
.then(() => renderOpportunitiesTable()).catch(() => {});
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -429,6 +432,7 @@ function createContentModal({
|
||||
}
|
||||
|
||||
function closeOpportunitiesModal() {
|
||||
resetCrmServiceRequests();
|
||||
const modal = document.getElementById(MODAL_ID);
|
||||
|
||||
if (modal) {
|
||||
|
||||
@@ -25,7 +25,7 @@ function createDebugContentView({
|
||||
if (entry.name === "requestOpptyProducts") return "products";
|
||||
if (entry.name === "requestGenerateAiSummary") return "ai";
|
||||
if (entry.name === "requestInstallBase") return "installBase";
|
||||
if (entry.name === "requestCustomerServiceRequest") return "serviceRequests";
|
||||
if (entry.name === "requestCustomerServiceRequest" || entry.name === "resquestCrmServiceRequest") return "serviceRequests";
|
||||
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService|ConsumptionPlanDetail|RatecardByPlan)/.test(entry.name)) return "consumption";
|
||||
return "other";
|
||||
}
|
||||
|
||||
@@ -13,6 +13,8 @@ function createOpportunitiesController({
|
||||
productsContentState,
|
||||
renderProductsProgress,
|
||||
requestOpportunitiesPage,
|
||||
loadCrmServiceRequests,
|
||||
resetCrmServiceRequests,
|
||||
requestOpptyProductsInBackground,
|
||||
requestTokenRelayOnce,
|
||||
setOpportunitiesTableState
|
||||
@@ -78,6 +80,8 @@ function createOpportunitiesController({
|
||||
}
|
||||
|
||||
try {
|
||||
await loadCrmServiceRequests(token, requestVersion);
|
||||
if (!isCurrentPeriodRequest(requestVersion)) return;
|
||||
const allItems = await requestOpportunitiesPage(token, period, dateRange, 0, [], requestVersion, 1);
|
||||
|
||||
if (allItems && isCurrentPeriodRequest(requestVersion)) {
|
||||
@@ -102,6 +106,7 @@ function createOpportunitiesController({
|
||||
}
|
||||
|
||||
function resetOpportunitiesTable() {
|
||||
resetCrmServiceRequests();
|
||||
opportunitiesFilterState.selectedStages = new Set(STAGE_OPTIONS);
|
||||
opportunitiesFilterState.selectedStatuses = new Set(STATUS_OPTIONS);
|
||||
opportunitiesFilterState.selectedCustomerKeys = new Set();
|
||||
|
||||
55
src/features/opportunities/crm-service-requests.js
Normal file
55
src/features/opportunities/crm-service-requests.js
Normal file
@@ -0,0 +1,55 @@
|
||||
function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, environment }) {
|
||||
let status = "idle";
|
||||
let byOpportunity = new Map();
|
||||
let pending = null;
|
||||
let generation = 0;
|
||||
function reset() { generation += 1; pending = null; status = "idle"; byOpportunity = new Map(); }
|
||||
function load(token, version) {
|
||||
if (!isCurrentPeriodRequest(version)) return;
|
||||
if (!pending) pending = run(token, version, generation);
|
||||
return pending;
|
||||
}
|
||||
async function run(token, version, currentGeneration) {
|
||||
const isActive = () => currentGeneration === generation;
|
||||
status = "loading";
|
||||
try {
|
||||
const items = await requestCrmServiceRequest(token, version, isActive);
|
||||
if (!items || !isActive()) return;
|
||||
const grouped = new Map();
|
||||
for (const item of items) {
|
||||
const number = item?.OpportunityName_Id_c?.OptyNumber;
|
||||
if (!number) continue;
|
||||
const key = String(number);
|
||||
if (!grouped.has(key)) grouped.set(key, []);
|
||||
grouped.get(key).push(item);
|
||||
}
|
||||
byOpportunity = grouped;
|
||||
status = "ready";
|
||||
} catch {
|
||||
if (isActive()) status = "error";
|
||||
}
|
||||
}
|
||||
function createBadge(optyNumber) {
|
||||
if (!optyNumber || status === "idle" || status === "loading") return null;
|
||||
const items = byOpportunity.get(String(optyNumber)) || [];
|
||||
const badge = environment.document.createElement("span");
|
||||
badge.className = "opportunities-extension-crm-sr-badge";
|
||||
badge.setAttribute("data-state", status === "error" ? "error" : items.length ? "matched" : "missing");
|
||||
badge.textContent = status === "error" ? "SR ?" : items.length ? "\u2705 SR" : "\u26d4 SR";
|
||||
const display = value => value === null || value === undefined || value === "" ? "N/D" : String(value);
|
||||
badge.title = status === "error" ? "Unable to check CRM service requests. See Debug Requests."
|
||||
: !items.length ? "No matching CRM service request with status SVC_PENDING_DELIVERY in your SR team."
|
||||
: items.map(item => [
|
||||
`SR Number: ${display(item.SrNumber)}`, `Title: ${display(item.Title)}`,
|
||||
`Opportunity: ${display(item.OpportunityName_Id_c?.OptyNumber)}`,
|
||||
`Reported Hours: ${display(item.ReportedHours_c)}`,
|
||||
`Owner: ${[item.Owner?.PersonFirstName, item.Owner?.PersonLastName].filter(Boolean).join(" ") || "N/D"}`,
|
||||
`Email: ${display(item.Owner?.PrimaryEmail?.EmailAddress)}`,
|
||||
`Do Not Call: ${display(item.DoNotCallFlag)}`, `Do Not Email: ${display(item.DoNotEmailFlag)}`
|
||||
].join("\n")).join("\n\n");
|
||||
badge.setAttribute("aria-label", badge.title);
|
||||
badge.tabIndex = 0;
|
||||
return badge;
|
||||
}
|
||||
return { reset, load, createBadge };
|
||||
}
|
||||
@@ -1,4 +1,23 @@
|
||||
/* Compact filters use available CSS width, including Windows display scaling. */
|
||||
.opportunities-extension-crm-sr-badge {
|
||||
flex: 0 0 auto;
|
||||
padding: 0 6px;
|
||||
border: 1px solid #408254;
|
||||
border-radius: 8px;
|
||||
font-size: 10px;
|
||||
line-height: 18px;
|
||||
font-weight: 600;
|
||||
white-space: nowrap;
|
||||
color: #185b30;
|
||||
background: #e1f1e5;
|
||||
}
|
||||
.opportunities-extension-crm-sr-badge[data-state="missing"] { color: #9f201b; background: #fde8e7; border-color: #c74640; }
|
||||
.opportunities-extension-crm-sr-badge[data-state="error"] { color: #785000; background: #fff0cd; border-color: #987021; }
|
||||
.opportunities-extension-crm-sr-badge:focus-visible { outline: 2px solid currentColor; outline-offset: 2px; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-crm-sr-badge { color: #b8e4c6; background: #214d34; border-color: #6eac82; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-crm-sr-badge[data-state="missing"] { color: #ffd9d5; background: #542824; border-color: #ef938b; }
|
||||
.opportunities-extension-modal[data-theme="dark"] .opportunities-extension-crm-sr-badge[data-state="error"] { color: #ffe0a3; background: #584219; border-color: #c99b4b; }
|
||||
|
||||
.opportunities-extension-form {
|
||||
display: grid;
|
||||
grid-template-columns: max-content minmax(110px, 1fr) minmax(110px, 1fr) minmax(145px, 180px) max-content max-content;
|
||||
|
||||
@@ -10,6 +10,7 @@ function createOpportunitiesTable({
|
||||
createDetailLink,
|
||||
createProductsCountButton,
|
||||
createProductsRampBadge,
|
||||
createCrmServiceRequestBadge,
|
||||
createProductsDetailRow,
|
||||
createStageBadge,
|
||||
createStatusBadge,
|
||||
@@ -134,6 +135,8 @@ function createOpportunitiesTable({
|
||||
const rampBadge = createProductsRampBadge(productsResult);
|
||||
if (rampBadge) productActions.append(rampBadge);
|
||||
}
|
||||
const srBadge = createCrmServiceRequestBadge(item.OptyNumber);
|
||||
if (srBadge) productActions.append(srBadge);
|
||||
nameCellContent.append(opportunityLink, productActions);
|
||||
cell.append(nameCellContent);
|
||||
} else if (column.key === "optyNumber" && item.OptyNumber) {
|
||||
|
||||
60
src/services/fusion/crm-service-requests.js
Normal file
60
src/services/fusion/crm-service-requests.js
Normal file
@@ -0,0 +1,60 @@
|
||||
function createFusionCrmServiceRequests({ isCurrentPeriodRequest,
|
||||
logRequestStart, logRequestSuccess, logRequestFailure, readResponseBodyForDebug, maskToken, environment }) {
|
||||
const { fetch } = environment;
|
||||
const url = "https://eeho.fa.us2.oraclecloud.com/crmRestApi/searchResources/latest/custom-actions/queries";
|
||||
function createPayload(offset) {
|
||||
return {
|
||||
aggregationResults: false, applications: ["ORACLE-ISS-APP"], onlyData: true,
|
||||
entity: "CRMServiceRequest", copiedFrom: "queries/c521309c-1310-49a0-9723-da2eb1988205",
|
||||
q: { op: "$and", criteria: [
|
||||
{ op: "$in", attribute: "StatusCd", values: ["SVC_PENDING_DELIVERY"] },
|
||||
{ op: "$eq", attribute: "RecordSet", value: "ORA_SVC_MY_SR_TEAM_SRS" }
|
||||
] },
|
||||
keywords: null,
|
||||
fields: ["SrNumber", "OpportunityName_Id_c.OptyNumber", "ReportedHours_c", "_actions", "Title",
|
||||
"Owner.PersonFirstName", "Owner.PersonLastName", "Owner.PrimaryEmail.EmailAddress", "DoNotCallFlag", "DoNotEmailFlag"],
|
||||
sort: [{ attribute: "_score", direction: "descending" }], language: "en",
|
||||
keywordsFields: ["SrNumber", "ServiceNew_Id_c.RecordName", "CustomerAccount", "OpportunityName_Id_c",
|
||||
"OpportunityName_Id_c.OptyNumber", "Owner"], skipInValidFields: true, limit: 15, offset
|
||||
};
|
||||
}
|
||||
async function requestCrmServiceRequest(token, version, isActive = () => isCurrentPeriodRequest(version)) {
|
||||
const items = [];
|
||||
let offset = 0;
|
||||
while (isActive()) {
|
||||
const payload = createPayload(offset);
|
||||
const headers = {
|
||||
Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${token}`,
|
||||
Origin: "https://eeho.fa.us2.oraclecloud.com", Preference: "transient"
|
||||
};
|
||||
const entry = logRequestStart({ name: "resquestCrmServiceRequest", method: "POST", url,
|
||||
headers: { ...headers, Authorization: `Bearer ${maskToken(token)}` }, body: JSON.stringify(payload, null, 2), metadata: { offset } });
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST", credentials: "include", headers, body: JSON.stringify(payload)
|
||||
});
|
||||
logRequestSuccess(entry, response, await readResponseBodyForDebug(response));
|
||||
if (!response.ok) throw new Error(`CRM service request query failed with status ${response.status}.`);
|
||||
const data = await response.json();
|
||||
if (!data || !Array.isArray(data.items) || typeof data.hasMore !== "boolean") {
|
||||
throw new Error("CRM service request query returned an invalid response.");
|
||||
}
|
||||
if (!isActive()) return null;
|
||||
items.push(...data.items);
|
||||
if (!data.hasMore) return items;
|
||||
const received = data.count === undefined ? data.items.length : Number(data.count);
|
||||
const current = data.offset === undefined ? offset : Number(data.offset);
|
||||
if (!data.items.length || !Number.isInteger(received) || received <= 0 || !Number.isInteger(current)
|
||||
|| current !== offset || current + received <= offset) {
|
||||
throw new Error("CRM service request pagination did not advance.");
|
||||
}
|
||||
offset = current + received;
|
||||
} catch (error) {
|
||||
logRequestFailure(entry, error.message || "CRM service request query failed.");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
return { createPayload, requestCrmServiceRequest };
|
||||
}
|
||||
151
tests/crmServiceRequests.test.cjs
Normal file
151
tests/crmServiceRequests.test.cjs
Normal file
@@ -0,0 +1,151 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const { test } = require("node:test");
|
||||
const { loadApplication } = require("./support/application.cjs");
|
||||
const response = data => new Response(JSON.stringify(data), { status: 200 });
|
||||
function setup(fetch) {
|
||||
const document = { createElement: () => ({ attributes: {}, setAttribute(key, value) { this.attributes[key] = value; } }) };
|
||||
const app = loadApplication("content", { fetch, document, Intl, Date });
|
||||
app.debugContentView.renderDebugPanel = () => {};
|
||||
app.opportunitiesQueryState.periodRequestVersion = 1;
|
||||
return app;
|
||||
}
|
||||
const sr = (number, id = "SR-1") => ({ SrNumber: id, Title: "<script>synthetic</script>", ReportedHours_c: 0,
|
||||
OpportunityName_Id_c: { OptyNumber: number }, Owner: { PersonFirstName: "Test", PersonLastName: "Owner",
|
||||
PrimaryEmail: { EmailAddress: "test@example.invalid" } }, DoNotCallFlag: false });
|
||||
|
||||
test("CRM fetch is called without the environment object as its receiver", async () => {
|
||||
let calls = 0;
|
||||
const app = setup(async function (url, options) {
|
||||
"use strict";
|
||||
assert.equal(this, undefined, "Browser fetch must not receive the dependency container as this");
|
||||
calls += 1;
|
||||
const { offset } = JSON.parse(options.body);
|
||||
return response({ items: [sr("TEST")], count: 1, offset, hasMore: offset === 0 });
|
||||
});
|
||||
const items = await app.fusionCrmServiceRequests.requestCrmServiceRequest("synthetic-token", 1);
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(items.length, 2);
|
||||
});
|
||||
|
||||
test("CRM query preserves payload, paginates before opportunities, uses tokenrelay Bearer and logs each page", async () => {
|
||||
const calls = [];
|
||||
const app = setup(async (url, options) => {
|
||||
const payload = JSON.parse(options.body);
|
||||
calls.push({ url, options, payload });
|
||||
return response({ items: [sr("TEST-1", `SR-${payload.offset}`)], count: 1, offset: payload.offset, hasMore: payload.offset === 0 });
|
||||
});
|
||||
const states = [];
|
||||
app.opportunitiesTable.setOpportunitiesTableState = state => states.push(state);
|
||||
app.productsContentJobs.renderProductsProgress = () => {};
|
||||
app.productsContentJobs.requestOpptyProductsInBackground = () => {};
|
||||
app.fusionOpportunities.requestOpportunitiesPage = async () => {
|
||||
assert.equal(calls.length, 2, "all CRM pages must finish first");
|
||||
return [{ OptyNumber: "TEST-1" }];
|
||||
};
|
||||
await app.opportunitiesController.requestOpportunities("synthetic-token", "Current Quarter", {}, 1);
|
||||
assert.equal(states.at(-1).status, "ready");
|
||||
assert.deepEqual(calls.map(call => call.payload.offset), [0, 1]);
|
||||
const { payload, options, url } = calls[0];
|
||||
assert.equal(url, "https://eeho.fa.us2.oraclecloud.com/crmRestApi/searchResources/latest/custom-actions/queries");
|
||||
assert.equal(options.method, "POST");
|
||||
assert.equal(options.credentials, "include");
|
||||
assert.equal(options.headers.Authorization, "Bearer synthetic-token");
|
||||
assert.equal(options.headers["Content-Type"], "application/json");
|
||||
assert.equal(options.headers.Origin, "https://eeho.fa.us2.oraclecloud.com");
|
||||
assert.equal(options.headers.Preference, "transient");
|
||||
assert.deepEqual(payload, {
|
||||
aggregationResults: false, applications: ["ORACLE-ISS-APP"], onlyData: true, entity: "CRMServiceRequest",
|
||||
copiedFrom: "queries/c521309c-1310-49a0-9723-da2eb1988205",
|
||||
q: { op: "$and", criteria: [
|
||||
{ op: "$in", attribute: "StatusCd", values: ["SVC_PENDING_DELIVERY"] },
|
||||
{ op: "$eq", attribute: "RecordSet", value: "ORA_SVC_MY_SR_TEAM_SRS" }
|
||||
] }, keywords: null,
|
||||
fields: ["SrNumber", "OpportunityName_Id_c.OptyNumber", "ReportedHours_c", "_actions", "Title", "Owner.PersonFirstName",
|
||||
"Owner.PersonLastName", "Owner.PrimaryEmail.EmailAddress", "DoNotCallFlag", "DoNotEmailFlag"],
|
||||
sort: [{ attribute: "_score", direction: "descending" }], language: "en",
|
||||
keywordsFields: ["SrNumber", "ServiceNew_Id_c.RecordName", "CustomerAccount", "OpportunityName_Id_c", "OpportunityName_Id_c.OptyNumber", "Owner"],
|
||||
skipInValidFields: true, limit: 15, offset: 0
|
||||
});
|
||||
assert.equal(app.debugState.requestLog.length, 2);
|
||||
for (const log of app.debugState.requestLog) {
|
||||
assert.equal(log.name, "resquestCrmServiceRequest");
|
||||
assert.equal(log.requestHeaders.Origin, "https://eeho.fa.us2.oraclecloud.com");
|
||||
assert.equal(log.requestHeaders.Preference, "transient");
|
||||
assert.equal(log.status, "success");
|
||||
assert.ok(log.responseBody);
|
||||
assert.ok(log.durationMs >= 0);
|
||||
assert.doesNotMatch(log.requestHeaders.Authorization, /synthetic-token/);
|
||||
assert.equal(app.debugContentView.getRequestCategory(log), "serviceRequests");
|
||||
}
|
||||
const found = app.opportunityCrmServiceRequests.createBadge("TEST-1");
|
||||
assert.equal(found.textContent, "\u2705 SR");
|
||||
assert.equal(found.attributes["data-state"], "matched");
|
||||
for (const detail of ["SR-0", "SR-1", "Reported Hours: 0", "Test Owner", "test@example.invalid", "<script>synthetic</script>"])
|
||||
assert.ok(found.title.includes(detail));
|
||||
assert.equal(app.opportunityCrmServiceRequests.createBadge("OTHER").textContent, "\u26d4 SR");
|
||||
assert.equal(app.opportunityCrmServiceRequests.createBadge(null), null);
|
||||
app.opportunityCrmServiceRequests.reset();
|
||||
assert.equal(app.opportunityCrmServiceRequests.createBadge("TEST-1"), null);
|
||||
});
|
||||
|
||||
test("CRM failures do not mislabel missing SRs or block the opportunity list", async () => {
|
||||
for (const fetch of [
|
||||
async () => { throw new Error("Synthetic network failure"); },
|
||||
async () => new Response("Denied", { status: 401 }),
|
||||
async () => new Response("Forbidden", { status: 403 }),
|
||||
async () => new Response("<html>Not JSON</html>"),
|
||||
async () => response({ items: [] }),
|
||||
async () => response({ items: [], count: 0, hasMore: true }),
|
||||
async () => response({ items: [sr("TEST")], count: 1, offset: -1, hasMore: true })
|
||||
]) {
|
||||
const app = setup(fetch);
|
||||
app.productsContentJobs.renderProductsProgress = () => {};
|
||||
app.productsContentJobs.requestOpptyProductsInBackground = () => {};
|
||||
let loaded = false;
|
||||
app.opportunitiesTable.setOpportunitiesTableState = () => {};
|
||||
app.fusionOpportunities.requestOpportunitiesPage = async () => { loaded = true; return []; };
|
||||
await app.opportunitiesController.requestOpportunities("token", "Current Quarter", {}, 1);
|
||||
assert.equal(loaded, true);
|
||||
assert.equal(app.opportunityCrmServiceRequests.createBadge("TEST").textContent, "SR ?");
|
||||
assert.equal(app.debugState.requestLog.at(-1).status, "failed");
|
||||
}
|
||||
});
|
||||
|
||||
test("stale CRM results cannot overwrite a reopened modal or continue pagination", async () => {
|
||||
let resolveOld;
|
||||
const app = setup(() => new Promise(resolve => { resolveOld = resolve; }));
|
||||
const old = app.opportunityCrmServiceRequests.load("token", 1);
|
||||
app.opportunitiesQueryState.periodRequestVersion = 2;
|
||||
app.opportunityCrmServiceRequests.reset();
|
||||
app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [sr("NEW")];
|
||||
await app.opportunityCrmServiceRequests.load("token", 2);
|
||||
resolveOld(response({ items: [sr("OLD")], hasMore: true, count: 1, offset: 0 }));
|
||||
await old;
|
||||
assert.equal(app.opportunityCrmServiceRequests.createBadge("NEW").textContent, "\u2705 SR");
|
||||
assert.equal(app.opportunityCrmServiceRequests.createBadge("OLD").textContent, "\u26d4 SR");
|
||||
assert.equal(app.debugState.requestLog.length, 1);
|
||||
});
|
||||
|
||||
test("Reference changes share pending pagination and reuse completed CRM results", async () => {
|
||||
let finish;
|
||||
let calls = 0;
|
||||
const app = setup(async () => {
|
||||
calls += 1;
|
||||
if (calls === 1) return new Promise(resolve => { finish = resolve; });
|
||||
return response({ items: [sr("B")], hasMore: false });
|
||||
});
|
||||
const first = app.opportunityCrmServiceRequests.load("token", 1);
|
||||
app.opportunitiesQueryState.periodRequestVersion = 2;
|
||||
const second = app.opportunityCrmServiceRequests.load("token", 2);
|
||||
assert.equal(first, second);
|
||||
finish(response({ items: [sr("A")], count: 1, offset: 0, hasMore: true }));
|
||||
await second;
|
||||
app.opportunitiesQueryState.periodRequestVersion = 3;
|
||||
await app.opportunityCrmServiceRequests.load("token", 3);
|
||||
assert.equal(calls, 2);
|
||||
assert.equal(app.opportunityCrmServiceRequests.createBadge("A").textContent, "\u2705 SR");
|
||||
assert.equal(app.opportunityCrmServiceRequests.createBadge("B").textContent, "\u2705 SR");
|
||||
app.opportunityCrmServiceRequests.reset();
|
||||
await app.opportunityCrmServiceRequests.load("token", 3);
|
||||
assert.equal(calls, 3);
|
||||
});
|
||||
@@ -116,6 +116,22 @@ test("products remain expandable with AI summary and a ten-column detail row aft
|
||||
assert.deepEqual(h.summaries, ["B"], "collapsing does not generate another summary");
|
||||
});
|
||||
|
||||
test("CRM SR badge follows Ramp Months and remains visible while product data is pending", async () => {
|
||||
const h = setup();
|
||||
h.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [{
|
||||
SrNumber: "SR-SYNTH", OpportunityName_Id_c: { OptyNumber: "TEST-1" }, ReportedHours_c: 0
|
||||
}];
|
||||
await h.app.opportunityCrmServiceRequests.load("synthetic", h.app.opportunitiesQueryState.periodRequestVersion);
|
||||
h.app.opportunitiesTable.renderOpportunitiesTable();
|
||||
assert.equal(findClass(h.cell("name"), "opportunities-extension-crm-sr-badge").textContent, "\u2705 SR");
|
||||
h.app.productsContentState.opptyProductsByNumber.set("TEST-1", { items: [{ RampMonths_c: 3 }] });
|
||||
h.app.opportunitiesTable.renderOpportunitiesTable();
|
||||
const actions = h.cell("name").children[0].children[1];
|
||||
assert.deepEqual(actions.children.map(child => child.textContent), ["1", "RM 3", "\u2705 SR"]);
|
||||
findClass(h.cell("name"), "opportunities-extension-products-count").click();
|
||||
assert.ok(findClass(h.rows()[1], "opportunities-extension-products-table"));
|
||||
});
|
||||
|
||||
test("product table distinguishes unavailable ramp months from numeric zero", () => {
|
||||
const h = setup();
|
||||
h.app.productsContentState.opptyProductsByNumber.set("TEST-1", {
|
||||
|
||||
Reference in New Issue
Block a user