Implemented the Create Internal Service Request functionality
This commit is contained in:
246
dist/chromium/background.js
vendored
246
dist/chromium/background.js
vendored
@@ -20,9 +20,12 @@ function createMessageTypes() {
|
||||
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
||||
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
|
||||
requestPostServiceRequest: "opportunitiesExtension.requestPostServiceRequest",
|
||||
requestPostInternalServiceRequest: "opportunitiesExtension.requestPostInternalServiceRequest",
|
||||
prepareSrSession: "opportunitiesExtension.prepareSrSession",
|
||||
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
|
||||
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
|
||||
requestLookupTypePlatform: "opportunitiesExtension.requestLookupTypePlatform",
|
||||
requestInternalSrServices: "opportunitiesExtension.requestInternalSrServices",
|
||||
requestSrLookupOpportunityBusiness: "opportunitiesExtension.requestSrLookupOpportunityBusiness",
|
||||
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
|
||||
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",
|
||||
@@ -33,45 +36,8 @@ function createMessageTypes() {
|
||||
}
|
||||
|
||||
// Source: src/shared/debug-body.js
|
||||
function createDebugBodyPreview(text, limit) {
|
||||
if (text.length <= limit) return { value: text || "(empty response body)", truncated: false };
|
||||
try {
|
||||
const normalized = text.replace(/^\uFEFF/, "").trimStart()
|
||||
.replace(/^\)\]\}',?\s*/, "").replace(/^while\s*\(1\);\s*/, "");
|
||||
let value = JSON.parse(normalized);
|
||||
for (let depth = 0; depth < 2 && typeof value === "string" && /^\s*[\[{]/.test(value); depth += 1) {
|
||||
try { value = JSON.parse(value); } catch { break; }
|
||||
}
|
||||
// Bound retained data without cutting JSON syntax or losing sensitive field names.
|
||||
let remaining = Math.max(0, limit - 256);
|
||||
const visit = (node, depth = 0) => {
|
||||
if (remaining <= 64 || depth >= 30) return "[omitted from debug preview]";
|
||||
if (node === null || typeof node !== "object") {
|
||||
const serialized = JSON.stringify(node);
|
||||
if (serialized.length > remaining) {
|
||||
remaining = 0;
|
||||
return "[omitted from debug preview]";
|
||||
}
|
||||
remaining -= serialized.length + 1;
|
||||
return node;
|
||||
}
|
||||
const result = Array.isArray(node) ? [] : Object.create(null);
|
||||
remaining -= 2;
|
||||
for (const key of Object.keys(node)) {
|
||||
const cost = Array.isArray(node) ? 1 : JSON.stringify(key).length + 2;
|
||||
if (remaining < cost + 64) break;
|
||||
remaining -= cost;
|
||||
result[key] = visit(node[key], depth + 1);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return {
|
||||
value: JSON.stringify({ debugNotice: "Partial response preview: remaining fields/items omitted due to the debug size limit.", preview: visit(value) }),
|
||||
truncated: true
|
||||
};
|
||||
} catch {
|
||||
return { value: `${text.slice(0, limit)}\n... truncated ${text.length - limit} characters`, truncated: true };
|
||||
}
|
||||
function createDebugBodyPreview(text) {
|
||||
return { value: text || "(empty response body)", truncated: false };
|
||||
}
|
||||
|
||||
// Source: src/background/contracts.js
|
||||
@@ -435,6 +401,7 @@ function createAuthSpaCredentials({
|
||||
function createSrRequestHeaders({ runtimeApi }) {
|
||||
const origin = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||
const appPath = "/ic/builder/rt/SeaaS_initiation/";
|
||||
const internalPath = "/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPICloudAccount/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests";
|
||||
const storageKey = "opportunitiesExtension.srAppBuilderClientId.v1";
|
||||
const maxAge = 60 * 60 * 1000;
|
||||
let observed = null;
|
||||
@@ -448,7 +415,7 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
function observeHeaders(rawUrl, headers) {
|
||||
let url;
|
||||
try { url = new URL(rawUrl); } catch { return; }
|
||||
if (url.origin !== origin || !/^\/ic\/builder\/rt\/SeaaS_initiation\/live(?:\/|;)/.test(url.pathname)) return;
|
||||
if (url.origin !== origin || !/^\/ic\/builder\/rt\/(?:SeaaS_initiation|SeaaS_SR_management)\/live(?:\/|;)/.test(url.pathname)) return;
|
||||
// Read only the client ID, not cookies, bodies or authorization.
|
||||
const value = typeof headers?.get === "function" ? headers.get("x-appbuilder-client-id")
|
||||
: headers?.find(item => item.name.toLowerCase() === "x-appbuilder-client-id")?.value;
|
||||
@@ -461,7 +428,7 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
function start() {
|
||||
if (started || !runtimeApi?.webRequest) return;
|
||||
started = true;
|
||||
const filter = { urls: [`${origin}${appPath}*`], types: ["xmlhttprequest"] };
|
||||
const filter = { urls: [`${origin}${appPath}*`, `${origin}/ic/builder/rt/SeaaS_SR_management/*`], types: ["xmlhttprequest"] };
|
||||
runtimeApi.webRequest.onBeforeSendHeaders?.addListener(details => {
|
||||
if (details.tabId >= 0 && !details.incognito) observeHeaders(details.url, details.requestHeaders);
|
||||
}, filter, ["requestHeaders"]);
|
||||
@@ -474,6 +441,9 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
if (target.origin !== origin || !target.pathname.startsWith(appPath) || !/\/Catalogue_c(?:\/\d+)?$/.test(target.pathname)) {
|
||||
throw new Error("Unexpected Service Request token URL.");
|
||||
}
|
||||
return sessionHeaders(target.origin, "application/vnd.oracle.adf.action+json");
|
||||
}
|
||||
async function sessionHeaders(targetOrigin, contentType) {
|
||||
if (!valid(observed)) {
|
||||
try {
|
||||
const stored = await runtimeApi.storage?.session?.get(storageKey);
|
||||
@@ -481,8 +451,8 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
} catch { /* A missing capture must not prevent trying the other required headers. */ }
|
||||
}
|
||||
return {
|
||||
Accept: "application/json", "Content-Type": "application/vnd.oracle.adf.action+json",
|
||||
Authorization: "Session", Origin: target.origin,
|
||||
Accept: "application/json", "Content-Type": contentType,
|
||||
Authorization: "Session", Origin: targetOrigin,
|
||||
...(valid(observed) ? { "x-appbuilder-client-id": observed.value } : {})
|
||||
};
|
||||
}
|
||||
@@ -490,8 +460,8 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
const headers = await previewTokenHeaders(url);
|
||||
if (!/\/Catalogue_c\/\d+$/.test(new URL(url).pathname)) throw new Error("Missing Catalogue Id.");
|
||||
const serviceTarget = new URL(serviceRequestUrl);
|
||||
if (serviceTarget.origin !== headers.Origin || !serviceTarget.pathname.startsWith(appPath)
|
||||
|| !serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests") || serviceTarget.search || serviceTarget.hash) {
|
||||
const isServiceRequest = serviceTarget.pathname.startsWith(appPath) && serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests");
|
||||
if (serviceTarget.origin !== headers.Origin || !isServiceRequest || serviceTarget.search || serviceTarget.hash) {
|
||||
throw new Error("Unexpected Service Request URL.");
|
||||
}
|
||||
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
|
||||
@@ -501,10 +471,11 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
const catalogUrl = url.slice(0, url.lastIndexOf("/") + 1);
|
||||
const escapedCatalogUrl = catalogUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const escapedServiceUrl = serviceTarget.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const ruleIds = [7201, 7202];
|
||||
await runtimeApi.declarativeNetRequest.updateSessionRules({
|
||||
removeRuleIds: [7201, 7202],
|
||||
removeRuleIds: ruleIds,
|
||||
addRules: [`^${escapedCatalogUrl}[0-9]+$`, `^${escapedServiceUrl}$`].map((regexFilter, index) => ({
|
||||
id: 7201 + index, priority: 1,
|
||||
id: ruleIds[index], priority: 1,
|
||||
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
|
||||
condition: { regexFilter, initiatorDomains: [extensionDomain],
|
||||
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
|
||||
@@ -512,7 +483,29 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
return { start, tokenHeaders, previewTokenHeaders, observeResponseHeaders: observeHeaders };
|
||||
async function internalServiceRequestHeaders(url) {
|
||||
const target = new URL(url);
|
||||
if (target.origin !== origin || target.pathname !== internalPath || target.search || target.hash) {
|
||||
throw new Error("Unexpected Internal Service Request URL.");
|
||||
}
|
||||
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
|
||||
throw new Error("Reload the extension to enable the Internal Service Request header permissions.");
|
||||
}
|
||||
const headers = await sessionHeaders(target.origin, "application/json");
|
||||
const extensionDomain = new URL(runtimeApi.runtime.getURL("/")).hostname;
|
||||
const escapedUrl = target.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
await runtimeApi.declarativeNetRequest.updateSessionRules({
|
||||
removeRuleIds: [7203],
|
||||
addRules: [{
|
||||
id: 7203, priority: 1,
|
||||
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
|
||||
condition: { regexFilter: `^${escapedUrl}$`, initiatorDomains: [extensionDomain],
|
||||
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
|
||||
}]
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
return { start, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders, observeResponseHeaders: observeHeaders };
|
||||
}
|
||||
|
||||
// Source: src/services/spa/accounts.js
|
||||
@@ -1029,7 +1022,33 @@ function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFire
|
||||
return { ok: true, items: result.items.map(({ Id, RecordName, Abstract_c }) => ({ Id: String(Id), RecordName, Abstract_c: Abstract_c == null ? "" : String(Abstract_c) })), debugRequests };
|
||||
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||
}
|
||||
async function requestInternalSrOptions(message, services = false) {
|
||||
const debugRequests = [];
|
||||
const name = services ? "requestInternalSrServices" : "requestLookupTypePlatform";
|
||||
try {
|
||||
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetSeaaSOKECustomRestAPI/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/";
|
||||
let path = "lookups?type=STANDARD&lookupType=SENGG_TECH_PLATFORM";
|
||||
if (services) {
|
||||
const id = String(message.parentServiceId ?? "");
|
||||
if (!/^\d+$/.test(id)) throw new Error("A numeric parent SrId is required.");
|
||||
if (typeof message.emailAddress !== "string" || !message.emailAddress.trim()) throw new Error("Current user email is unavailable.");
|
||||
path = `services?emailAddress=${encodeURIComponent(message.emailAddress.trim())}&parentServiceId=${encodeURIComponent(id)}&serviceType=INTERNAL`;
|
||||
}
|
||||
const result = await fetchJson(base + path, { method: "GET", credentials: "include",
|
||||
headers: { Accept: "application/json", Authorization: "Session" }
|
||||
}, name, debugRequests);
|
||||
if (!Array.isArray(result?.items) || result.items.some(item => services
|
||||
? !/^\d+$/.test(String(item?.Id ?? "")) || typeof item?.RecordName !== "string"
|
||||
: typeof item?.code !== "string" || typeof item?.name !== "string")) throw new Error(`Invalid ${name} response.`);
|
||||
const items = services
|
||||
? result.items.map(item => ({ Id: String(item.Id), RecordName: item.RecordName, Description_c: String(item.Description_c ?? "") }))
|
||||
: result.items.filter(item => item.enabledFlag !== "N").map(({ code, name }) => ({ code, name }));
|
||||
return { ok: true, items, debugRequests };
|
||||
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||
}
|
||||
return { requestLookupWithSession, requestInternalServiceRequest, requestSrLookupOpportunityBusiness,
|
||||
requestLookupTypePlatform: () => requestInternalSrOptions({}),
|
||||
requestInternalSrServices: message => requestInternalSrOptions(message, true),
|
||||
prepareSession: sender => requestLookupWithSession(null, {}, sender, true),
|
||||
requestSrLookupPrimaryPillar: () => requestLookup("SENGG_PRIMARY_PILLAR", "requestSrLookupPrimaryPillar"),
|
||||
requestSrLookupLanguage: () => requestLookup("SENGG_LANGUAGE", "requestSrLookupLanguage"),
|
||||
@@ -1037,7 +1056,7 @@ function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFire
|
||||
}
|
||||
|
||||
// Source: src/services/fusion/sr-create.js
|
||||
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders }) {
|
||||
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders }) {
|
||||
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/";
|
||||
const catalog = base + "crmRestApiResources/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/11.13.18.05/Catalogue_c";
|
||||
const resourceUsers = base + "oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/resourceUsers";
|
||||
@@ -1050,26 +1069,76 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
|
||||
if (typeof value !== "string" || !value.trim()) throw new Error(`Missing ${name}.`);
|
||||
return value.trim();
|
||||
}
|
||||
function dateOnly(value, name) {
|
||||
const date = text(value, name);
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|
||||
|| new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) throw new Error(`Invalid ${name}.`);
|
||||
return date;
|
||||
}
|
||||
function payloadFor(input) {
|
||||
const payload = {};
|
||||
for (const name of ["AccountPartyId", "OpportunityName_Id_c", "ServiceNew_Id_c"]) payload[name] = numericId(input?.[name], name);
|
||||
for (const name of ["Title", "RegionText_c", "ProblemDescription", "Language_c", "CustomerDeadline_c", "TechPlatformLookUp_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(input?.[name], name);
|
||||
const date = payload.CustomerDeadline_c;
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|
||||
|| new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) throw new Error("Invalid Customer deadline.");
|
||||
dateOnly(payload.CustomerDeadline_c, "Customer deadline");
|
||||
return { ...payload, SvcQuestionsCollection_c: [], SourceCd: "SVC_OPPORTUNITY_SCREEN", SeverityCd: "ORA_SVC_SEV2", ChannelTypeCd: "ORA_SVC_WEB" };
|
||||
}
|
||||
async function requestResourceUser(email, debugRequests) {
|
||||
const emailAddress = text(email, "current user email");
|
||||
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
|
||||
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||
}, "requestResourceUser", debugRequests);
|
||||
return numericId(resource?.resourcePartyId, "resourcePartyId");
|
||||
}
|
||||
async function requestPostInternalServiceRequest(message) {
|
||||
const debugRequests = [];
|
||||
try {
|
||||
const source = message.payload;
|
||||
const parentSrId = numericId(source?.ServiceRequest_Id_SRToInternalSR, "parent SrId");
|
||||
const payload = {
|
||||
Title: text(source?.Title, "Title"),
|
||||
ServiceNew_Id_c: numericId(source?.ServiceNew_Id_c, "ServiceNew_Id_c"),
|
||||
ProblemDescription: text(source?.ProblemDescription, "Description"),
|
||||
CustomerDeadline_c: dateOnly(source?.CustomerDeadline_c, "Customer deadline"),
|
||||
TechPlatformLookUp_c: text(source?.TechPlatformLookUp_c, "Platform"),
|
||||
InternalDeadline_c: `${dateOnly(source?.InternalDeadline_c, "Internal Deadline")}T00:00:00.000Z`,
|
||||
SvcQuestionsCollection_c: []
|
||||
};
|
||||
const parent = await requestSRInfo(parentSrId, debugRequests);
|
||||
payload.ServiceRequest_Id_SRToInternalSR = String(parentSrId);
|
||||
for (const name of ["AccountPartyId", "OpportunityName_Id_c"]) payload[name] = numericId(parent[name], name);
|
||||
for (const name of ["OpportunityName_c", "RegionText_c", "SvcHealth_c", "Language_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(parent[name], name);
|
||||
payload.SecondaryPillarNew_c = parent.SecondaryPillarNew_c == null ? null : text(parent.SecondaryPillarNew_c, "SecondaryPillarNew_c");
|
||||
payload.ReportedByPartyId = await requestResourceUser(message.emailAddress, debugRequests);
|
||||
const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPICloudAccount/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests";
|
||||
const result = await fetchJson(url, { method: "POST", credentials: "include",
|
||||
headers: await internalServiceRequestHeaders(url),
|
||||
body: JSON.stringify(payload)
|
||||
}, "requestPostInternalServiceRequest", debugRequests, { allowEmpty: true });
|
||||
return { ok: true, result, debugRequests };
|
||||
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||
}
|
||||
async function requestSRInfo(parentSrId, debugRequests) {
|
||||
const url = new URL("https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPI/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests");
|
||||
url.searchParams.set("fields", "PrimaryPillarNew_c,SecondaryPillarNew_c,SvcHealth_c,SrNumber,AccountPartyId,CountryText_c,Language_c,OpportunityName_Id_c,OpportunityName_c,RegionText_c,ServiceNew_Id_c");
|
||||
url.searchParams.set("onlyData", "true");
|
||||
url.searchParams.set("orderBy", "LastUpdateDate:desc");
|
||||
url.searchParams.set("q", `SrId=${parentSrId}`);
|
||||
const result = await fetchJson(url.href, {
|
||||
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||
}, "requestSRInfo", debugRequests);
|
||||
if (!Array.isArray(result?.items) || result.items.length !== 1 || !result.items[0]
|
||||
|| (result.items[0].SrId != null && String(result.items[0].SrId) !== String(parentSrId))) {
|
||||
throw new Error("requestSRInfo did not return the selected parent Service Request.");
|
||||
}
|
||||
return result.items[0];
|
||||
}
|
||||
async function requestPostServiceRequest(message) {
|
||||
const debugRequests = [];
|
||||
try {
|
||||
if (message.phase !== "prepare" && message.phase !== "confirm") throw new Error("Service Request confirmation is required.");
|
||||
const payload = payloadFor(message.payload);
|
||||
if (message.phase === "prepare") {
|
||||
const emailAddress = text(message.emailAddress, "current user email");
|
||||
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
|
||||
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||
}, "requestResourceUser", debugRequests);
|
||||
payload.ReportedByPartyId = numericId(resource?.resourcePartyId, "resourcePartyId");
|
||||
payload.ReportedByPartyId = await requestResourceUser(message.emailAddress, debugRequests);
|
||||
const sharedHeaders = await previewTokenHeaders(catalog);
|
||||
return { ok: true, payload, debugRequests, headers: {
|
||||
requestSrTokenCatalog: sharedHeaders,
|
||||
@@ -1077,7 +1146,20 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
|
||||
} };
|
||||
}
|
||||
payload.ReportedByPartyId = numericId(message.payload?.ReportedByPartyId, "ReportedByPartyId");
|
||||
let id = message.catalogId;
|
||||
const headers = await requestCreationHeaders(message.catalogId, endpoint, debugRequests);
|
||||
const result = await fetchJson(endpoint, {
|
||||
method: "POST", credentials: "include", headers,
|
||||
body: JSON.stringify(payload)
|
||||
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
|
||||
return { ok: true, result, debugRequests };
|
||||
} catch (error) {
|
||||
error.debugRequests = debugRequests;
|
||||
throw error;
|
||||
} finally {
|
||||
annotateSigningRequest(debugRequests);
|
||||
}
|
||||
}
|
||||
async function requestCreationHeaders(id, target, debugRequests) {
|
||||
if (id == null) {
|
||||
const result = await fetchJson(`${catalog}?onlyData=true&fields=Id&limit=1`, {
|
||||
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||
@@ -1086,7 +1168,7 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
|
||||
}
|
||||
id = numericId(id, "Catalogue Id");
|
||||
const tokenUrl = `${catalog}/${id}`;
|
||||
let catalogHeaders = await tokenHeaders(tokenUrl, endpoint);
|
||||
let catalogHeaders = await tokenHeaders(tokenUrl, target);
|
||||
const requestToken = () => fetchJson(tokenUrl, {
|
||||
method: "POST", credentials: "include", headers: catalogHeaders, body: JSON.stringify({ name: "getJwtToken" })
|
||||
}, "requestSrTokenCatalog", debugRequests);
|
||||
@@ -1103,27 +1185,16 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
|
||||
if (debugRequests.at(-1)?.httpStatus !== 200 || typeof token?.result !== "string" || !token.result.trim()) {
|
||||
throw new Error("requestSrTokenCatalog did not return HTTP 200 with a token.");
|
||||
}
|
||||
const result = await fetchJson(endpoint, {
|
||||
method: "POST", credentials: "include", headers: { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() },
|
||||
body: JSON.stringify(payload)
|
||||
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
|
||||
return { ok: true, result, debugRequests };
|
||||
} catch (error) {
|
||||
error.debugRequests = debugRequests;
|
||||
throw error;
|
||||
} finally {
|
||||
// Never forward the signing token to content scripts, even with Debug secrets enabled.
|
||||
return { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() };
|
||||
}
|
||||
function annotateSigningRequest(debugRequests) {
|
||||
for (const entry of debugRequests) {
|
||||
if (entry.name === "requestSrTokenCatalog") {
|
||||
entry.metadata.appBuilderClientId = entry.requestHeaders?.["x-appbuilder-client-id"] ? "Observed in SeaaS requests" : "Not observed in SeaaS requests";
|
||||
entry.responseBody = JSON.stringify({ result: "(redacted)" });
|
||||
entry.responseBodyTruncated = false;
|
||||
}
|
||||
if (entry.requestHeaders?.["vb-proxy-header-x-oracle-ectoken"]) entry.requestHeaders["vb-proxy-header-x-oracle-ectoken"] = "(redacted)";
|
||||
}
|
||||
}
|
||||
}
|
||||
return { requestPostServiceRequest };
|
||||
return { requestPostServiceRequest, requestPostInternalServiceRequest };
|
||||
}
|
||||
|
||||
// Source: src/services/spa/current-user.js
|
||||
@@ -1144,12 +1215,6 @@ function createSpaCurrentUser({ fetchJson }) {
|
||||
for (const entry of debugRequests) { entry.status = "failed"; entry.error = error.message; }
|
||||
error.debugRequests = debugRequests;
|
||||
throw error;
|
||||
} finally {
|
||||
// The identity token is not needed by content scripts or debug, including on failures.
|
||||
for (const entry of debugRequests) {
|
||||
entry.responseBody = JSON.stringify(user ? { user, token: "(redacted)" } : { error: "Identity response omitted." }, null, 2);
|
||||
entry.responseBodyTruncated = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { requestCurrentUser };
|
||||
@@ -1330,7 +1395,6 @@ function createSpaResponseShapes() {
|
||||
// Source: src/services/http/background-json.js
|
||||
function createHttpBackgroundJson({
|
||||
previewDebugBody,
|
||||
DEBUG_BODY_LIMIT,
|
||||
observeResponseHeaders = () => {},
|
||||
environment
|
||||
}) {
|
||||
@@ -1457,7 +1521,7 @@ function createHttpBackgroundJson({
|
||||
}
|
||||
|
||||
function createDebugBody(text) {
|
||||
return previewDebugBody(text, DEBUG_BODY_LIMIT);
|
||||
return previewDebugBody(text);
|
||||
}
|
||||
|
||||
function normalizeDebugHeaders(headers) {
|
||||
@@ -2227,12 +2291,15 @@ function registerBackgroundMessages({
|
||||
requestActionFindLookup,
|
||||
requestMgmtAction,
|
||||
requestPostServiceRequest,
|
||||
requestPostInternalServiceRequest,
|
||||
requestInternalServiceRequest,
|
||||
requestSrLookupPrimaryPillar,
|
||||
requestSrLookupWithSession,
|
||||
prepareSrSession,
|
||||
requestSrLookupLanguage,
|
||||
requestSrLookupTechPlatform,
|
||||
requestLookupTypePlatform,
|
||||
requestInternalSrServices,
|
||||
requestSrLookupOpportunityBusiness,
|
||||
requestCurrentUser,
|
||||
requestConsumerTokenService,
|
||||
@@ -2246,6 +2313,11 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (!message || !message.type) {
|
||||
return false;
|
||||
}
|
||||
if (message.type === messageTypes.requestPostInternalServiceRequest) {
|
||||
requestPostInternalServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||
return true;
|
||||
}
|
||||
if (message.type === messageTypes.requestPostServiceRequest) {
|
||||
requestPostServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||
@@ -2257,6 +2329,8 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
return true;
|
||||
}
|
||||
const srLookups = {
|
||||
[messageTypes.requestLookupTypePlatform]: requestLookupTypePlatform,
|
||||
[messageTypes.requestInternalSrServices]: requestInternalSrServices,
|
||||
[messageTypes.requestSrLookupPrimaryPillar]: requestSrLookupPrimaryPillar,
|
||||
[messageTypes.requestSrLookupLanguage]: requestSrLookupLanguage,
|
||||
[messageTypes.requestSrLookupTechPlatform]: requestSrLookupTechPlatform,
|
||||
@@ -2491,7 +2565,6 @@ function createBackgroundApplication(overrides = {}) {
|
||||
const httpBackgroundJson = createHttpBackgroundJson({
|
||||
observeResponseHeaders: (...args) => srRequestHeaders.observeResponseHeaders(...args),
|
||||
previewDebugBody: createDebugBodyPreview,
|
||||
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
||||
environment
|
||||
});
|
||||
const productsBackgroundJobs = createProductsBackgroundJobs({
|
||||
@@ -2563,6 +2636,7 @@ function createBackgroundApplication(overrides = {}) {
|
||||
const fusionSrCreate = createFusionSrCreate({
|
||||
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args),
|
||||
tokenHeaders: (...args) => srRequestHeaders.tokenHeaders(...args),
|
||||
internalServiceRequestHeaders: url => srRequestHeaders.internalServiceRequestHeaders(url),
|
||||
previewTokenHeaders: url => srRequestHeaders.previewTokenHeaders(url)
|
||||
});
|
||||
const fusionSrDetails = createFusionSrDetails({
|
||||
@@ -2576,7 +2650,11 @@ function createBackgroundApplication(overrides = {}) {
|
||||
});
|
||||
function start() {
|
||||
srRequestHeaders.start();
|
||||
startBackgroundApplication({ requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
|
||||
startBackgroundApplication({
|
||||
requestLookupTypePlatform: () => fusionSrDetails.requestLookupTypePlatform(),
|
||||
requestPostInternalServiceRequest: message => fusionSrCreate.requestPostInternalServiceRequest(message),
|
||||
requestInternalSrServices: message => fusionSrDetails.requestInternalSrServices(message),
|
||||
requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
|
||||
}
|
||||
return {
|
||||
start, contracts,
|
||||
|
||||
333
dist/chromium/content.js
vendored
333
dist/chromium/content.js
vendored
File diff suppressed because one or more lines are too long
246
dist/firefox/background.js
vendored
246
dist/firefox/background.js
vendored
@@ -20,9 +20,12 @@ function createMessageTypes() {
|
||||
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
||||
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
|
||||
requestPostServiceRequest: "opportunitiesExtension.requestPostServiceRequest",
|
||||
requestPostInternalServiceRequest: "opportunitiesExtension.requestPostInternalServiceRequest",
|
||||
prepareSrSession: "opportunitiesExtension.prepareSrSession",
|
||||
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
|
||||
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
|
||||
requestLookupTypePlatform: "opportunitiesExtension.requestLookupTypePlatform",
|
||||
requestInternalSrServices: "opportunitiesExtension.requestInternalSrServices",
|
||||
requestSrLookupOpportunityBusiness: "opportunitiesExtension.requestSrLookupOpportunityBusiness",
|
||||
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
|
||||
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",
|
||||
@@ -33,45 +36,8 @@ function createMessageTypes() {
|
||||
}
|
||||
|
||||
// Source: src/shared/debug-body.js
|
||||
function createDebugBodyPreview(text, limit) {
|
||||
if (text.length <= limit) return { value: text || "(empty response body)", truncated: false };
|
||||
try {
|
||||
const normalized = text.replace(/^\uFEFF/, "").trimStart()
|
||||
.replace(/^\)\]\}',?\s*/, "").replace(/^while\s*\(1\);\s*/, "");
|
||||
let value = JSON.parse(normalized);
|
||||
for (let depth = 0; depth < 2 && typeof value === "string" && /^\s*[\[{]/.test(value); depth += 1) {
|
||||
try { value = JSON.parse(value); } catch { break; }
|
||||
}
|
||||
// Bound retained data without cutting JSON syntax or losing sensitive field names.
|
||||
let remaining = Math.max(0, limit - 256);
|
||||
const visit = (node, depth = 0) => {
|
||||
if (remaining <= 64 || depth >= 30) return "[omitted from debug preview]";
|
||||
if (node === null || typeof node !== "object") {
|
||||
const serialized = JSON.stringify(node);
|
||||
if (serialized.length > remaining) {
|
||||
remaining = 0;
|
||||
return "[omitted from debug preview]";
|
||||
}
|
||||
remaining -= serialized.length + 1;
|
||||
return node;
|
||||
}
|
||||
const result = Array.isArray(node) ? [] : Object.create(null);
|
||||
remaining -= 2;
|
||||
for (const key of Object.keys(node)) {
|
||||
const cost = Array.isArray(node) ? 1 : JSON.stringify(key).length + 2;
|
||||
if (remaining < cost + 64) break;
|
||||
remaining -= cost;
|
||||
result[key] = visit(node[key], depth + 1);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return {
|
||||
value: JSON.stringify({ debugNotice: "Partial response preview: remaining fields/items omitted due to the debug size limit.", preview: visit(value) }),
|
||||
truncated: true
|
||||
};
|
||||
} catch {
|
||||
return { value: `${text.slice(0, limit)}\n... truncated ${text.length - limit} characters`, truncated: true };
|
||||
}
|
||||
function createDebugBodyPreview(text) {
|
||||
return { value: text || "(empty response body)", truncated: false };
|
||||
}
|
||||
|
||||
// Source: src/background/contracts.js
|
||||
@@ -435,6 +401,7 @@ function createAuthSpaCredentials({
|
||||
function createSrRequestHeaders({ runtimeApi }) {
|
||||
const origin = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||
const appPath = "/ic/builder/rt/SeaaS_initiation/";
|
||||
const internalPath = "/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPICloudAccount/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests";
|
||||
const storageKey = "opportunitiesExtension.srAppBuilderClientId.v1";
|
||||
const maxAge = 60 * 60 * 1000;
|
||||
let observed = null;
|
||||
@@ -448,7 +415,7 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
function observeHeaders(rawUrl, headers) {
|
||||
let url;
|
||||
try { url = new URL(rawUrl); } catch { return; }
|
||||
if (url.origin !== origin || !/^\/ic\/builder\/rt\/SeaaS_initiation\/live(?:\/|;)/.test(url.pathname)) return;
|
||||
if (url.origin !== origin || !/^\/ic\/builder\/rt\/(?:SeaaS_initiation|SeaaS_SR_management)\/live(?:\/|;)/.test(url.pathname)) return;
|
||||
// Read only the client ID, not cookies, bodies or authorization.
|
||||
const value = typeof headers?.get === "function" ? headers.get("x-appbuilder-client-id")
|
||||
: headers?.find(item => item.name.toLowerCase() === "x-appbuilder-client-id")?.value;
|
||||
@@ -461,7 +428,7 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
function start() {
|
||||
if (started || !runtimeApi?.webRequest) return;
|
||||
started = true;
|
||||
const filter = { urls: [`${origin}${appPath}*`], types: ["xmlhttprequest"] };
|
||||
const filter = { urls: [`${origin}${appPath}*`, `${origin}/ic/builder/rt/SeaaS_SR_management/*`], types: ["xmlhttprequest"] };
|
||||
runtimeApi.webRequest.onBeforeSendHeaders?.addListener(details => {
|
||||
if (details.tabId >= 0 && !details.incognito) observeHeaders(details.url, details.requestHeaders);
|
||||
}, filter, ["requestHeaders"]);
|
||||
@@ -474,6 +441,9 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
if (target.origin !== origin || !target.pathname.startsWith(appPath) || !/\/Catalogue_c(?:\/\d+)?$/.test(target.pathname)) {
|
||||
throw new Error("Unexpected Service Request token URL.");
|
||||
}
|
||||
return sessionHeaders(target.origin, "application/vnd.oracle.adf.action+json");
|
||||
}
|
||||
async function sessionHeaders(targetOrigin, contentType) {
|
||||
if (!valid(observed)) {
|
||||
try {
|
||||
const stored = await runtimeApi.storage?.session?.get(storageKey);
|
||||
@@ -481,8 +451,8 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
} catch { /* A missing capture must not prevent trying the other required headers. */ }
|
||||
}
|
||||
return {
|
||||
Accept: "application/json", "Content-Type": "application/vnd.oracle.adf.action+json",
|
||||
Authorization: "Session", Origin: target.origin,
|
||||
Accept: "application/json", "Content-Type": contentType,
|
||||
Authorization: "Session", Origin: targetOrigin,
|
||||
...(valid(observed) ? { "x-appbuilder-client-id": observed.value } : {})
|
||||
};
|
||||
}
|
||||
@@ -490,8 +460,8 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
const headers = await previewTokenHeaders(url);
|
||||
if (!/\/Catalogue_c\/\d+$/.test(new URL(url).pathname)) throw new Error("Missing Catalogue Id.");
|
||||
const serviceTarget = new URL(serviceRequestUrl);
|
||||
if (serviceTarget.origin !== headers.Origin || !serviceTarget.pathname.startsWith(appPath)
|
||||
|| !serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests") || serviceTarget.search || serviceTarget.hash) {
|
||||
const isServiceRequest = serviceTarget.pathname.startsWith(appPath) && serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests");
|
||||
if (serviceTarget.origin !== headers.Origin || !isServiceRequest || serviceTarget.search || serviceTarget.hash) {
|
||||
throw new Error("Unexpected Service Request URL.");
|
||||
}
|
||||
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
|
||||
@@ -501,10 +471,11 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
const catalogUrl = url.slice(0, url.lastIndexOf("/") + 1);
|
||||
const escapedCatalogUrl = catalogUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const escapedServiceUrl = serviceTarget.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const ruleIds = [7201, 7202];
|
||||
await runtimeApi.declarativeNetRequest.updateSessionRules({
|
||||
removeRuleIds: [7201, 7202],
|
||||
removeRuleIds: ruleIds,
|
||||
addRules: [`^${escapedCatalogUrl}[0-9]+$`, `^${escapedServiceUrl}$`].map((regexFilter, index) => ({
|
||||
id: 7201 + index, priority: 1,
|
||||
id: ruleIds[index], priority: 1,
|
||||
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
|
||||
condition: { regexFilter, initiatorDomains: [extensionDomain],
|
||||
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
|
||||
@@ -512,7 +483,29 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
return { start, tokenHeaders, previewTokenHeaders, observeResponseHeaders: observeHeaders };
|
||||
async function internalServiceRequestHeaders(url) {
|
||||
const target = new URL(url);
|
||||
if (target.origin !== origin || target.pathname !== internalPath || target.search || target.hash) {
|
||||
throw new Error("Unexpected Internal Service Request URL.");
|
||||
}
|
||||
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
|
||||
throw new Error("Reload the extension to enable the Internal Service Request header permissions.");
|
||||
}
|
||||
const headers = await sessionHeaders(target.origin, "application/json");
|
||||
const extensionDomain = new URL(runtimeApi.runtime.getURL("/")).hostname;
|
||||
const escapedUrl = target.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
await runtimeApi.declarativeNetRequest.updateSessionRules({
|
||||
removeRuleIds: [7203],
|
||||
addRules: [{
|
||||
id: 7203, priority: 1,
|
||||
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
|
||||
condition: { regexFilter: `^${escapedUrl}$`, initiatorDomains: [extensionDomain],
|
||||
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
|
||||
}]
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
return { start, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders, observeResponseHeaders: observeHeaders };
|
||||
}
|
||||
|
||||
// Source: src/services/spa/accounts.js
|
||||
@@ -1029,7 +1022,33 @@ function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFire
|
||||
return { ok: true, items: result.items.map(({ Id, RecordName, Abstract_c }) => ({ Id: String(Id), RecordName, Abstract_c: Abstract_c == null ? "" : String(Abstract_c) })), debugRequests };
|
||||
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||
}
|
||||
async function requestInternalSrOptions(message, services = false) {
|
||||
const debugRequests = [];
|
||||
const name = services ? "requestInternalSrServices" : "requestLookupTypePlatform";
|
||||
try {
|
||||
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetSeaaSOKECustomRestAPI/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/";
|
||||
let path = "lookups?type=STANDARD&lookupType=SENGG_TECH_PLATFORM";
|
||||
if (services) {
|
||||
const id = String(message.parentServiceId ?? "");
|
||||
if (!/^\d+$/.test(id)) throw new Error("A numeric parent SrId is required.");
|
||||
if (typeof message.emailAddress !== "string" || !message.emailAddress.trim()) throw new Error("Current user email is unavailable.");
|
||||
path = `services?emailAddress=${encodeURIComponent(message.emailAddress.trim())}&parentServiceId=${encodeURIComponent(id)}&serviceType=INTERNAL`;
|
||||
}
|
||||
const result = await fetchJson(base + path, { method: "GET", credentials: "include",
|
||||
headers: { Accept: "application/json", Authorization: "Session" }
|
||||
}, name, debugRequests);
|
||||
if (!Array.isArray(result?.items) || result.items.some(item => services
|
||||
? !/^\d+$/.test(String(item?.Id ?? "")) || typeof item?.RecordName !== "string"
|
||||
: typeof item?.code !== "string" || typeof item?.name !== "string")) throw new Error(`Invalid ${name} response.`);
|
||||
const items = services
|
||||
? result.items.map(item => ({ Id: String(item.Id), RecordName: item.RecordName, Description_c: String(item.Description_c ?? "") }))
|
||||
: result.items.filter(item => item.enabledFlag !== "N").map(({ code, name }) => ({ code, name }));
|
||||
return { ok: true, items, debugRequests };
|
||||
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||
}
|
||||
return { requestLookupWithSession, requestInternalServiceRequest, requestSrLookupOpportunityBusiness,
|
||||
requestLookupTypePlatform: () => requestInternalSrOptions({}),
|
||||
requestInternalSrServices: message => requestInternalSrOptions(message, true),
|
||||
prepareSession: sender => requestLookupWithSession(null, {}, sender, true),
|
||||
requestSrLookupPrimaryPillar: () => requestLookup("SENGG_PRIMARY_PILLAR", "requestSrLookupPrimaryPillar"),
|
||||
requestSrLookupLanguage: () => requestLookup("SENGG_LANGUAGE", "requestSrLookupLanguage"),
|
||||
@@ -1037,7 +1056,7 @@ function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFire
|
||||
}
|
||||
|
||||
// Source: src/services/fusion/sr-create.js
|
||||
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders }) {
|
||||
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders }) {
|
||||
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/";
|
||||
const catalog = base + "crmRestApiResources/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/11.13.18.05/Catalogue_c";
|
||||
const resourceUsers = base + "oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/resourceUsers";
|
||||
@@ -1050,26 +1069,76 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
|
||||
if (typeof value !== "string" || !value.trim()) throw new Error(`Missing ${name}.`);
|
||||
return value.trim();
|
||||
}
|
||||
function dateOnly(value, name) {
|
||||
const date = text(value, name);
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|
||||
|| new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) throw new Error(`Invalid ${name}.`);
|
||||
return date;
|
||||
}
|
||||
function payloadFor(input) {
|
||||
const payload = {};
|
||||
for (const name of ["AccountPartyId", "OpportunityName_Id_c", "ServiceNew_Id_c"]) payload[name] = numericId(input?.[name], name);
|
||||
for (const name of ["Title", "RegionText_c", "ProblemDescription", "Language_c", "CustomerDeadline_c", "TechPlatformLookUp_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(input?.[name], name);
|
||||
const date = payload.CustomerDeadline_c;
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|
||||
|| new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) throw new Error("Invalid Customer deadline.");
|
||||
dateOnly(payload.CustomerDeadline_c, "Customer deadline");
|
||||
return { ...payload, SvcQuestionsCollection_c: [], SourceCd: "SVC_OPPORTUNITY_SCREEN", SeverityCd: "ORA_SVC_SEV2", ChannelTypeCd: "ORA_SVC_WEB" };
|
||||
}
|
||||
async function requestResourceUser(email, debugRequests) {
|
||||
const emailAddress = text(email, "current user email");
|
||||
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
|
||||
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||
}, "requestResourceUser", debugRequests);
|
||||
return numericId(resource?.resourcePartyId, "resourcePartyId");
|
||||
}
|
||||
async function requestPostInternalServiceRequest(message) {
|
||||
const debugRequests = [];
|
||||
try {
|
||||
const source = message.payload;
|
||||
const parentSrId = numericId(source?.ServiceRequest_Id_SRToInternalSR, "parent SrId");
|
||||
const payload = {
|
||||
Title: text(source?.Title, "Title"),
|
||||
ServiceNew_Id_c: numericId(source?.ServiceNew_Id_c, "ServiceNew_Id_c"),
|
||||
ProblemDescription: text(source?.ProblemDescription, "Description"),
|
||||
CustomerDeadline_c: dateOnly(source?.CustomerDeadline_c, "Customer deadline"),
|
||||
TechPlatformLookUp_c: text(source?.TechPlatformLookUp_c, "Platform"),
|
||||
InternalDeadline_c: `${dateOnly(source?.InternalDeadline_c, "Internal Deadline")}T00:00:00.000Z`,
|
||||
SvcQuestionsCollection_c: []
|
||||
};
|
||||
const parent = await requestSRInfo(parentSrId, debugRequests);
|
||||
payload.ServiceRequest_Id_SRToInternalSR = String(parentSrId);
|
||||
for (const name of ["AccountPartyId", "OpportunityName_Id_c"]) payload[name] = numericId(parent[name], name);
|
||||
for (const name of ["OpportunityName_c", "RegionText_c", "SvcHealth_c", "Language_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(parent[name], name);
|
||||
payload.SecondaryPillarNew_c = parent.SecondaryPillarNew_c == null ? null : text(parent.SecondaryPillarNew_c, "SecondaryPillarNew_c");
|
||||
payload.ReportedByPartyId = await requestResourceUser(message.emailAddress, debugRequests);
|
||||
const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPICloudAccount/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests";
|
||||
const result = await fetchJson(url, { method: "POST", credentials: "include",
|
||||
headers: await internalServiceRequestHeaders(url),
|
||||
body: JSON.stringify(payload)
|
||||
}, "requestPostInternalServiceRequest", debugRequests, { allowEmpty: true });
|
||||
return { ok: true, result, debugRequests };
|
||||
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||
}
|
||||
async function requestSRInfo(parentSrId, debugRequests) {
|
||||
const url = new URL("https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPI/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests");
|
||||
url.searchParams.set("fields", "PrimaryPillarNew_c,SecondaryPillarNew_c,SvcHealth_c,SrNumber,AccountPartyId,CountryText_c,Language_c,OpportunityName_Id_c,OpportunityName_c,RegionText_c,ServiceNew_Id_c");
|
||||
url.searchParams.set("onlyData", "true");
|
||||
url.searchParams.set("orderBy", "LastUpdateDate:desc");
|
||||
url.searchParams.set("q", `SrId=${parentSrId}`);
|
||||
const result = await fetchJson(url.href, {
|
||||
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||
}, "requestSRInfo", debugRequests);
|
||||
if (!Array.isArray(result?.items) || result.items.length !== 1 || !result.items[0]
|
||||
|| (result.items[0].SrId != null && String(result.items[0].SrId) !== String(parentSrId))) {
|
||||
throw new Error("requestSRInfo did not return the selected parent Service Request.");
|
||||
}
|
||||
return result.items[0];
|
||||
}
|
||||
async function requestPostServiceRequest(message) {
|
||||
const debugRequests = [];
|
||||
try {
|
||||
if (message.phase !== "prepare" && message.phase !== "confirm") throw new Error("Service Request confirmation is required.");
|
||||
const payload = payloadFor(message.payload);
|
||||
if (message.phase === "prepare") {
|
||||
const emailAddress = text(message.emailAddress, "current user email");
|
||||
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
|
||||
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||
}, "requestResourceUser", debugRequests);
|
||||
payload.ReportedByPartyId = numericId(resource?.resourcePartyId, "resourcePartyId");
|
||||
payload.ReportedByPartyId = await requestResourceUser(message.emailAddress, debugRequests);
|
||||
const sharedHeaders = await previewTokenHeaders(catalog);
|
||||
return { ok: true, payload, debugRequests, headers: {
|
||||
requestSrTokenCatalog: sharedHeaders,
|
||||
@@ -1077,7 +1146,20 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
|
||||
} };
|
||||
}
|
||||
payload.ReportedByPartyId = numericId(message.payload?.ReportedByPartyId, "ReportedByPartyId");
|
||||
let id = message.catalogId;
|
||||
const headers = await requestCreationHeaders(message.catalogId, endpoint, debugRequests);
|
||||
const result = await fetchJson(endpoint, {
|
||||
method: "POST", credentials: "include", headers,
|
||||
body: JSON.stringify(payload)
|
||||
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
|
||||
return { ok: true, result, debugRequests };
|
||||
} catch (error) {
|
||||
error.debugRequests = debugRequests;
|
||||
throw error;
|
||||
} finally {
|
||||
annotateSigningRequest(debugRequests);
|
||||
}
|
||||
}
|
||||
async function requestCreationHeaders(id, target, debugRequests) {
|
||||
if (id == null) {
|
||||
const result = await fetchJson(`${catalog}?onlyData=true&fields=Id&limit=1`, {
|
||||
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||
@@ -1086,7 +1168,7 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
|
||||
}
|
||||
id = numericId(id, "Catalogue Id");
|
||||
const tokenUrl = `${catalog}/${id}`;
|
||||
let catalogHeaders = await tokenHeaders(tokenUrl, endpoint);
|
||||
let catalogHeaders = await tokenHeaders(tokenUrl, target);
|
||||
const requestToken = () => fetchJson(tokenUrl, {
|
||||
method: "POST", credentials: "include", headers: catalogHeaders, body: JSON.stringify({ name: "getJwtToken" })
|
||||
}, "requestSrTokenCatalog", debugRequests);
|
||||
@@ -1103,27 +1185,16 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
|
||||
if (debugRequests.at(-1)?.httpStatus !== 200 || typeof token?.result !== "string" || !token.result.trim()) {
|
||||
throw new Error("requestSrTokenCatalog did not return HTTP 200 with a token.");
|
||||
}
|
||||
const result = await fetchJson(endpoint, {
|
||||
method: "POST", credentials: "include", headers: { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() },
|
||||
body: JSON.stringify(payload)
|
||||
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
|
||||
return { ok: true, result, debugRequests };
|
||||
} catch (error) {
|
||||
error.debugRequests = debugRequests;
|
||||
throw error;
|
||||
} finally {
|
||||
// Never forward the signing token to content scripts, even with Debug secrets enabled.
|
||||
return { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() };
|
||||
}
|
||||
function annotateSigningRequest(debugRequests) {
|
||||
for (const entry of debugRequests) {
|
||||
if (entry.name === "requestSrTokenCatalog") {
|
||||
entry.metadata.appBuilderClientId = entry.requestHeaders?.["x-appbuilder-client-id"] ? "Observed in SeaaS requests" : "Not observed in SeaaS requests";
|
||||
entry.responseBody = JSON.stringify({ result: "(redacted)" });
|
||||
entry.responseBodyTruncated = false;
|
||||
}
|
||||
if (entry.requestHeaders?.["vb-proxy-header-x-oracle-ectoken"]) entry.requestHeaders["vb-proxy-header-x-oracle-ectoken"] = "(redacted)";
|
||||
}
|
||||
}
|
||||
}
|
||||
return { requestPostServiceRequest };
|
||||
return { requestPostServiceRequest, requestPostInternalServiceRequest };
|
||||
}
|
||||
|
||||
// Source: src/services/spa/current-user.js
|
||||
@@ -1144,12 +1215,6 @@ function createSpaCurrentUser({ fetchJson }) {
|
||||
for (const entry of debugRequests) { entry.status = "failed"; entry.error = error.message; }
|
||||
error.debugRequests = debugRequests;
|
||||
throw error;
|
||||
} finally {
|
||||
// The identity token is not needed by content scripts or debug, including on failures.
|
||||
for (const entry of debugRequests) {
|
||||
entry.responseBody = JSON.stringify(user ? { user, token: "(redacted)" } : { error: "Identity response omitted." }, null, 2);
|
||||
entry.responseBodyTruncated = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { requestCurrentUser };
|
||||
@@ -1330,7 +1395,6 @@ function createSpaResponseShapes() {
|
||||
// Source: src/services/http/background-json.js
|
||||
function createHttpBackgroundJson({
|
||||
previewDebugBody,
|
||||
DEBUG_BODY_LIMIT,
|
||||
observeResponseHeaders = () => {},
|
||||
environment
|
||||
}) {
|
||||
@@ -1457,7 +1521,7 @@ function createHttpBackgroundJson({
|
||||
}
|
||||
|
||||
function createDebugBody(text) {
|
||||
return previewDebugBody(text, DEBUG_BODY_LIMIT);
|
||||
return previewDebugBody(text);
|
||||
}
|
||||
|
||||
function normalizeDebugHeaders(headers) {
|
||||
@@ -2227,12 +2291,15 @@ function registerBackgroundMessages({
|
||||
requestActionFindLookup,
|
||||
requestMgmtAction,
|
||||
requestPostServiceRequest,
|
||||
requestPostInternalServiceRequest,
|
||||
requestInternalServiceRequest,
|
||||
requestSrLookupPrimaryPillar,
|
||||
requestSrLookupWithSession,
|
||||
prepareSrSession,
|
||||
requestSrLookupLanguage,
|
||||
requestSrLookupTechPlatform,
|
||||
requestLookupTypePlatform,
|
||||
requestInternalSrServices,
|
||||
requestSrLookupOpportunityBusiness,
|
||||
requestCurrentUser,
|
||||
requestConsumerTokenService,
|
||||
@@ -2246,6 +2313,11 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (!message || !message.type) {
|
||||
return false;
|
||||
}
|
||||
if (message.type === messageTypes.requestPostInternalServiceRequest) {
|
||||
requestPostInternalServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||
return true;
|
||||
}
|
||||
if (message.type === messageTypes.requestPostServiceRequest) {
|
||||
requestPostServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||
@@ -2257,6 +2329,8 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
return true;
|
||||
}
|
||||
const srLookups = {
|
||||
[messageTypes.requestLookupTypePlatform]: requestLookupTypePlatform,
|
||||
[messageTypes.requestInternalSrServices]: requestInternalSrServices,
|
||||
[messageTypes.requestSrLookupPrimaryPillar]: requestSrLookupPrimaryPillar,
|
||||
[messageTypes.requestSrLookupLanguage]: requestSrLookupLanguage,
|
||||
[messageTypes.requestSrLookupTechPlatform]: requestSrLookupTechPlatform,
|
||||
@@ -2491,7 +2565,6 @@ function createBackgroundApplication(overrides = {}) {
|
||||
const httpBackgroundJson = createHttpBackgroundJson({
|
||||
observeResponseHeaders: (...args) => srRequestHeaders.observeResponseHeaders(...args),
|
||||
previewDebugBody: createDebugBodyPreview,
|
||||
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
||||
environment
|
||||
});
|
||||
const productsBackgroundJobs = createProductsBackgroundJobs({
|
||||
@@ -2563,6 +2636,7 @@ function createBackgroundApplication(overrides = {}) {
|
||||
const fusionSrCreate = createFusionSrCreate({
|
||||
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args),
|
||||
tokenHeaders: (...args) => srRequestHeaders.tokenHeaders(...args),
|
||||
internalServiceRequestHeaders: url => srRequestHeaders.internalServiceRequestHeaders(url),
|
||||
previewTokenHeaders: url => srRequestHeaders.previewTokenHeaders(url)
|
||||
});
|
||||
const fusionSrDetails = createFusionSrDetails({
|
||||
@@ -2576,7 +2650,11 @@ function createBackgroundApplication(overrides = {}) {
|
||||
});
|
||||
function start() {
|
||||
srRequestHeaders.start();
|
||||
startBackgroundApplication({ requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
|
||||
startBackgroundApplication({
|
||||
requestLookupTypePlatform: () => fusionSrDetails.requestLookupTypePlatform(),
|
||||
requestPostInternalServiceRequest: message => fusionSrCreate.requestPostInternalServiceRequest(message),
|
||||
requestInternalSrServices: message => fusionSrDetails.requestInternalSrServices(message),
|
||||
requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
|
||||
}
|
||||
return {
|
||||
start, contracts,
|
||||
|
||||
333
dist/firefox/content.js
vendored
333
dist/firefox/content.js
vendored
File diff suppressed because one or more lines are too long
@@ -40,6 +40,7 @@ module.exports = {
|
||||
"src/features/opportunities/activity-filters.js",
|
||||
"src/features/opportunities/crm-sr-popover.js",
|
||||
"src/features/opportunities/crm-sr-form.js",
|
||||
"src/features/opportunities/internal-sr-form.js",
|
||||
"src/features/opportunities/searchable-select.js",
|
||||
"src/features/opportunities/internal-sr-session.js",
|
||||
"src/features/opportunities/sr-session-preparation.js",
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
function createSrRequestHeaders({ runtimeApi }) {
|
||||
const origin = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||
const appPath = "/ic/builder/rt/SeaaS_initiation/";
|
||||
const internalPath = "/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPICloudAccount/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests";
|
||||
const storageKey = "opportunitiesExtension.srAppBuilderClientId.v1";
|
||||
const maxAge = 60 * 60 * 1000;
|
||||
let observed = null;
|
||||
@@ -14,7 +15,7 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
function observeHeaders(rawUrl, headers) {
|
||||
let url;
|
||||
try { url = new URL(rawUrl); } catch { return; }
|
||||
if (url.origin !== origin || !/^\/ic\/builder\/rt\/SeaaS_initiation\/live(?:\/|;)/.test(url.pathname)) return;
|
||||
if (url.origin !== origin || !/^\/ic\/builder\/rt\/(?:SeaaS_initiation|SeaaS_SR_management)\/live(?:\/|;)/.test(url.pathname)) return;
|
||||
// Read only the client ID, not cookies, bodies or authorization.
|
||||
const value = typeof headers?.get === "function" ? headers.get("x-appbuilder-client-id")
|
||||
: headers?.find(item => item.name.toLowerCase() === "x-appbuilder-client-id")?.value;
|
||||
@@ -27,7 +28,7 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
function start() {
|
||||
if (started || !runtimeApi?.webRequest) return;
|
||||
started = true;
|
||||
const filter = { urls: [`${origin}${appPath}*`], types: ["xmlhttprequest"] };
|
||||
const filter = { urls: [`${origin}${appPath}*`, `${origin}/ic/builder/rt/SeaaS_SR_management/*`], types: ["xmlhttprequest"] };
|
||||
runtimeApi.webRequest.onBeforeSendHeaders?.addListener(details => {
|
||||
if (details.tabId >= 0 && !details.incognito) observeHeaders(details.url, details.requestHeaders);
|
||||
}, filter, ["requestHeaders"]);
|
||||
@@ -40,6 +41,9 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
if (target.origin !== origin || !target.pathname.startsWith(appPath) || !/\/Catalogue_c(?:\/\d+)?$/.test(target.pathname)) {
|
||||
throw new Error("Unexpected Service Request token URL.");
|
||||
}
|
||||
return sessionHeaders(target.origin, "application/vnd.oracle.adf.action+json");
|
||||
}
|
||||
async function sessionHeaders(targetOrigin, contentType) {
|
||||
if (!valid(observed)) {
|
||||
try {
|
||||
const stored = await runtimeApi.storage?.session?.get(storageKey);
|
||||
@@ -47,8 +51,8 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
} catch { /* A missing capture must not prevent trying the other required headers. */ }
|
||||
}
|
||||
return {
|
||||
Accept: "application/json", "Content-Type": "application/vnd.oracle.adf.action+json",
|
||||
Authorization: "Session", Origin: target.origin,
|
||||
Accept: "application/json", "Content-Type": contentType,
|
||||
Authorization: "Session", Origin: targetOrigin,
|
||||
...(valid(observed) ? { "x-appbuilder-client-id": observed.value } : {})
|
||||
};
|
||||
}
|
||||
@@ -56,8 +60,8 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
const headers = await previewTokenHeaders(url);
|
||||
if (!/\/Catalogue_c\/\d+$/.test(new URL(url).pathname)) throw new Error("Missing Catalogue Id.");
|
||||
const serviceTarget = new URL(serviceRequestUrl);
|
||||
if (serviceTarget.origin !== headers.Origin || !serviceTarget.pathname.startsWith(appPath)
|
||||
|| !serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests") || serviceTarget.search || serviceTarget.hash) {
|
||||
const isServiceRequest = serviceTarget.pathname.startsWith(appPath) && serviceTarget.pathname.endsWith("/set/seaas/mgmt/serviceRequests");
|
||||
if (serviceTarget.origin !== headers.Origin || !isServiceRequest || serviceTarget.search || serviceTarget.hash) {
|
||||
throw new Error("Unexpected Service Request URL.");
|
||||
}
|
||||
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
|
||||
@@ -67,10 +71,11 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
const catalogUrl = url.slice(0, url.lastIndexOf("/") + 1);
|
||||
const escapedCatalogUrl = catalogUrl.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const escapedServiceUrl = serviceTarget.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
const ruleIds = [7201, 7202];
|
||||
await runtimeApi.declarativeNetRequest.updateSessionRules({
|
||||
removeRuleIds: [7201, 7202],
|
||||
removeRuleIds: ruleIds,
|
||||
addRules: [`^${escapedCatalogUrl}[0-9]+$`, `^${escapedServiceUrl}$`].map((regexFilter, index) => ({
|
||||
id: 7201 + index, priority: 1,
|
||||
id: ruleIds[index], priority: 1,
|
||||
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
|
||||
condition: { regexFilter, initiatorDomains: [extensionDomain],
|
||||
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
|
||||
@@ -78,5 +83,27 @@ function createSrRequestHeaders({ runtimeApi }) {
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
return { start, tokenHeaders, previewTokenHeaders, observeResponseHeaders: observeHeaders };
|
||||
async function internalServiceRequestHeaders(url) {
|
||||
const target = new URL(url);
|
||||
if (target.origin !== origin || target.pathname !== internalPath || target.search || target.hash) {
|
||||
throw new Error("Unexpected Internal Service Request URL.");
|
||||
}
|
||||
if (!runtimeApi?.declarativeNetRequest?.updateSessionRules || !runtimeApi.runtime?.getURL) {
|
||||
throw new Error("Reload the extension to enable the Internal Service Request header permissions.");
|
||||
}
|
||||
const headers = await sessionHeaders(target.origin, "application/json");
|
||||
const extensionDomain = new URL(runtimeApi.runtime.getURL("/")).hostname;
|
||||
const escapedUrl = target.href.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
||||
await runtimeApi.declarativeNetRequest.updateSessionRules({
|
||||
removeRuleIds: [7203],
|
||||
addRules: [{
|
||||
id: 7203, priority: 1,
|
||||
action: { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: headers.Origin }] },
|
||||
condition: { regexFilter: `^${escapedUrl}$`, initiatorDomains: [extensionDomain],
|
||||
requestMethods: ["post"], resourceTypes: ["xmlhttprequest"] }
|
||||
}]
|
||||
});
|
||||
return headers;
|
||||
}
|
||||
return { start, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders, observeResponseHeaders: observeHeaders };
|
||||
}
|
||||
|
||||
@@ -92,10 +92,7 @@ function createAuthTokenRelay({
|
||||
throw new Error("Token relay response does not contain access_token.");
|
||||
}
|
||||
|
||||
logRequestSuccess(requestEntry, response, createDebugBody(JSON.stringify({
|
||||
...responseData,
|
||||
access_token: maskToken(responseData.access_token)
|
||||
}, null, 2)));
|
||||
logRequestSuccess(requestEntry, response, responseBody);
|
||||
|
||||
if (url === TOKEN_RELAY_URL) {
|
||||
authSessionState.principal = typeof responseData.principal === "string" ? responseData.principal.trim() : "";
|
||||
@@ -122,7 +119,7 @@ function createAuthTokenRelay({
|
||||
method: "GET",
|
||||
url,
|
||||
headers: {
|
||||
"x-xsrf-token": xsrfTokenSource.token ? maskToken(xsrfTokenSource.token) : "(missing)"
|
||||
"x-xsrf-token": xsrfTokenSource.token || "(missing)"
|
||||
},
|
||||
metadata: {
|
||||
xsrfCookieName: xsrfTokenSource.cookieName || "(missing)",
|
||||
|
||||
@@ -58,7 +58,6 @@ function createBackgroundApplication(overrides = {}) {
|
||||
const httpBackgroundJson = createHttpBackgroundJson({
|
||||
observeResponseHeaders: (...args) => srRequestHeaders.observeResponseHeaders(...args),
|
||||
previewDebugBody: createDebugBodyPreview,
|
||||
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
||||
environment
|
||||
});
|
||||
const productsBackgroundJobs = createProductsBackgroundJobs({
|
||||
@@ -130,6 +129,7 @@ function createBackgroundApplication(overrides = {}) {
|
||||
const fusionSrCreate = createFusionSrCreate({
|
||||
fetchJson: (...args) => httpBackgroundJson.fetchJson(...args),
|
||||
tokenHeaders: (...args) => srRequestHeaders.tokenHeaders(...args),
|
||||
internalServiceRequestHeaders: url => srRequestHeaders.internalServiceRequestHeaders(url),
|
||||
previewTokenHeaders: url => srRequestHeaders.previewTokenHeaders(url)
|
||||
});
|
||||
const fusionSrDetails = createFusionSrDetails({
|
||||
@@ -143,7 +143,11 @@ function createBackgroundApplication(overrides = {}) {
|
||||
});
|
||||
function start() {
|
||||
srRequestHeaders.start();
|
||||
startBackgroundApplication({ requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
|
||||
startBackgroundApplication({
|
||||
requestLookupTypePlatform: () => fusionSrDetails.requestLookupTypePlatform(),
|
||||
requestPostInternalServiceRequest: message => fusionSrCreate.requestPostInternalServiceRequest(message),
|
||||
requestInternalSrServices: message => fusionSrDetails.requestInternalSrServices(message),
|
||||
requestPostServiceRequest: message => fusionSrCreate.requestPostServiceRequest(message), prepareSrSession: sender => fusionSrDetails.prepareSession(sender), requestSrLookupWithSession: (...args) => fusionSrDetails.requestLookupWithSession(...args), messageTypes, requestSrLookupLanguage: () => fusionSrDetails.requestSrLookupLanguage(), requestSrLookupTechPlatform: () => fusionSrDetails.requestSrLookupTechPlatform(), requestSrLookupOpportunityBusiness: message => fusionSrDetails.requestSrLookupOpportunityBusiness(message), requestSrLookupPrimaryPillar: () => fusionSrDetails.requestSrLookupPrimaryPillar(), requestInternalServiceRequest: (message, sender) => fusionSrDetails.requestInternalServiceRequest(message, sender), requestMgmtAction: message => spaActionOptions.requestMgmtAction(message), invalidateProductsCache: () => productsCache.invalidate(), requestCurrentUser, getXsrfTokenCookie, refreshXsrfCookie, requestAccountPlans, requestInstallBase, requestCustomerServiceRequest, requestWlActions, requestActionTeamMembers, requestActionFindLookup, requestConsumerTokenService, requestConsumptionTrend, requestConsumptionPlanDetail, requestRatecardByPlan, requestOpptyProducts, runtimeApi });
|
||||
}
|
||||
return {
|
||||
start, contracts,
|
||||
|
||||
@@ -12,12 +12,15 @@ function registerBackgroundMessages({
|
||||
requestActionFindLookup,
|
||||
requestMgmtAction,
|
||||
requestPostServiceRequest,
|
||||
requestPostInternalServiceRequest,
|
||||
requestInternalServiceRequest,
|
||||
requestSrLookupPrimaryPillar,
|
||||
requestSrLookupWithSession,
|
||||
prepareSrSession,
|
||||
requestSrLookupLanguage,
|
||||
requestSrLookupTechPlatform,
|
||||
requestLookupTypePlatform,
|
||||
requestInternalSrServices,
|
||||
requestSrLookupOpportunityBusiness,
|
||||
requestCurrentUser,
|
||||
requestConsumerTokenService,
|
||||
@@ -31,6 +34,11 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (!message || !message.type) {
|
||||
return false;
|
||||
}
|
||||
if (message.type === messageTypes.requestPostInternalServiceRequest) {
|
||||
requestPostInternalServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||
return true;
|
||||
}
|
||||
if (message.type === messageTypes.requestPostServiceRequest) {
|
||||
requestPostServiceRequest(message).then(sendResponse).catch(error => sendResponse({ ok: false,
|
||||
error: error.message, debugRequests: error.debugRequests || [] }));
|
||||
@@ -42,6 +50,8 @@ runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
return true;
|
||||
}
|
||||
const srLookups = {
|
||||
[messageTypes.requestLookupTypePlatform]: requestLookupTypePlatform,
|
||||
[messageTypes.requestInternalSrServices]: requestInternalSrServices,
|
||||
[messageTypes.requestSrLookupPrimaryPillar]: requestSrLookupPrimaryPillar,
|
||||
[messageTypes.requestSrLookupLanguage]: requestSrLookupLanguage,
|
||||
[messageTypes.requestSrLookupTechPlatform]: requestSrLookupTechPlatform,
|
||||
|
||||
@@ -57,7 +57,7 @@ function createContentApplication(overrides = {}) {
|
||||
});
|
||||
const contentModal = createContentModal({
|
||||
prepareSrSession: () => srSessionPreparation.ensure(),
|
||||
closeActionForm: (...args) => opportunityCalendar.closeDetails(...args) || crmSrForm.close(...args) || workloadActionForm.close(...args),
|
||||
closeActionForm: (...args) => internalSrForm.close(...args) || opportunityCalendar.closeDetails(...args) || crmSrForm.close(...args) || workloadActionForm.close(...args),
|
||||
mountOpportunityViews: (...args) => opportunityCalendar.mount(...args),
|
||||
createActivityFilterControls: (...args) => activityFilters.createControls(...args),
|
||||
closeCrmSrPopover: () => crmSrPopover.close(),
|
||||
@@ -228,6 +228,8 @@ function createContentApplication(overrides = {}) {
|
||||
}
|
||||
});
|
||||
const crmSrPopover = createCrmSrPopover({
|
||||
openInternalSrForm: (parent, trigger) => internalSrForm.open(parent, trigger),
|
||||
formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value),
|
||||
requestDetails: srId => internalSrSession.request(srId),
|
||||
MODAL_ID: contracts.MODAL_ID,
|
||||
createCopyButton: (...args) => opportunitiesLinks.createCopyOpportunityButton(...args),
|
||||
@@ -240,6 +242,20 @@ function createContentApplication(overrides = {}) {
|
||||
if (!result?.ok) throw new Error(result?.error || "Unable to load Service Request options.");
|
||||
return result.items;
|
||||
}
|
||||
const internalSrForm = createInternalSrForm({
|
||||
notify: (...args) => opportunitiesLinks.showCopyConfirmation(...args),
|
||||
requestSave: async payload => {
|
||||
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestPostInternalServiceRequest, payload, emailAddress: currentUser.getUserId() });
|
||||
debugContentLog.appendBackgroundRequestLog(result?.debugRequests || []);
|
||||
if (!result?.ok) throw new Error(result?.error || "Unable to create Internal Service Request.");
|
||||
return result.result;
|
||||
},
|
||||
getOpportunity: number => number == null ? null : opportunitiesTableState.opportunitiesTableState.items.find(item => String(item.OptyNumber) === String(number)),
|
||||
MODAL_ID: contracts.MODAL_ID, environment, searchableSelect,
|
||||
formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value),
|
||||
requestPlatform: () => requestSrFormLookup(messageTypes.requestLookupTypePlatform),
|
||||
requestServices: parentServiceId => requestSrFormLookup(messageTypes.requestInternalSrServices, { parentServiceId, emailAddress: currentUser.getUserId() })
|
||||
});
|
||||
const crmSrForm = createCrmSrForm({
|
||||
requestSave: async fields => {
|
||||
const result = await platformContentMessages.sendRuntimeMessage({ type: messageTypes.requestPostServiceRequest, ...fields, emailAddress: currentUser.getUserId() });
|
||||
@@ -261,8 +277,9 @@ function createContentApplication(overrides = {}) {
|
||||
requestPillars: () => requestSrFormLookup(messageTypes.requestSrLookupPrimaryPillar)
|
||||
});
|
||||
const opportunityCrmServiceRequests = createOpportunityCrmServiceRequests({
|
||||
formatDate: value => opportunitiesFormatters.formatOracleResponseDate(value),
|
||||
createSrButton: number => crmSrForm.createButton(number),
|
||||
closeSrForm: () => crmSrForm.close(false),
|
||||
closeSrForm: () => { internalSrForm.close(false); crmSrForm.close(false); },
|
||||
setBadgeContent: (...args) => opportunityBadgeContent.setContent(...args),
|
||||
activityCache,
|
||||
reportProgress: (...args) => activityProgress.update("sr", ...args),
|
||||
@@ -639,7 +656,6 @@ function createContentApplication(overrides = {}) {
|
||||
});
|
||||
const debugContentLog = createDebugContentLog({
|
||||
previewDebugBody: createDebugBodyPreview,
|
||||
DEBUG_BODY_LIMIT: contracts.DEBUG_BODY_LIMIT,
|
||||
debugState,
|
||||
renderDebugPanel: (...args) => debugContentView.renderDebugPanel(...args)
|
||||
});
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
function createDebugContentLog({
|
||||
previewDebugBody,
|
||||
DEBUG_BODY_LIMIT,
|
||||
debugState,
|
||||
renderDebugPanel
|
||||
}) {
|
||||
@@ -99,7 +98,7 @@ function createDebugContentLog({
|
||||
}
|
||||
|
||||
function createDebugBody(text) {
|
||||
return previewDebugBody(text, DEBUG_BODY_LIMIT);
|
||||
return previewDebugBody(text);
|
||||
}
|
||||
|
||||
function headersToObject(headers) {
|
||||
@@ -126,7 +125,7 @@ function createDebugContentLog({
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
function formatDebugBody(body) {
|
||||
function formatDebugBody(body, requestName = "") {
|
||||
const text = body || "(none)";
|
||||
try {
|
||||
const normalized = String(text).replace(/^\uFEFF/, "").trimStart()
|
||||
@@ -137,8 +136,14 @@ function createDebugContentLog({
|
||||
for (let depth = 0; depth < 2 && typeof value === "string" && /^[\s]*[\[{]/.test(value); depth += 1) {
|
||||
try { value = JSON.parse(value); } catch { break; }
|
||||
}
|
||||
if (!debugState.debugSecretsVisible && requestName === "requestSrTokenCatalog") {
|
||||
value = value && typeof value === "object" && !Array.isArray(value)
|
||||
? { ...value, ...(Object.hasOwn(value, "result") ? { result: formatHiddenDebugValue(value.result) } : {}) }
|
||||
: formatHiddenDebugValue(value);
|
||||
}
|
||||
return JSON.stringify(debugState.debugSecretsVisible ? value : maskSensitiveDebugValue(value), null, 2);
|
||||
} catch (error) {
|
||||
if (!debugState.debugSecretsVisible && /^(requestSrTokenCatalog|requestCurrentUser|requestTokenRelay.*)$/.test(requestName)) return formatHiddenDebugValue(text);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
@@ -162,7 +167,7 @@ function createDebugContentLog({
|
||||
}
|
||||
|
||||
function isSensitiveDebugKey(key) {
|
||||
return /authorization|cookie|password|secret|(^|[_-])token($|[_-])|token$/i.test(key);
|
||||
return /authorization|cookie|password|secret|(^|[_-])token($|[_-])|token$|x-appbuilder-client-id/i.test(key);
|
||||
}
|
||||
|
||||
function formatHiddenDebugValue(value) {
|
||||
|
||||
@@ -26,6 +26,7 @@ function createDebugContentView({
|
||||
if (["requestWlActions", "requestActionTeamMembers", "requestActionFindLookup", "requestMgmtAction"].includes(entry.name)) return "actions";
|
||||
if (entry.name === "requestGenerateAiSummary") return "ai";
|
||||
if (entry.name === "requestInstallBase") return "installBase";
|
||||
if (["requestSRInfo", "requestLookupTypePlatform", "requestInternalSrServices", "requestPostInternalServiceRequest"].includes(entry.name)) return "serviceRequests";
|
||||
if (["requestCustomerServiceRequest", "resquestCrmServiceRequest", "requestInternalServiceRequest", "requestSrLookupPrimaryPillar", "requestSrLookupLanguage", "requestSrLookupTechPlatform", "requestSrLookupOpportunityBusiness", "requestResourceUser", "requestSrCatalog", "requestSrTokenCatalog", "requestPostServiceRequest"].includes(entry.name)) return "serviceRequests";
|
||||
if (/^request(?:AccountSummary|AccountPlans|ConsumptionService|ConsumptionPlanDetail|RatecardByPlan)/.test(entry.name)) return "consumption";
|
||||
return "other";
|
||||
@@ -249,7 +250,7 @@ function createDebugContentView({
|
||||
appendDebugBlock(details, "Request Headers", formatHeaderBlock(entry.requestHeaders));
|
||||
appendDebugBlock(details, "Request Body", formatDebugBody(entry.requestBody));
|
||||
appendDebugBlock(details, "Response Headers", formatHeaderBlock(entry.responseHeaders));
|
||||
appendDebugBlock(details, "Response Body", formatDebugBody(entry.responseBody));
|
||||
appendDebugBlock(details, "Response Body", formatDebugBody(entry.responseBody, entry.name));
|
||||
|
||||
item.append(heading, details);
|
||||
list.append(item);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, createSrButton, closeSrForm, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, environment }) {
|
||||
function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurrentPeriodRequest, bindPopover, closePopover, createSrButton, closeSrForm, allowsCrmActivities, reportProgress, activityCache, setBadgeContent, formatDate, environment }) {
|
||||
let status = "idle";
|
||||
let byOpportunity = new Map();
|
||||
let pending = null;
|
||||
@@ -79,6 +79,7 @@ function createOpportunityCrmServiceRequests({ requestCrmServiceRequest, isCurre
|
||||
`Title: ${display(item.Title)}`,
|
||||
`Opportunity: ${display(item.OpportunityName_Id_c?.OptyNumber)}`,
|
||||
`Reported Hours: ${display(item.ReportedHours_c)}`,
|
||||
`Customer Deadline: ${display(formatDate(item.CustomerDeadline_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)}`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, requestDetails, environment }) {
|
||||
function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, requestDetails, formatDate, openInternalSrForm, environment }) {
|
||||
const { document, window, setTimeout, clearTimeout } = environment;
|
||||
let active = null;
|
||||
let sequence = 0;
|
||||
@@ -91,6 +91,29 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, reque
|
||||
label.textContent = "SR Number:";
|
||||
heading.append(label, link);
|
||||
if (number) heading.append(createCopyButton(String(number), "SR number", "SR Number"));
|
||||
const add = document.createElement("button");
|
||||
add.type = "button";
|
||||
add.className = "opportunities-extension-copy-button opportunities-extension-internal-sr-add";
|
||||
add.title = "Create Internal Service Request";
|
||||
add.setAttribute("aria-label", add.title);
|
||||
add.setAttribute("aria-haspopup", "dialog");
|
||||
const icon = document.createElementNS("http://www.w3.org/2000/svg", "svg");
|
||||
icon.setAttribute("viewBox", "0 0 24 24");
|
||||
icon.setAttribute("aria-hidden", "true");
|
||||
icon.setAttribute("focusable", "false");
|
||||
const path = document.createElementNS("http://www.w3.org/2000/svg", "path");
|
||||
path.setAttribute("d", "M12 5v14M5 12h14");
|
||||
path.setAttribute("stroke", "currentColor");
|
||||
path.setAttribute("stroke-width", "2");
|
||||
path.setAttribute("stroke-linecap", "round");
|
||||
icon.append(path);
|
||||
add.append(icon);
|
||||
add.addEventListener("click", event => {
|
||||
event.preventDefault(); event.stopPropagation();
|
||||
close();
|
||||
openInternalSrForm(item, button);
|
||||
});
|
||||
heading.append(add);
|
||||
const details = document.createElement("div");
|
||||
details.className = "opportunities-extension-sr-details";
|
||||
details.textContent = descriptions[index];
|
||||
@@ -126,7 +149,7 @@ function createCrmSrPopover({ MODAL_ID, createCopyButton, createCloseIcon, reque
|
||||
body.className = "opportunities-extension-sr-details";
|
||||
body.textContent = ["Title", "OpportunityName_c", "StatusCd", "ServiceNew_c", "AccountPartyName",
|
||||
"LastUpdateDate", "SvcHealth_c", "AssigneePersonName", "OpenDate"].map(key =>
|
||||
`${key === "ServiceNew_c" ? "Service Queue" : key}: ${record[key] == null || record[key] === "" ? "N/D" : String(record[key])}`).join("\n");
|
||||
`${key === "ServiceNew_c" ? "Service Queue" : key}: ${record[key] == null || record[key] === "" ? "N/D" : String(key === "LastUpdateDate" || key === "OpenDate" ? formatDate(record[key]) : record[key])}`).join("\n");
|
||||
child.append(row, body);
|
||||
children.append(child);
|
||||
}
|
||||
|
||||
197
src/features/opportunities/internal-sr-form.js
Normal file
197
src/features/opportunities/internal-sr-form.js
Normal file
@@ -0,0 +1,197 @@
|
||||
function createInternalSrForm({ MODAL_ID, searchableSelect, formatDate, getOpportunity, requestPlatform, requestServices, requestSave, notify, environment }) {
|
||||
const { document } = environment;
|
||||
let active = null;
|
||||
function node(tag, className, text) {
|
||||
const el = document.createElement(tag);
|
||||
if (className) el.className = className;
|
||||
if (text !== undefined) el.textContent = text;
|
||||
return el;
|
||||
}
|
||||
function close(restoreFocus = true) {
|
||||
if (!active) return false;
|
||||
if (active.saving) return true;
|
||||
const previous = active;
|
||||
active = null;
|
||||
previous.modal.remove();
|
||||
if (restoreFocus && previous.trigger?.isConnected) previous.trigger.focus();
|
||||
return true;
|
||||
}
|
||||
function open(parent, trigger) {
|
||||
if (active?.saving) return;
|
||||
const host = document.getElementById(MODAL_ID);
|
||||
if (!host || host.hidden) return;
|
||||
close(false);
|
||||
const modal = node("section", "opportunities-extension-action-form-modal");
|
||||
modal.setAttribute("role", "dialog");
|
||||
modal.setAttribute("aria-modal", "true");
|
||||
modal.setAttribute("aria-labelledby", "internal-sr-form-title");
|
||||
const dialog = node("div", "opportunities-extension-action-form-dialog opportunities-extension-internal-sr-dialog");
|
||||
const header = node("header", "opportunities-extension-account-plans-header");
|
||||
const heading = node("div", "opportunities-extension-action-form-heading");
|
||||
const title = node("h2", "", "Create Internal Service Request");
|
||||
title.id = "internal-sr-form-title";
|
||||
const context = node("dl", "opportunities-extension-action-form-context");
|
||||
const opportunity = getOpportunity(parent.OpportunityName_Id_c?.OptyNumber);
|
||||
for (const [label, value] of [["SR Number", parent.SrNumber], ["Opportunity", opportunity?.Name], ["Title", parent.Title]]) {
|
||||
context.append(node("dt", "", label), node("dd", "", value == null || value === "" ? "N/D" : String(value)));
|
||||
}
|
||||
heading.append(title, context);
|
||||
header.append(heading);
|
||||
const form = node("form", "opportunities-extension-action-form");
|
||||
form.noValidate = true;
|
||||
const fields = new Map();
|
||||
const combos = new Map();
|
||||
const ready = new Set();
|
||||
function field(name, label, type, wide = false) {
|
||||
const wrapper = node("label", "opportunities-extension-action-form-field" + (wide ? " opportunities-extension-action-form-wide" : ""));
|
||||
const input = node(type === "textarea" || type === "select" ? type : "input");
|
||||
if (!["textarea", "select"].includes(type)) input.type = type;
|
||||
if (type === "textarea") input.rows = 4;
|
||||
input.id = `internal-sr-${name}`;
|
||||
input.name = name;
|
||||
fields.set(name, input);
|
||||
input.required = name !== "customerDeadline";
|
||||
input.setAttribute("aria-required", String(input.required));
|
||||
wrapper.htmlFor = input.id;
|
||||
wrapper.append(node("span", "", input.required ? `${label} (required)` : label), input);
|
||||
form.append(wrapper);
|
||||
return { input, wrapper };
|
||||
}
|
||||
const titleInput = field("title", "Title", "text", true).input;
|
||||
const deadline = field("customerDeadline", "Customer Deadline", "text").input;
|
||||
deadline.readOnly = true;
|
||||
deadline.value = formatDate(parent.CustomerDeadline_c) || "N/D";
|
||||
field("internalDeadline", "Internal Deadline", "date");
|
||||
const platform = field("platform", "Platform", "select", true);
|
||||
field("description", "Description", "textarea", true);
|
||||
const services = field("services", "Services", "select", true);
|
||||
const footer = node("footer", "opportunities-extension-action-form-footer");
|
||||
const cancel = node("button", "", "Cancel");
|
||||
const save = node("button", "opportunities-extension-action-form-save", "Save");
|
||||
save.type = "submit";
|
||||
save.disabled = true;
|
||||
const feedback = node("p", "opportunities-extension-action-form-status opportunities-extension-action-form-wide");
|
||||
feedback.setAttribute("role", "status");
|
||||
feedback.hidden = true;
|
||||
cancel.type = "button";
|
||||
cancel.addEventListener("click", () => close());
|
||||
footer.append(save, cancel);
|
||||
form.append(feedback, footer);
|
||||
dialog.append(header, form);
|
||||
modal.append(dialog);
|
||||
host.append(modal);
|
||||
const entry = { modal, trigger, saving: false };
|
||||
active = entry;
|
||||
const validDate = value => /^\d{4}-\d{2}-\d{2}$/.test(value) && Number.isFinite(Date.parse(`${value}T00:00:00Z`))
|
||||
&& new Date(`${value}T00:00:00Z`).toISOString().slice(0, 10) === value;
|
||||
form.addEventListener("submit", async event => {
|
||||
event.preventDefault();
|
||||
if (active !== entry || entry.saving || ready.size !== 2) return;
|
||||
let firstInvalid = null;
|
||||
for (const [name, input] of fields) {
|
||||
if (!input.required) continue;
|
||||
const value = String(input.value || "").trim();
|
||||
const valid = !!value && (name !== "internalDeadline" || validDate(value))
|
||||
&& (!combos.has(name) || Array.from(input.children).some(option => option.value === value));
|
||||
input.setAttribute("aria-invalid", String(!valid));
|
||||
const visible = combos.get(name)?.input || input;
|
||||
visible.setAttribute("aria-invalid", String(!valid));
|
||||
if (!valid && !firstInvalid) firstInvalid = visible;
|
||||
}
|
||||
feedback.hidden = false;
|
||||
if (firstInvalid) {
|
||||
feedback.setAttribute("role", "alert");
|
||||
feedback.textContent = "Complete all required fields with valid values.";
|
||||
firstInvalid.focus();
|
||||
return;
|
||||
}
|
||||
const value = name => String(fields.get(name).value || "").trim();
|
||||
const dateParts = value("customerDeadline").match(/^(\d{2})\/(\d{2})\/(\d{4})$/);
|
||||
const payload = {
|
||||
Title: value("title"),
|
||||
ServiceNew_Id_c: value("services"),
|
||||
ProblemDescription: value("description"),
|
||||
CustomerDeadline_c: dateParts ? `${dateParts[3]}-${dateParts[2]}-${dateParts[1]}` : "",
|
||||
TechPlatformLookUp_c: value("platform"),
|
||||
ServiceRequest_Id_SRToInternalSR: String(parent.SrId ?? ""),
|
||||
SvcQuestionsCollection_c: [], InternalDeadline_c: value("internalDeadline")
|
||||
};
|
||||
entry.saving = true;
|
||||
form.setAttribute("aria-busy", "true");
|
||||
save.disabled = cancel.disabled = true;
|
||||
save.textContent = "Saving...";
|
||||
for (const [name, input] of fields) { input.disabled = true; combos.get(name)?.sync(); }
|
||||
feedback.setAttribute("role", "status");
|
||||
feedback.textContent = "Creating Internal Service Request...";
|
||||
try {
|
||||
await requestSave(payload);
|
||||
entry.saving = false;
|
||||
close();
|
||||
notify("Internal Service Request created successfully.");
|
||||
} catch (error) {
|
||||
if (active !== entry) return;
|
||||
feedback.setAttribute("role", "alert");
|
||||
feedback.textContent = error.message || "Unable to create Internal Service Request.";
|
||||
notify(feedback.textContent, true);
|
||||
} finally {
|
||||
entry.saving = false;
|
||||
if (active === entry) {
|
||||
form.setAttribute("aria-busy", "false");
|
||||
save.disabled = cancel.disabled = false;
|
||||
save.textContent = "Save";
|
||||
for (const [name, input] of fields) { input.disabled = false; combos.get(name)?.sync(); }
|
||||
}
|
||||
}
|
||||
});
|
||||
function load(target, request, service) {
|
||||
const { input, wrapper } = target;
|
||||
input.disabled = true;
|
||||
const combo = searchableSelect(input, wrapper);
|
||||
combos.set(input.name, combo);
|
||||
const status = node("p", "opportunities-extension-action-form-status", "Loading...");
|
||||
status.setAttribute("role", "status");
|
||||
wrapper.append(status);
|
||||
wrapper.setAttribute("aria-busy", "true");
|
||||
Promise.resolve().then(request).then(items => {
|
||||
if (active !== entry) return;
|
||||
const placeholder = node("option", "", items.length ? "Select an option" : "No options available");
|
||||
placeholder.value = "";
|
||||
input.append(placeholder);
|
||||
const seen = new Set();
|
||||
for (const item of items) {
|
||||
const value = String(service ? item.Id : item.code);
|
||||
if (seen.has(value)) continue;
|
||||
seen.add(value);
|
||||
const option = node("option", "", service ? item.RecordName : item.name);
|
||||
option.value = value;
|
||||
if (service && item.Description_c) option.setAttribute("data-description", item.Description_c);
|
||||
input.append(option);
|
||||
}
|
||||
input.value = "";
|
||||
input.disabled = !items.length;
|
||||
if (items.length) ready.add(input.name);
|
||||
save.disabled = ready.size !== 2;
|
||||
status.textContent = items.length ? "" : "No options available.";
|
||||
status.hidden = !!items.length;
|
||||
combo.sync();
|
||||
}).catch(error => {
|
||||
if (active !== entry) return;
|
||||
status.setAttribute("role", "alert");
|
||||
status.textContent = error.message || "Unable to load options. Close and reopen the form to retry.";
|
||||
}).finally(() => { if (active === entry) wrapper.setAttribute("aria-busy", "false"); });
|
||||
}
|
||||
load(platform, requestPlatform, false);
|
||||
load(services, () => requestServices(parent.SrId), true);
|
||||
modal.addEventListener("keydown", event => {
|
||||
if (event.key === "Escape") { event.preventDefault(); event.stopPropagation(); close(); }
|
||||
if (event.key === "Tab") {
|
||||
const focusable = Array.from(form.querySelectorAll("input, textarea, button")).filter(el => !el.disabled && !el.hidden);
|
||||
const first = focusable[0], last = focusable.at(-1);
|
||||
if (event.shiftKey && document.activeElement === first) { event.preventDefault(); last.focus(); }
|
||||
else if (!event.shiftKey && document.activeElement === last) { event.preventDefault(); first.focus(); }
|
||||
}
|
||||
});
|
||||
titleInput.focus();
|
||||
}
|
||||
return { open, close };
|
||||
}
|
||||
@@ -66,11 +66,13 @@ function createSearchableSelect({ environment }) {
|
||||
}
|
||||
function render(query = "") {
|
||||
if (input.disabled) return;
|
||||
matches = options().filter(item => normalize(`${item.textContent} ${item.value}`).includes(normalize(query)));
|
||||
matches = options().filter(item => normalize(`${item.textContent} ${item.getAttribute?.("data-description") || ""} ${item.value}`).includes(normalize(query)));
|
||||
index = -1;
|
||||
list.textContent = "";
|
||||
matches.forEach((item, i) => {
|
||||
const row = element("div", "opportunities-extension-action-option", item.textContent);
|
||||
const description = item.getAttribute?.("data-description");
|
||||
if (description) row.append(element("small", "opportunities-extension-option-description", description));
|
||||
row.id = `${list.id}-${i}`;
|
||||
row.setAttribute("role", "option");
|
||||
row.setAttribute("aria-selected", "false");
|
||||
|
||||
@@ -626,3 +626,5 @@
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.opportunities-extension-refresh-all[aria-busy="true"] svg { animation: none; }
|
||||
}
|
||||
.opportunities-extension-option-description { display: block; font-size: 12px; line-height: 1.4; margin-top: 4px; white-space: normal; overflow-wrap: anywhere; opacity: .85; }
|
||||
.opportunities-extension-action-form-dialog.opportunities-extension-internal-sr-dialog { width: min(760px, 100%); }
|
||||
|
||||
@@ -11,7 +11,7 @@ function createFusionCrmServiceRequests({ isCurrentPeriodRequest,
|
||||
{ op: "$eq", attribute: "RecordSet", value: "ORA_SVC_MY_SR_TEAM_SRS" }
|
||||
] },
|
||||
keywords: null,
|
||||
fields: ["SrNumber", "OpportunityName_Id_c.OptyNumber", "ReportedHours_c", "_actions", "Title",
|
||||
fields: ["SrNumber", "OpportunityName_Id_c.OptyNumber", "ReportedHours_c", "CustomerDeadline_c", "PrimaryPillarNew_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",
|
||||
@@ -29,7 +29,7 @@ function createFusionCrmServiceRequests({ isCurrentPeriodRequest,
|
||||
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, attempt } });
|
||||
headers: { ...headers }, body: JSON.stringify(payload, null, 2), metadata: { offset, attempt } });
|
||||
try {
|
||||
const response = await fetch(url, {
|
||||
method: "POST", credentials: "include", headers, body: JSON.stringify(payload)
|
||||
|
||||
@@ -24,7 +24,7 @@ function createFusionOpportunities({
|
||||
url: OPPORTUNITIES_QUERY_URL,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${maskToken(token)}`,
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
Origin: "https://eeho.fa.us2.oraclecloud.com",
|
||||
Preference: "transient"
|
||||
@@ -151,7 +151,8 @@ function createFusionOpportunities({
|
||||
"CustomerAccount.PartyUniqueName",
|
||||
"CustomerAccount.PartyId",
|
||||
"CustomerAccount.PartyNumber",
|
||||
"CustomerAccount.Region_c"
|
||||
"CustomerAccount.Region_c",
|
||||
"CustomerAccount.PrimaryAddress.CountryCode"
|
||||
],
|
||||
sort: [],
|
||||
language: "en",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders }) {
|
||||
function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders, internalServiceRequestHeaders }) {
|
||||
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_initiation/live;profile=PROD/services/auth/1.1/proxy/";
|
||||
const catalog = base + "crmRestApiResources/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/11.13.18.05/Catalogue_c";
|
||||
const resourceUsers = base + "oke_seaas/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/resourceUsers";
|
||||
@@ -11,26 +11,76 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
|
||||
if (typeof value !== "string" || !value.trim()) throw new Error(`Missing ${name}.`);
|
||||
return value.trim();
|
||||
}
|
||||
function dateOnly(value, name) {
|
||||
const date = text(value, name);
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|
||||
|| new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) throw new Error(`Invalid ${name}.`);
|
||||
return date;
|
||||
}
|
||||
function payloadFor(input) {
|
||||
const payload = {};
|
||||
for (const name of ["AccountPartyId", "OpportunityName_Id_c", "ServiceNew_Id_c"]) payload[name] = numericId(input?.[name], name);
|
||||
for (const name of ["Title", "RegionText_c", "ProblemDescription", "Language_c", "CustomerDeadline_c", "TechPlatformLookUp_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(input?.[name], name);
|
||||
const date = payload.CustomerDeadline_c;
|
||||
if (!/^\d{4}-\d{2}-\d{2}$/.test(date) || !Number.isFinite(Date.parse(`${date}T00:00:00Z`))
|
||||
|| new Date(`${date}T00:00:00Z`).toISOString().slice(0, 10) !== date) throw new Error("Invalid Customer deadline.");
|
||||
dateOnly(payload.CustomerDeadline_c, "Customer deadline");
|
||||
return { ...payload, SvcQuestionsCollection_c: [], SourceCd: "SVC_OPPORTUNITY_SCREEN", SeverityCd: "ORA_SVC_SEV2", ChannelTypeCd: "ORA_SVC_WEB" };
|
||||
}
|
||||
async function requestResourceUser(email, debugRequests) {
|
||||
const emailAddress = text(email, "current user email");
|
||||
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
|
||||
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||
}, "requestResourceUser", debugRequests);
|
||||
return numericId(resource?.resourcePartyId, "resourcePartyId");
|
||||
}
|
||||
async function requestPostInternalServiceRequest(message) {
|
||||
const debugRequests = [];
|
||||
try {
|
||||
const source = message.payload;
|
||||
const parentSrId = numericId(source?.ServiceRequest_Id_SRToInternalSR, "parent SrId");
|
||||
const payload = {
|
||||
Title: text(source?.Title, "Title"),
|
||||
ServiceNew_Id_c: numericId(source?.ServiceNew_Id_c, "ServiceNew_Id_c"),
|
||||
ProblemDescription: text(source?.ProblemDescription, "Description"),
|
||||
CustomerDeadline_c: dateOnly(source?.CustomerDeadline_c, "Customer deadline"),
|
||||
TechPlatformLookUp_c: text(source?.TechPlatformLookUp_c, "Platform"),
|
||||
InternalDeadline_c: `${dateOnly(source?.InternalDeadline_c, "Internal Deadline")}T00:00:00.000Z`,
|
||||
SvcQuestionsCollection_c: []
|
||||
};
|
||||
const parent = await requestSRInfo(parentSrId, debugRequests);
|
||||
payload.ServiceRequest_Id_SRToInternalSR = String(parentSrId);
|
||||
for (const name of ["AccountPartyId", "OpportunityName_Id_c"]) payload[name] = numericId(parent[name], name);
|
||||
for (const name of ["OpportunityName_c", "RegionText_c", "SvcHealth_c", "Language_c", "CountryText_c", "PrimaryPillarNew_c"]) payload[name] = text(parent[name], name);
|
||||
payload.SecondaryPillarNew_c = parent.SecondaryPillarNew_c == null ? null : text(parent.SecondaryPillarNew_c, "SecondaryPillarNew_c");
|
||||
payload.ReportedByPartyId = await requestResourceUser(message.emailAddress, debugRequests);
|
||||
const url = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPICloudAccount/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests";
|
||||
const result = await fetchJson(url, { method: "POST", credentials: "include",
|
||||
headers: await internalServiceRequestHeaders(url),
|
||||
body: JSON.stringify(payload)
|
||||
}, "requestPostInternalServiceRequest", debugRequests, { allowEmpty: true });
|
||||
return { ok: true, result, debugRequests };
|
||||
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||
}
|
||||
async function requestSRInfo(parentSrId, debugRequests) {
|
||||
const url = new URL("https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetCRMRestAPI/uri/https/eeho.fa.us2.oraclecloud.com/crmRestApi/resources/latest/serviceRequests");
|
||||
url.searchParams.set("fields", "PrimaryPillarNew_c,SecondaryPillarNew_c,SvcHealth_c,SrNumber,AccountPartyId,CountryText_c,Language_c,OpportunityName_Id_c,OpportunityName_c,RegionText_c,ServiceNew_Id_c");
|
||||
url.searchParams.set("onlyData", "true");
|
||||
url.searchParams.set("orderBy", "LastUpdateDate:desc");
|
||||
url.searchParams.set("q", `SrId=${parentSrId}`);
|
||||
const result = await fetchJson(url.href, {
|
||||
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||
}, "requestSRInfo", debugRequests);
|
||||
if (!Array.isArray(result?.items) || result.items.length !== 1 || !result.items[0]
|
||||
|| (result.items[0].SrId != null && String(result.items[0].SrId) !== String(parentSrId))) {
|
||||
throw new Error("requestSRInfo did not return the selected parent Service Request.");
|
||||
}
|
||||
return result.items[0];
|
||||
}
|
||||
async function requestPostServiceRequest(message) {
|
||||
const debugRequests = [];
|
||||
try {
|
||||
if (message.phase !== "prepare" && message.phase !== "confirm") throw new Error("Service Request confirmation is required.");
|
||||
const payload = payloadFor(message.payload);
|
||||
if (message.phase === "prepare") {
|
||||
const emailAddress = text(message.emailAddress, "current user email");
|
||||
const resource = await fetchJson(`${resourceUsers}?emailAddress=${encodeURIComponent(emailAddress)}`, {
|
||||
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||
}, "requestResourceUser", debugRequests);
|
||||
payload.ReportedByPartyId = numericId(resource?.resourcePartyId, "resourcePartyId");
|
||||
payload.ReportedByPartyId = await requestResourceUser(message.emailAddress, debugRequests);
|
||||
const sharedHeaders = await previewTokenHeaders(catalog);
|
||||
return { ok: true, payload, debugRequests, headers: {
|
||||
requestSrTokenCatalog: sharedHeaders,
|
||||
@@ -38,7 +88,20 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
|
||||
} };
|
||||
}
|
||||
payload.ReportedByPartyId = numericId(message.payload?.ReportedByPartyId, "ReportedByPartyId");
|
||||
let id = message.catalogId;
|
||||
const headers = await requestCreationHeaders(message.catalogId, endpoint, debugRequests);
|
||||
const result = await fetchJson(endpoint, {
|
||||
method: "POST", credentials: "include", headers,
|
||||
body: JSON.stringify(payload)
|
||||
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
|
||||
return { ok: true, result, debugRequests };
|
||||
} catch (error) {
|
||||
error.debugRequests = debugRequests;
|
||||
throw error;
|
||||
} finally {
|
||||
annotateSigningRequest(debugRequests);
|
||||
}
|
||||
}
|
||||
async function requestCreationHeaders(id, target, debugRequests) {
|
||||
if (id == null) {
|
||||
const result = await fetchJson(`${catalog}?onlyData=true&fields=Id&limit=1`, {
|
||||
method: "GET", credentials: "include", headers: { Accept: "application/json", Authorization: "Session" }
|
||||
@@ -47,7 +110,7 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
|
||||
}
|
||||
id = numericId(id, "Catalogue Id");
|
||||
const tokenUrl = `${catalog}/${id}`;
|
||||
let catalogHeaders = await tokenHeaders(tokenUrl, endpoint);
|
||||
let catalogHeaders = await tokenHeaders(tokenUrl, target);
|
||||
const requestToken = () => fetchJson(tokenUrl, {
|
||||
method: "POST", credentials: "include", headers: catalogHeaders, body: JSON.stringify({ name: "getJwtToken" })
|
||||
}, "requestSrTokenCatalog", debugRequests);
|
||||
@@ -64,25 +127,14 @@ function createFusionSrCreate({ fetchJson, tokenHeaders, previewTokenHeaders })
|
||||
if (debugRequests.at(-1)?.httpStatus !== 200 || typeof token?.result !== "string" || !token.result.trim()) {
|
||||
throw new Error("requestSrTokenCatalog did not return HTTP 200 with a token.");
|
||||
}
|
||||
const result = await fetchJson(endpoint, {
|
||||
method: "POST", credentials: "include", headers: { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() },
|
||||
body: JSON.stringify(payload)
|
||||
}, "requestPostServiceRequest", debugRequests, { allowEmpty: true });
|
||||
return { ok: true, result, debugRequests };
|
||||
} catch (error) {
|
||||
error.debugRequests = debugRequests;
|
||||
throw error;
|
||||
} finally {
|
||||
// Never forward the signing token to content scripts, even with Debug secrets enabled.
|
||||
return { ...catalogHeaders, "vb-proxy-header-x-oracle-ectoken": token.result.trim() };
|
||||
}
|
||||
function annotateSigningRequest(debugRequests) {
|
||||
for (const entry of debugRequests) {
|
||||
if (entry.name === "requestSrTokenCatalog") {
|
||||
entry.metadata.appBuilderClientId = entry.requestHeaders?.["x-appbuilder-client-id"] ? "Observed in SeaaS requests" : "Not observed in SeaaS requests";
|
||||
entry.responseBody = JSON.stringify({ result: "(redacted)" });
|
||||
entry.responseBodyTruncated = false;
|
||||
}
|
||||
if (entry.requestHeaders?.["vb-proxy-header-x-oracle-ectoken"]) entry.requestHeaders["vb-proxy-header-x-oracle-ectoken"] = "(redacted)";
|
||||
}
|
||||
}
|
||||
}
|
||||
return { requestPostServiceRequest };
|
||||
return { requestPostServiceRequest, requestPostInternalServiceRequest };
|
||||
}
|
||||
|
||||
@@ -194,7 +194,33 @@ function createFusionSrDetails({ fetchJson, tabsCreate, tabsRemove, supportsFire
|
||||
return { ok: true, items: result.items.map(({ Id, RecordName, Abstract_c }) => ({ Id: String(Id), RecordName, Abstract_c: Abstract_c == null ? "" : String(Abstract_c) })), debugRequests };
|
||||
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||
}
|
||||
async function requestInternalSrOptions(message, services = false) {
|
||||
const debugRequests = [];
|
||||
const name = services ? "requestInternalSrServices" : "requestLookupTypePlatform";
|
||||
try {
|
||||
const base = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com/ic/builder/rt/SeaaS_SR_management/live;profile=PROD/services/auth/1.1/proxy/oalsetSeaaSOKECustomRestAPI/uri/https/gxpap.oracle.com/oalcrm/service/set/seaas/crm/";
|
||||
let path = "lookups?type=STANDARD&lookupType=SENGG_TECH_PLATFORM";
|
||||
if (services) {
|
||||
const id = String(message.parentServiceId ?? "");
|
||||
if (!/^\d+$/.test(id)) throw new Error("A numeric parent SrId is required.");
|
||||
if (typeof message.emailAddress !== "string" || !message.emailAddress.trim()) throw new Error("Current user email is unavailable.");
|
||||
path = `services?emailAddress=${encodeURIComponent(message.emailAddress.trim())}&parentServiceId=${encodeURIComponent(id)}&serviceType=INTERNAL`;
|
||||
}
|
||||
const result = await fetchJson(base + path, { method: "GET", credentials: "include",
|
||||
headers: { Accept: "application/json", Authorization: "Session" }
|
||||
}, name, debugRequests);
|
||||
if (!Array.isArray(result?.items) || result.items.some(item => services
|
||||
? !/^\d+$/.test(String(item?.Id ?? "")) || typeof item?.RecordName !== "string"
|
||||
: typeof item?.code !== "string" || typeof item?.name !== "string")) throw new Error(`Invalid ${name} response.`);
|
||||
const items = services
|
||||
? result.items.map(item => ({ Id: String(item.Id), RecordName: item.RecordName, Description_c: String(item.Description_c ?? "") }))
|
||||
: result.items.filter(item => item.enabledFlag !== "N").map(({ code, name }) => ({ code, name }));
|
||||
return { ok: true, items, debugRequests };
|
||||
} catch (error) { error.debugRequests = debugRequests; throw error; }
|
||||
}
|
||||
return { requestLookupWithSession, requestInternalServiceRequest, requestSrLookupOpportunityBusiness,
|
||||
requestLookupTypePlatform: () => requestInternalSrOptions({}),
|
||||
requestInternalSrServices: message => requestInternalSrOptions(message, true),
|
||||
prepareSession: sender => requestLookupWithSession(null, {}, sender, true),
|
||||
requestSrLookupPrimaryPillar: () => requestLookup("SENGG_PRIMARY_PILLAR", "requestSrLookupPrimaryPillar"),
|
||||
requestSrLookupLanguage: () => requestLookup("SENGG_LANGUAGE", "requestSrLookupLanguage"),
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
function createHttpBackgroundJson({
|
||||
previewDebugBody,
|
||||
DEBUG_BODY_LIMIT,
|
||||
observeResponseHeaders = () => {},
|
||||
environment
|
||||
}) {
|
||||
@@ -127,7 +126,7 @@ function createHttpBackgroundJson({
|
||||
}
|
||||
|
||||
function createDebugBody(text) {
|
||||
return previewDebugBody(text, DEBUG_BODY_LIMIT);
|
||||
return previewDebugBody(text);
|
||||
}
|
||||
|
||||
function normalizeDebugHeaders(headers) {
|
||||
|
||||
@@ -15,12 +15,6 @@ function createSpaCurrentUser({ fetchJson }) {
|
||||
for (const entry of debugRequests) { entry.status = "failed"; entry.error = error.message; }
|
||||
error.debugRequests = debugRequests;
|
||||
throw error;
|
||||
} finally {
|
||||
// The identity token is not needed by content scripts or debug, including on failures.
|
||||
for (const entry of debugRequests) {
|
||||
entry.responseBody = JSON.stringify(user ? { user, token: "(redacted)" } : { error: "Identity response omitted." }, null, 2);
|
||||
entry.responseBodyTruncated = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return { requestCurrentUser };
|
||||
|
||||
@@ -1,40 +1,3 @@
|
||||
function createDebugBodyPreview(text, limit) {
|
||||
if (text.length <= limit) return { value: text || "(empty response body)", truncated: false };
|
||||
try {
|
||||
const normalized = text.replace(/^\uFEFF/, "").trimStart()
|
||||
.replace(/^\)\]\}',?\s*/, "").replace(/^while\s*\(1\);\s*/, "");
|
||||
let value = JSON.parse(normalized);
|
||||
for (let depth = 0; depth < 2 && typeof value === "string" && /^\s*[\[{]/.test(value); depth += 1) {
|
||||
try { value = JSON.parse(value); } catch { break; }
|
||||
}
|
||||
// Bound retained data without cutting JSON syntax or losing sensitive field names.
|
||||
let remaining = Math.max(0, limit - 256);
|
||||
const visit = (node, depth = 0) => {
|
||||
if (remaining <= 64 || depth >= 30) return "[omitted from debug preview]";
|
||||
if (node === null || typeof node !== "object") {
|
||||
const serialized = JSON.stringify(node);
|
||||
if (serialized.length > remaining) {
|
||||
remaining = 0;
|
||||
return "[omitted from debug preview]";
|
||||
}
|
||||
remaining -= serialized.length + 1;
|
||||
return node;
|
||||
}
|
||||
const result = Array.isArray(node) ? [] : Object.create(null);
|
||||
remaining -= 2;
|
||||
for (const key of Object.keys(node)) {
|
||||
const cost = Array.isArray(node) ? 1 : JSON.stringify(key).length + 2;
|
||||
if (remaining < cost + 64) break;
|
||||
remaining -= cost;
|
||||
result[key] = visit(node[key], depth + 1);
|
||||
}
|
||||
return result;
|
||||
};
|
||||
return {
|
||||
value: JSON.stringify({ debugNotice: "Partial response preview: remaining fields/items omitted due to the debug size limit.", preview: visit(value) }),
|
||||
truncated: true
|
||||
};
|
||||
} catch {
|
||||
return { value: `${text.slice(0, limit)}\n... truncated ${text.length - limit} characters`, truncated: true };
|
||||
}
|
||||
function createDebugBodyPreview(text) {
|
||||
return { value: text || "(empty response body)", truncated: false };
|
||||
}
|
||||
|
||||
@@ -17,9 +17,12 @@ function createMessageTypes() {
|
||||
requestInternalServiceRequest: "opportunitiesExtension.requestInternalServiceRequest",
|
||||
requestSrLookupPrimaryPillar: "opportunitiesExtension.requestSrLookupPrimaryPillar",
|
||||
requestPostServiceRequest: "opportunitiesExtension.requestPostServiceRequest",
|
||||
requestPostInternalServiceRequest: "opportunitiesExtension.requestPostInternalServiceRequest",
|
||||
prepareSrSession: "opportunitiesExtension.prepareSrSession",
|
||||
requestSrLookupLanguage: "opportunitiesExtension.requestSrLookupLanguage",
|
||||
requestSrLookupTechPlatform: "opportunitiesExtension.requestSrLookupTechPlatform",
|
||||
requestLookupTypePlatform: "opportunitiesExtension.requestLookupTypePlatform",
|
||||
requestInternalSrServices: "opportunitiesExtension.requestInternalSrServices",
|
||||
requestSrLookupOpportunityBusiness: "opportunitiesExtension.requestSrLookupOpportunityBusiness",
|
||||
requestCurrentUser: "opportunitiesExtension.requestCurrentUser",
|
||||
invalidateProductsCache: "opportunitiesExtension.invalidateProductsCache",
|
||||
|
||||
@@ -93,7 +93,7 @@ test("opportunity query preserves fields, record-set OR, date AND, and paginatio
|
||||
{ op: "$or", criteria: types.map(value => ({ op: "$eq", attribute: "RecordSet", value })) },
|
||||
{ op: "$wi", attribute: "EffectiveDate", value1: range.startCloseDate, value2: range.endCloseDate, dynamicDate: false }
|
||||
] }, keywords: null, keywordsFields: fields,
|
||||
fields: [...fields, "CustomerAccount.PartyUniqueName", "CustomerAccount.PartyId", "CustomerAccount.PartyNumber", "CustomerAccount.Region_c"],
|
||||
fields: [...fields, "CustomerAccount.PartyUniqueName", "CustomerAccount.PartyId", "CustomerAccount.PartyNumber", "CustomerAccount.Region_c", "CustomerAccount.PrimaryAddress.CountryCode"],
|
||||
sort: [], language: "en", skipInValidFields: true, skipHiddenFromUIFields: true,
|
||||
copiedFrom: "queries/d5dbcd01-9ff3-40be-9b5b-64744cbf7162", limit: 15, offset: 15
|
||||
});
|
||||
@@ -183,6 +183,8 @@ test("tokenrelay missing-cookie recovery and HTTP failure do not change the requ
|
||||
assert.equal(h.refreshes.length, 1);
|
||||
assert.equal(calls.length, 1);
|
||||
assert.equal(calls[0].options.headers["x-xsrf-token"], "synthetic-xsrf");
|
||||
assert.equal(h.requestLog.at(-1).requestHeaders["x-xsrf-token"], "synthetic-xsrf");
|
||||
assert.equal(JSON.parse(h.requestLog.at(-1).responseBody).access_token, "synthetic-access");
|
||||
assert.equal(calls[0].options.method, "GET");
|
||||
for (const status of [400, 401, 403]) {
|
||||
let attempts = 0;
|
||||
|
||||
@@ -28,6 +28,14 @@ test("SR badge shows reported hours, sums linked SRs and preserves missing value
|
||||
}
|
||||
});
|
||||
|
||||
test("SR tooltip formats customer deadlines without timezone day shifts", async () => {
|
||||
for (const [value, expected] of [["2026-05-14T00:00:00.000+0000", "14/05/2026"], [null, "N/D"], [undefined, "N/D"], ["", "N/D"]]) {
|
||||
const app = setup(async () => response({ items: [{ ...sr("A"), CustomerDeadline_c: value }], hasMore: false }));
|
||||
await app.opportunityCrmServiceRequests.load("synthetic", 1);
|
||||
assert.ok(app.opportunityCrmServiceRequests.createBadge("A").attributes["aria-label"].includes(`Customer Deadline: ${expected}`));
|
||||
}
|
||||
});
|
||||
|
||||
test("CRM fetch is called without the environment object as its receiver", async () => {
|
||||
let calls = 0;
|
||||
const app = setup(async function (url, options) {
|
||||
@@ -76,7 +84,7 @@ test("CRM query preserves payload and pagination after opportunities, uses token
|
||||
{ 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",
|
||||
fields: ["SrNumber", "OpportunityName_Id_c.OptyNumber", "ReportedHours_c", "CustomerDeadline_c", "PrimaryPillarNew_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"],
|
||||
@@ -90,7 +98,11 @@ test("CRM query preserves payload and pagination after opportunities, uses token
|
||||
assert.equal(log.status, "success");
|
||||
assert.ok(log.responseBody);
|
||||
assert.ok(log.durationMs >= 0);
|
||||
assert.doesNotMatch(log.requestHeaders.Authorization, /synthetic-token/);
|
||||
assert.equal(log.requestHeaders.Authorization, "Bearer synthetic-token");
|
||||
assert.doesNotMatch(app.debugContentLog.formatHeaderBlock(log.requestHeaders), /synthetic-token/);
|
||||
app.debugState.debugSecretsVisible = true;
|
||||
assert.match(app.debugContentLog.formatHeaderBlock(log.requestHeaders), /Bearer synthetic-token/);
|
||||
app.debugState.debugSecretsVisible = false;
|
||||
assert.equal(app.debugContentView.getRequestCategory(log), "serviceRequests");
|
||||
}
|
||||
const found = app.opportunityCrmServiceRequests.createBadge("TEST-1");
|
||||
|
||||
@@ -3,6 +3,33 @@ const { test } = require("node:test");
|
||||
const { loadBackground, response } = require("./support/characterization.cjs");
|
||||
const type = "opportunitiesExtension.requestInternalServiceRequest";
|
||||
|
||||
test("internal SR form lookups use management proxy, Session and parent SrId", async () => {
|
||||
const calls = [];
|
||||
const h = loadBackground({ fetch: async (url, options) => {
|
||||
calls.push({ url: new URL(url), options });
|
||||
return response({ items: String(url).includes("/lookups?")
|
||||
? [{ code: "OCI", name: "Oracle Cloud" }]
|
||||
: [{ Id: 123, RecordName: "Review", Description_c: "Review the architecture" }] });
|
||||
} });
|
||||
const platform = await h.dispatch({ type: "opportunitiesExtension.requestLookupTypePlatform" });
|
||||
assert.equal(platform.ok, true);
|
||||
assert.equal(platform.items[0].code, "OCI");
|
||||
const service = await h.dispatch({ type: "opportunitiesExtension.requestInternalSrServices", parentServiceId: "456", emailAddress: "user+test@example.invalid" });
|
||||
assert.equal(service.ok, true);
|
||||
assert.equal(service.items[0].Id, "123");
|
||||
assert.equal(service.items[0].Description_c, "Review the architecture");
|
||||
assert.equal(calls[1].url.searchParams.get("parentServiceId"), "456");
|
||||
assert.equal(calls[1].url.searchParams.get("emailAddress"), "user+test@example.invalid");
|
||||
assert.equal(calls[1].url.searchParams.get("serviceType"), "INTERNAL");
|
||||
for (const call of calls) {
|
||||
assert.match(call.url.pathname, /SeaaS_SR_management.*oalsetSeaaSOKECustomRestAPI/);
|
||||
assert.equal(call.options.headers.Authorization, "Session");
|
||||
assert.equal(call.options.credentials, "include");
|
||||
}
|
||||
assert.equal((await h.dispatch({ type: "opportunitiesExtension.requestInternalSrServices", parentServiceId: "bad", emailAddress: "user@example.invalid" })).ok, false);
|
||||
assert.equal(calls.length, 2);
|
||||
});
|
||||
|
||||
test("preparation reuses an existing unexpired cookie pair without navigation or API calls", async () => {
|
||||
const domain = "comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||
const h = loadBackground({
|
||||
|
||||
@@ -3,6 +3,87 @@ const { test } = require("node:test");
|
||||
const { loadApplication } = require("./support/application.cjs");
|
||||
const flush = () => new Promise(resolve => setImmediate(resolve));
|
||||
|
||||
test("internal SR form validates Save, maps parent data, prevents duplicate posts and preserves failures", async () => {
|
||||
const h = setup();
|
||||
h.app.currentUser.getUserId = () => "current@example.invalid";
|
||||
h.app.opportunitiesTableState.opportunitiesTableState.items = [
|
||||
{ OptyNumber: "OTHER", Name: "Other opportunity", CustomerAccount: { PrimaryAddress: { CountryCode: "US" } } },
|
||||
{ OptyNumber: "OP1", Name: "<Opportunity name>", OptyId: 456, CountryCode: "CA",
|
||||
CustomerAccount: { PartyId: 789, Region_c_localizedValue: "LAD", CountryCode: "AR", PrimaryAddress: { CountryCode: "BR" } } }
|
||||
];
|
||||
h.app.debugContentView.renderDebugPanel = () => {};
|
||||
h.app.fusionCrmServiceRequests.requestCrmServiceRequest = async () => [{ SrId: "123", SrNumber: "SR123",
|
||||
CustomerDeadline_c: "2026-05-14T00:00:00.000+0000", PrimaryPillarNew_c: "Tech_Cloud_Infrastructure", Title: "<Parent SR title>", OpportunityName_Id_c: { OptyNumber: "OP1" } }];
|
||||
const calls = [];
|
||||
h.app.platformContentMessages.sendRuntimeMessage = async message => {
|
||||
calls.push(message);
|
||||
return { ok: true, items: message.type.endsWith("requestLookupTypePlatform") ? [{ code: "OCI", name: "Oracle Cloud" }]
|
||||
: message.type.endsWith("requestInternalSrServices") ? [{ Id: "45", RecordName: "Review", Description_c: "Architecture review" }] : [] };
|
||||
};
|
||||
await h.app.opportunityCrmServiceRequests.load("synthetic", 1);
|
||||
const badge = h.app.opportunityCrmServiceRequests.createBadge("OP1");
|
||||
h.modal.append(badge);
|
||||
badge.listeners.pointerenter();
|
||||
await flush();
|
||||
const add = h.walk(h.panel()).find(el => el.title === "Create Internal Service Request");
|
||||
assert.ok(add);
|
||||
assert.equal(add.parent.children[2].title, "Copy SR number");
|
||||
add.listeners.click({ preventDefault() {}, stopPropagation() {} });
|
||||
await flush();
|
||||
const nodes = h.walk(h.modal);
|
||||
const context = nodes.find(el => el.className === "opportunities-extension-action-form-context");
|
||||
assert.deepEqual(context.children.map(el => el.textContent), ["SR Number", "SR123", "Opportunity", "<Opportunity name>", "Title", "<Parent SR title>"]);
|
||||
assert.equal(nodes.find(el => el.name === "customerDeadline").value, "14/05/2026");
|
||||
assert.equal(nodes.find(el => el.name === "customerDeadline").readOnly, true);
|
||||
assert.equal(nodes.find(el => el.name === "customerDeadline").required, false);
|
||||
for (const name of ["title", "internalDeadline", "platform", "description", "services"]) {
|
||||
const input = nodes.find(el => el.name === name);
|
||||
assert.equal(input.required, true, name);
|
||||
assert.equal(input.attributes["aria-required"], "true", name);
|
||||
assert.match(input.parent.children[0].textContent, /\(required\)$/);
|
||||
}
|
||||
for (const name of ["platform", "services"]) {
|
||||
const input = nodes.find(el => el.id === `internal-sr-${name}-search`);
|
||||
assert.equal(input.required, true);
|
||||
assert.equal(input.attributes["aria-required"], "true");
|
||||
}
|
||||
assert.equal(nodes.find(el => el.name === "internalDeadline").type, "date");
|
||||
assert.equal(nodes.find(el => el.name === "platform").children[1].value, "OCI");
|
||||
assert.equal(nodes.find(el => el.name === "services").children[1].attributes["data-description"], "Architecture review");
|
||||
assert.equal(calls.at(-1).parentServiceId, "123");
|
||||
assert.equal(calls.at(-1).emailAddress, "current@example.invalid");
|
||||
const form = nodes.find(el => el.tag === "form");
|
||||
assert.deepEqual(h.walk(form).filter(el => el.tag === "button").map(el => el.textContent), ["Save", "Cancel"]);
|
||||
const submit = () => form.listeners.submit({ preventDefault() {} });
|
||||
const before = calls.length;
|
||||
await submit();
|
||||
assert.equal(calls.length, before);
|
||||
assert.equal(nodes.find(el => el.name === "title").attributes["aria-invalid"], "true");
|
||||
for (const [name, value] of Object.entries({ title: "New internal SR", internalDeadline: "2026-05-01", platform: "OCI", description: "Detailed request", services: "45" })) nodes.find(el => el.name === name).value = value;
|
||||
const posts = [], toasts = [];
|
||||
h.app.opportunitiesLinks.showCopyConfirmation = (message, isError = false) => toasts.push({ message, isError });
|
||||
h.app.platformContentMessages.sendRuntimeMessage = message => new Promise(resolve => posts.push({ message, resolve }));
|
||||
const saving = submit();
|
||||
await submit();
|
||||
assert.equal(posts.length, 1);
|
||||
assert.equal(posts[0].message.type, "opportunitiesExtension.requestPostInternalServiceRequest");
|
||||
assert.equal(posts[0].message.emailAddress, "current@example.invalid");
|
||||
assert.deepEqual(JSON.parse(JSON.stringify(posts[0].message.payload)), {
|
||||
Title: "New internal SR", ServiceNew_Id_c: "45",
|
||||
ProblemDescription: "Detailed request", CustomerDeadline_c: "2026-05-14", TechPlatformLookUp_c: "OCI",
|
||||
ServiceRequest_Id_SRToInternalSR: "123", SvcQuestionsCollection_c: [], InternalDeadline_c: "2026-05-01"
|
||||
});
|
||||
posts[0].resolve({ ok: false, httpStatus: 403, error: "Synthetic save failure" });
|
||||
await saving;
|
||||
assert.equal(nodes.find(el => el.name === "title").value, "New internal SR");
|
||||
assert.deepEqual(toasts, [{ message: "Synthetic save failure", isError: true }]);
|
||||
const retry = submit();
|
||||
posts[1].resolve({ ok: true, result: { SrNumber: "ISR1" } });
|
||||
await retry;
|
||||
assert.deepEqual(toasts.at(-1), { message: "Internal Service Request created successfully.", isError: false });
|
||||
assert.ok(!h.walk(h.modal).includes(form));
|
||||
});
|
||||
|
||||
test("internal SR uses Session without TokenRelay and reports unavailable iframe host", async () => {
|
||||
const h = setup();
|
||||
let renewals = 0;
|
||||
@@ -146,7 +227,8 @@ test("SR tooltip loads nested details with copy/link and empty text, discarding
|
||||
assert.equal(calls[0].message.token, undefined);
|
||||
assert.equal(calls[0].message.srId, "NOT-THE-URL-ID");
|
||||
assert.ok(h.walk(h.panel()).some(node => node.textContent === "Loading SR details..."));
|
||||
calls[0].resolve({ ok: true, items: [{ SrId: 99, SrNumber: "SR &child", Title: "<child>", ServiceNew_c: "Queue" }] });
|
||||
calls[0].resolve({ ok: true, items: [{ SrId: 99, SrNumber: "SR &child", Title: "<child>", ServiceNew_c: "Queue",
|
||||
LastUpdateDate: "2026-05-14T00:00:00.000+0000", OpenDate: "2026-04-01T23:30:00.000+0000" }] });
|
||||
calls[1].resolve({ ok: true, items: [] });
|
||||
await flush();
|
||||
const nodes = h.walk(h.panel());
|
||||
@@ -155,6 +237,8 @@ test("SR tooltip loads nested details with copy/link and empty text, discarding
|
||||
assert.ok(link.href.endsWith("srNumber=SR%20%26child&srId=99"));
|
||||
assert.equal(link.parent.children[2].title, "Copy SR number");
|
||||
assert.ok(nodes.some(node => node.textContent.includes("Service Queue: Queue")));
|
||||
assert.ok(nodes.some(node => node.textContent.includes("LastUpdateDate: 14/05/2026")));
|
||||
assert.ok(nodes.some(node => node.textContent.includes("OpenDate: 01/04/2026")));
|
||||
assert.ok(nodes.some(node => node.textContent === "No further details."));
|
||||
h.app.crmSrPopover?.close();
|
||||
h.app.opportunityCrmServiceRequests.reset();
|
||||
|
||||
@@ -5,7 +5,7 @@ const { loadApplication } = require("./support/application.cjs");
|
||||
const type = "opportunitiesExtension.requestCurrentUser";
|
||||
|
||||
for (const family of ["chrome", "firefox"]) {
|
||||
test(`${family}: current user GET uses SPA cookies and discards identity token before messaging/debug`, async () => {
|
||||
test(`${family}: current user GET preserves identity token only in debug data for explicit reveal`, async () => {
|
||||
const calls = [];
|
||||
const app = loadBackground({ family, fetch: async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
@@ -20,12 +20,16 @@ for (const family of ["chrome", "firefox"]) {
|
||||
assert.equal(calls[0].options.cache, "no-store");
|
||||
assert.equal(result.debugRequests[0].name, "requestCurrentUser");
|
||||
assert.equal(result.debugRequests[0].httpStatus, 200);
|
||||
assert.doesNotMatch(JSON.stringify(result), /synthetic-secret/);
|
||||
assert.equal(JSON.parse(result.debugRequests[0].responseBody).token, "synthetic-secret");
|
||||
const content = loadApplication("content");
|
||||
assert.doesNotMatch(content.debugContentLog.formatDebugBody(result.debugRequests[0].responseBody, "requestCurrentUser"), /synthetic-secret/);
|
||||
content.debugState.debugSecretsVisible = true;
|
||||
assert.match(content.debugContentLog.formatDebugBody(result.debugRequests[0].responseBody, "requestCurrentUser"), /synthetic-secret/);
|
||||
assert.equal(result.token, undefined);
|
||||
});
|
||||
}
|
||||
|
||||
test("identity failures and malformed roles remain explicit and never expose response tokens", async () => {
|
||||
test("identity failures retain diagnostics but hide response tokens by default", async () => {
|
||||
for (const fetch of [
|
||||
async () => new Response("synthetic-secret<html>fallback</html>"),
|
||||
async () => response({ token: "synthetic-secret", user: {} }),
|
||||
@@ -37,7 +41,8 @@ test("identity failures and malformed roles remain explicit and never expose res
|
||||
const result = await app.dispatch({ type });
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(result.debugRequests[0].status, "failed");
|
||||
assert.doesNotMatch(JSON.stringify(result), /synthetic-secret/);
|
||||
const content = loadApplication("content");
|
||||
assert.doesNotMatch(content.debugContentLog.formatDebugBody(result.debugRequests[0].responseBody, "requestCurrentUser"), /synthetic-secret/);
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -20,6 +20,26 @@ function fixture() {
|
||||
return { ...dom, app, add, nodes, tab, ids, select };
|
||||
}
|
||||
|
||||
test("catalog and identity secrets are revealed exactly by the eye button and hidden again", () => {
|
||||
const h = fixture();
|
||||
const body = JSON.stringify({ result: "catalog-exact-secret", detail: "Preserved" });
|
||||
h.app.debugContentLog.appendBackgroundRequestLog([{
|
||||
name: "requestSrTokenCatalog", method: "POST", startedAt: new Date().toISOString(), status: "success",
|
||||
responseBody: body, requestHeaders: { "vb-proxy-header-x-oracle-ectoken": "header-exact-secret", "x-appbuilder-client-id": "client-exact-secret" }
|
||||
}, {
|
||||
name: "requestCurrentUser", method: "GET", startedAt: new Date().toISOString(), status: "success",
|
||||
responseBody: JSON.stringify({ user: { roles: ["SIMPLE_USER"] }, token: "identity-exact-secret" })
|
||||
}]);
|
||||
const text = node => node.text + node.children.map(text).join("");
|
||||
const toggle = () => h.nodes().find(node => node.getAttribute("data-debug-focus") === "visibility").listeners.click();
|
||||
assert.doesNotMatch(text(h.panel), /exact-secret/);
|
||||
toggle();
|
||||
for (const secret of ["catalog-exact-secret", "header-exact-secret", "client-exact-secret", "identity-exact-secret"]) assert.ok(text(h.panel).includes(secret));
|
||||
toggle();
|
||||
assert.doesNotMatch(text(h.panel), /exact-secret/);
|
||||
assert.equal(h.app.debugState.requestLog[0].responseBody, body, "display masking never overwrites originals");
|
||||
});
|
||||
|
||||
test("request summaries show duration after status, including zero, failure and pending", () => {
|
||||
const h = fixture();
|
||||
h.add("duration", "requestOpportunities", 1);
|
||||
@@ -33,23 +53,23 @@ test("request summaries show duration after status, including zero, failure and
|
||||
}
|
||||
});
|
||||
|
||||
test("large response previews stay valid, formatted, masked and colored in both runtimes", () => {
|
||||
test("complete large responses stay formatted, masked and colored in both runtimes", () => {
|
||||
const h = fixture();
|
||||
const background = loadApplication("background");
|
||||
const body = JSON.stringify({ items: Array.from({ length: 2000 }, (_, index) => ({
|
||||
id: index, name: "Synthetic opportunity", access_token: "synthetic-secret",
|
||||
uiaasHeader: { value: "synthetic-header-secret" }, owner: { name: "Synthetic owner" }
|
||||
})) });
|
||||
for (const [logger, limit] of [[h.app.debugContentLog, 12000], [background.httpBackgroundJson, 100000]]) {
|
||||
for (const logger of [h.app.debugContentLog, background.httpBackgroundJson]) {
|
||||
const preview = logger.createDebugBody(body);
|
||||
assert.equal(preview.truncated, true);
|
||||
assert.ok(preview.value.length <= limit);
|
||||
assert.equal(preview.truncated, false);
|
||||
assert.equal(preview.value, body);
|
||||
const parsed = JSON.parse(preview.value);
|
||||
assert.match(parsed.debugNotice, /Partial response/);
|
||||
assert.ok(parsed.preview.items.length > 0);
|
||||
assert.ok(parsed.preview.items.length < 2000);
|
||||
assert.equal(parsed.items.length, 2000);
|
||||
assert.equal(parsed.items.at(-1).id, 1999);
|
||||
const formatted = h.app.debugContentLog.formatDebugBody(preview.value);
|
||||
assert.match(formatted, /\n "debugNotice"/);
|
||||
assert.match(formatted, /\n "items"/);
|
||||
assert.match(formatted, /"id": 1999/);
|
||||
assert.doesNotMatch(formatted, /synthetic-secret|synthetic-header-secret/);
|
||||
const container = h.document.createElement("div");
|
||||
h.app.debugContentView.appendDebugBlock(container, "Response Body", formatted);
|
||||
@@ -58,7 +78,9 @@ test("large response previews stay valid, formatted, masked and colored in both
|
||||
assert.match(h.app.debugContentLog.formatDebugBody(preview.value), /synthetic-secret/);
|
||||
h.app.debugState.debugSecretsVisible = false;
|
||||
assert.equal(logger.createDebugBody('{"ok":true}').value, '{"ok":true}');
|
||||
assert.match(logger.createDebugBody("<html>" + "x".repeat(limit + 1000)).value, /truncated/);
|
||||
const html = "<html>" + "x".repeat(150000) + "</html>";
|
||||
assert.equal(logger.createDebugBody(html).value, html);
|
||||
assert.equal(logger.createDebugBody("").value, "(empty response body)");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -69,7 +91,7 @@ test("request categories cover every current family and unknown requests stay vi
|
||||
requestConsumerTokenService: "authentication", requestCustomerTokenService: "authentication",
|
||||
requestOpportunities: "opportunities", requestOpptyProducts: "products", requestGenerateAiSummary: "ai",
|
||||
requestAccountSummary: "consumption", requestAccountPlans: "consumption",
|
||||
requestInstallBase: "installBase", requestCustomerServiceRequest: "serviceRequests",
|
||||
requestInstallBase: "installBase", requestCustomerServiceRequest: "serviceRequests", requestSRInfo: "serviceRequests",
|
||||
requestConsumptionPlanDetail: "consumption",
|
||||
requestRatecardByPlan: "consumption",
|
||||
requestConsumptionServiceQuarterly: "consumption", requestConsumptionServiceMonthly: "consumption",
|
||||
|
||||
13
tests/fixtures/extension.js
vendored
13
tests/fixtures/extension.js
vendored
@@ -1,10 +1,12 @@
|
||||
(() => {
|
||||
const listeners = [];
|
||||
const storage = new Map();
|
||||
const internalSrPreview = new URLSearchParams(location.search).has("internal-sr");
|
||||
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: "test-party", PartyNumber: "test-account" },
|
||||
CustomerAccount: { PartyUniqueName: "Example public sector customer (Example City, BR)", PartyId: internalSrPreview ? 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`,
|
||||
@@ -28,7 +30,11 @@
|
||||
onMessage: { addListener: listener => listeners.push(listener) },
|
||||
async sendMessage(message) {
|
||||
const operation = message.type.split(".").at(-1);
|
||||
if (operation === "requestCurrentUser") return { ok: true, user: { id: "example@example.invalid", name: "Example Owner", roles: ["SALES_REP"] }, debugRequests: [] };
|
||||
if (operation === "requestPostInternalServiceRequest") return { ok: true, result: { SrNumber: "ISR123" }, debugRequests: [] };
|
||||
if (operation === "requestCurrentUser") return { ok: true, user: { id: "example@example.invalid", name: "Example Owner", roles: internalSrPreview ? [] : ["SALES_REP"] }, debugRequests: [] };
|
||||
if (["prepareSrSession", "requestInternalServiceRequest", "requestWlActions"].includes(operation)) return { ok: true, items: [], actions: [], debugRequests: [] };
|
||||
if (operation === "requestLookupTypePlatform") return { ok: true, items: [{ code: "OCI", name: "Oracle Cloud Infrastructure" }, { code: "MULTI", name: "Multicloud" }], debugRequests: [] };
|
||||
if (operation === "requestInternalSrServices") return { ok: true, items: [{ Id: "123", RecordName: "Architecture review", Description_c: "Review the customer architecture, workloads and technical requirements." }, { Id: "124", RecordName: "Technical workshop", Description_c: "Prepare and deliver a technical session for the customer team." }], debugRequests: [] };
|
||||
if (operation === "getXsrfToken") return { ok: true, token: "synthetic-xsrf", cookieName: "XSRF-TOKEN-TEST" };
|
||||
if (operation === "requestConsumerTokenService") return { ok: true, authorizations, debugRequests: [] };
|
||||
if (operation === "requestOpptyProducts") {
|
||||
@@ -65,7 +71,8 @@
|
||||
let body;
|
||||
if (url.pathname === "/fscmRestApi/tokenrelay") body = { access_token: "synthetic-fusion-token", expires_in: 3600 };
|
||||
else if (url.pathname.endsWith("custom-actions/queries")) {
|
||||
const { offset, limit } = JSON.parse(options.body);
|
||||
const { offset, limit, entity } = JSON.parse(options.body);
|
||||
if (entity === "CRMServiceRequest") return new Response(JSON.stringify({ items: [{ SrId: "123456", SrNumber: "SR123456", Title: "Example parent SR", PrimaryPillarNew_c: "Tech_Cloud_Infrastructure", CustomerDeadline_c: "2026-11-30T00:00:00.000+0000", ReportedHours_c: 4, OpportunityName_Id_c: { OptyNumber: "TEST1" } }], hasMore: false }), { status: 200 });
|
||||
body = { items: opportunities.slice(offset, offset + limit), totalResults: opportunities.length, count: Math.min(limit, opportunities.length - offset), hasMore: offset + limit < opportunities.length, offset, limit };
|
||||
} else if (url.pathname.endsWith("summarizations/generate")) body = { summaryText: "Synthetic AI summary.\nText is displayed safely: <script>never executed</script>." };
|
||||
else throw new Error(`Unexpected synthetic fetch: ${url.pathname}`);
|
||||
|
||||
@@ -1,8 +1,141 @@
|
||||
const assert = require("node:assert/strict");
|
||||
const { test } = require("node:test");
|
||||
const { loadBackground, response } = require("./support/characterization.cjs");
|
||||
const { loadApplication } = require("./support/application.cjs");
|
||||
function assertDebugVisibility(result, secret) {
|
||||
const app = loadApplication("content");
|
||||
const entries = result.debugRequests || [];
|
||||
const render = () => entries.map(entry => app.debugContentLog.formatHeaderBlock(entry.requestHeaders)
|
||||
+ app.debugContentLog.formatDebugBody(entry.responseBody, entry.name)).join("\n");
|
||||
assert.ok(!render().includes(secret));
|
||||
app.debugState.debugSecretsVisible = true;
|
||||
assert.equal(render().includes(secret), JSON.stringify(entries).includes(secret));
|
||||
}
|
||||
const type = "opportunitiesExtension.requestPostServiceRequest";
|
||||
const emailAddress = "signed.in+sr@example.invalid";
|
||||
const internalType = "opportunitiesExtension.requestPostInternalServiceRequest";
|
||||
const internalInput = () => ({ Title: "Internal request title", ServiceNew_Id_c: "404", ProblemDescription: "Form description",
|
||||
CustomerDeadline_c: "2026-11-29", TechPlatformLookUp_c: "DATABASE", InternalDeadline_c: "2027-01-31",
|
||||
ServiceRequest_Id_SRToInternalSR: "505" });
|
||||
const parentInfo = () => ({ SrId: 505, SrNumber: "SR505", AccountPartyId: 100000693906507, OpportunityName_c: "A8YPN8",
|
||||
OpportunityName_Id_c: 302959906052470, RegionText_c: "LAD", SvcHealth_c: "SVC_HEALTH_YELLOW", Language_c: "PORTUGUESE",
|
||||
CountryText_c: "BR", PrimaryPillarNew_c: "Tech_Cloud_Platform", SecondaryPillarNew_c: "Tech_Cloud_Infrastructure", ServiceNew_Id_c: 300010753841648 });
|
||||
|
||||
for (const family of ["chrome", "firefox"]) {
|
||||
test(`${family}: internal SR reads parent SRInfo, resolves user and posts form values with Session and observed client ID`, async () => {
|
||||
const calls = [];
|
||||
const h = loadBackground({ family, fetch: async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
if (options.method === "GET" && url.includes("/serviceRequests?")) return withClientId({ items: [parentInfo()] }, 200, "internal-client");
|
||||
if (url.includes("/resourceUsers?")) return response({ resourcePartyId: 303 });
|
||||
assert.ok(!url.includes("Catalogue_c"), "Internal SR must not use catalog signing");
|
||||
return response({ SrNumber: "ISR1" }, 201);
|
||||
} });
|
||||
const input = { ...internalInput(), AccountPartyId: 1, OpportunityName_c: "STALE", OpportunityName_Id_c: 2, RegionText_c: "APAC",
|
||||
SvcHealth_c: "SVC_HEALTH_GREEN", Language_c: "ENGLISH", CountryText_c: "US", PrimaryPillarNew_c: "STALE", SecondaryPillarNew_c: null, ReportedByPartyId: 999 };
|
||||
const result = await h.dispatch({ type: internalType, emailAddress, payload: input });
|
||||
assert.equal(result.ok, true);
|
||||
assert.equal(calls.length, 3);
|
||||
const info = new URL(calls[0].url);
|
||||
assert.match(info.pathname, /SeaaS_SR_management.*\/oalsetCRMRestAPI\/.*resources\/latest\/serviceRequests$/);
|
||||
assert.equal(info.searchParams.get("q"), "SrId=505");
|
||||
assert.equal(info.searchParams.get("onlyData"), "true");
|
||||
assert.equal(info.searchParams.get("orderBy"), "LastUpdateDate:desc");
|
||||
assert.equal(info.searchParams.get("fields"), "PrimaryPillarNew_c,SecondaryPillarNew_c,SvcHealth_c,SrNumber,AccountPartyId,CountryText_c,Language_c,OpportunityName_Id_c,OpportunityName_c,RegionText_c,ServiceNew_Id_c");
|
||||
const post = calls[2];
|
||||
assert.equal(new URL(calls[1].url).searchParams.get("emailAddress"), emailAddress);
|
||||
assert.match(post.url, /SeaaS_SR_management.*oalsetCRMRestAPICloudAccount.*resources\/latest\/serviceRequests$/);
|
||||
assert.equal(post.options.method, "POST");
|
||||
const { SrId, SrNumber, ...parent } = parentInfo();
|
||||
assert.deepEqual(JSON.parse(post.options.body), { ...parent, ...internalInput(),
|
||||
ServiceRequest_Id_SRToInternalSR: "505", ServiceNew_Id_c: 404,
|
||||
InternalDeadline_c: "2027-01-31T00:00:00.000Z", ReportedByPartyId: 303, SvcQuestionsCollection_c: [] });
|
||||
assert.ok(calls.every(call => call.options.headers.Authorization === "Session" && call.options.credentials === "include"));
|
||||
const origin = "https://comcipapic-oalprod.integration.ocp.oraclecloud.com";
|
||||
assert.deepEqual({ ...post.options.headers }, { Accept: "application/json", "Content-Type": "application/json",
|
||||
Authorization: "Session", Origin: origin, "x-appbuilder-client-id": "internal-client" });
|
||||
assertDebugVisibility(result, "internal-client");
|
||||
assert.equal(calls[0].options.headers.Origin, undefined);
|
||||
assert.equal(result.debugRequests[2].requestHeaders.Origin, origin);
|
||||
assert.equal(h.headerRules.length, 1);
|
||||
const { removeRuleIds, addRules } = h.headerRules[0];
|
||||
assert.deepEqual(removeRuleIds, [7203]);
|
||||
assert.equal(addRules.length, 1);
|
||||
const rule = addRules[0];
|
||||
assert.deepEqual(rule.action, { type: "modifyHeaders", requestHeaders: [{ header: "Origin", operation: "set", value: origin }] });
|
||||
assert.deepEqual(rule.condition.initiatorDomains, ["synthetic-extension"]);
|
||||
assert.deepEqual(rule.condition.requestMethods, ["post"]);
|
||||
assert.deepEqual(rule.condition.resourceTypes, ["xmlhttprequest"]);
|
||||
const pattern = new RegExp(rule.condition.regexFilter);
|
||||
assert.ok(pattern.test(post.url));
|
||||
assert.ok(!pattern.test(calls[0].url));
|
||||
assert.ok(!pattern.test(post.url + "/other"));
|
||||
assert.ok(!pattern.test(post.url + "?other=1"));
|
||||
assert.deepEqual(result.debugRequests.map(entry => entry.name), ["requestSRInfo", "requestResourceUser", "requestPostInternalServiceRequest"]);
|
||||
});
|
||||
}
|
||||
|
||||
test("internal SR queries the current parent on every Save and reuses captured client ID after worker restart", async () => {
|
||||
const sessionStorage = {};
|
||||
const queries = [], posts = [];
|
||||
const fetch = async (url, options) => {
|
||||
if (url.includes("/serviceRequests?")) {
|
||||
const id = Number(new URL(url).searchParams.get("q").slice("SrId=".length));
|
||||
queries.push(id);
|
||||
const parent = { ...parentInfo(), SrId: id, SecondaryPillarNew_c: null, ServiceNew_Id_c: id + 1000 };
|
||||
if (id === 606) delete parent.ServiceNew_Id_c;
|
||||
return id === 505 ? withClientId({ items: [parent] }, 200, "saved-internal-client") : response({ items: [parent] });
|
||||
}
|
||||
if (url.includes("/resourceUsers?")) return response({ resourcePartyId: 303 });
|
||||
posts.push(options);
|
||||
return response({ SrNumber: "ISR1" }, 201);
|
||||
};
|
||||
const h = loadBackground({ fetch, sessionStorage });
|
||||
assert.equal((await h.dispatch({ type: internalType, emailAddress, payload: internalInput() })).ok, true);
|
||||
const restarted = loadBackground({ fetch, sessionStorage });
|
||||
assert.equal((await restarted.dispatch({ type: internalType, emailAddress,
|
||||
payload: { ...internalInput(), ServiceRequest_Id_SRToInternalSR: "606" } })).ok, true);
|
||||
assert.deepEqual(queries, [505, 606]);
|
||||
assert.equal(posts[1].headers["x-appbuilder-client-id"], "saved-internal-client");
|
||||
assert.equal(JSON.parse(posts[1].body).ServiceRequest_Id_SRToInternalSR, "606", "links to parent SrId, independently of the parent's service ID");
|
||||
assert.equal(JSON.parse(posts[1].body).SecondaryPillarNew_c, null);
|
||||
});
|
||||
|
||||
test("internal SR save validates form fields and parent ID before requesting SRInfo", async () => {
|
||||
let calls = 0;
|
||||
const h = loadBackground({ fetch: async () => { calls++; return response({}, 403); } });
|
||||
for (const change of [{ Title: "" }, { ProblemDescription: "" }, { TechPlatformLookUp_c: "" }, { InternalDeadline_c: "2026-02-30" },
|
||||
{ CustomerDeadline_c: "2026-02-30" }, { ServiceNew_Id_c: "bad" }, { ServiceRequest_Id_SRToInternalSR: "bad" }]) {
|
||||
assert.equal((await h.dispatch({ type: internalType, emailAddress, payload: { ...internalInput(), ...change } })).ok, false);
|
||||
}
|
||||
assert.equal(calls, 0);
|
||||
assert.equal((await h.dispatch({ type: internalType, emailAddress, payload: internalInput() })).ok, false);
|
||||
assert.equal(calls, 1);
|
||||
});
|
||||
|
||||
test("internal SR stops after missing or rejected parent data, failed user lookup or POST and never retries creation", async () => {
|
||||
for (const mode of ["info-401", "info-500", "empty", "multiple", "wrong-parent", "missing-country", "user", "post-403", "post-500", "network"]) {
|
||||
const calls = [];
|
||||
const h = loadBackground({ fetch: async (url, options) => {
|
||||
calls.push({ url, options });
|
||||
if (url.includes("/serviceRequests?")) {
|
||||
const parent = parentInfo();
|
||||
if (mode === "wrong-parent") parent.SrId = 999;
|
||||
if (mode === "missing-country") delete parent.CountryText_c;
|
||||
return response({ items: mode === "empty" ? [] : mode === "multiple" ? [parent, parent] : [parent] }, mode === "info-401" ? 401 : mode === "info-500" ? 500 : 200);
|
||||
}
|
||||
if (url.includes("/resourceUsers?")) return response({ resourcePartyId: 303 }, mode === "user" ? 403 : 200);
|
||||
if (mode === "network") throw new Error("Synthetic network failure");
|
||||
return response({}, mode === "post-403" ? 403 : 500);
|
||||
} });
|
||||
const result = await h.dispatch({ type: internalType, emailAddress, payload: internalInput() });
|
||||
assert.equal(result.ok, false, mode);
|
||||
const hasPost = ["post-403", "post-500", "network"].includes(mode);
|
||||
assert.equal(calls.length, hasPost ? 3 : mode === "user" ? 2 : 1, mode);
|
||||
assert.equal(calls.filter(call => call.options.method === "POST").length, hasPost ? 1 : 0);
|
||||
assert.equal(result.debugRequests.at(-1).status, ["empty", "multiple", "wrong-parent", "missing-country"].includes(mode) ? "success" : "failed");
|
||||
}
|
||||
});
|
||||
const payload = () => ({ AccountPartyId: 100000000000101, OpportunityName_Id_c: 202,
|
||||
ServiceNew_Id_c: 404, Title: "Service-OP1-Customer", RegionText_c: "LAD", CountryText_c: "BR",
|
||||
ProblemDescription: "Test description", Language_c: "ENGLISH", CustomerDeadline_c: "2027-01-31",
|
||||
@@ -80,7 +213,7 @@ test("token 403 retries once only with a newly returned client ID; SR POST is ne
|
||||
if (["recovered", "save-failure"].includes(mode)) {
|
||||
assert.deepEqual({ ...calls.at(-1).options.headers }, { ...tokenCalls.at(-1).options.headers, "vb-proxy-header-x-oracle-ectoken": "retry-token" });
|
||||
}
|
||||
assert.ok(!JSON.stringify(result).includes("retry-token"));
|
||||
assertDebugVisibility(result, "retry-token");
|
||||
}
|
||||
});
|
||||
|
||||
@@ -137,7 +270,7 @@ for (const family of ["chrome", "firefox"]) {
|
||||
assert.deepEqual(JSON.parse(calls[3].options.body), { ...payload(), ReportedByPartyId: 303, SvcQuestionsCollection_c: [],
|
||||
SourceCd: "SVC_OPPORTUNITY_SCREEN", SeverityCd: "ORA_SVC_SEV2", ChannelTypeCd: "ORA_SVC_WEB" });
|
||||
assert.equal(calls[3].options.body, JSON.stringify(prepared.payload));
|
||||
assert.ok(!JSON.stringify(result).includes("synthetic-secret-token"));
|
||||
assertDebugVisibility(result, "synthetic-secret-token");
|
||||
assert.equal(h.tabs.length, 0);
|
||||
});
|
||||
}
|
||||
@@ -209,6 +342,20 @@ test("Origin rule failure blocks token acquisition and SR creation", async () =>
|
||||
assert.match(result.error, /Rule permission denied/);
|
||||
});
|
||||
|
||||
test("Origin rule failure prevents the Internal SR POST", async () => {
|
||||
const calls = [];
|
||||
const h = loadBackground({ ruleError: true, fetch: async url => {
|
||||
calls.push(url);
|
||||
if (url.includes("/serviceRequests?")) return response({ items: [parentInfo()] });
|
||||
assert.match(url, /\/resourceUsers\?/);
|
||||
return response({ resourcePartyId: 303 });
|
||||
} });
|
||||
const result = await h.dispatch({ type: internalType, emailAddress, payload: internalInput() });
|
||||
assert.equal(result.ok, false);
|
||||
assert.match(result.error, /Rule permission denied/);
|
||||
assert.equal(calls.length, 2);
|
||||
});
|
||||
|
||||
test("resource lookup failures or invalid resourcePartyId never obtain a token or create SR", async () => {
|
||||
for (const body of [{ resourcePartyId: null }, { resourceProfileId: 999 }, { resourcePartyId: "9007199254740993" }, { resourcePartyId: 0 }, "unauthorized"]) {
|
||||
let calls = 0;
|
||||
@@ -238,7 +385,7 @@ test("token rejection never creates SR; creation failures are never automaticall
|
||||
const result = await h.dispatch({ type, phase: "confirm", catalogId: 505, payload: prepared.payload });
|
||||
assert.equal(result.ok, false);
|
||||
assert.equal(calls, failure === "token" || failure === "empty-token" ? 2 : 3);
|
||||
assert.ok(!JSON.stringify(result).includes("secret-token"));
|
||||
assertDebugVisibility(result, "secret-token");
|
||||
assert.equal(h.tabs.length, 0);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -125,9 +125,9 @@ test("SR Save validates form, maps dynamic values, locks duplicate clicks and pr
|
||||
h.app.debugContentView.renderDebugPanel = () => {};
|
||||
await h.app.opportunityCrmServiceRequests.load("test", 1);
|
||||
h.app.currentUser.getUserId = () => "current@example.invalid";
|
||||
h.app.opportunitiesTableState.opportunitiesTableState.items = [{ OptyNumber: "B", CustomerAccount: { PartyId: 999, Region_c: "EMEA" } }, {
|
||||
OptyNumber: "A", OptyId: 202, PartyId: 888, AccountPartyId: 777, Owner: { PartyId: 666 }, Region_c: "EMEA", RegionText_c: "LAD", Name: "Opportunity",
|
||||
CustomerAccount: { PartyId: 100000000000101, PartyUniqueName: "Customer", Region_c: "APAC", PrimaryAddress: { CountryCode: "BR" } }
|
||||
h.app.opportunitiesTableState.opportunitiesTableState.items = [{ OptyNumber: "B", CustomerAccount: { PartyId: 999, Region_c: "EMEA", PrimaryAddress: { CountryCode: "US" } } }, {
|
||||
OptyNumber: "A", OptyId: 202, PartyId: 888, AccountPartyId: 777, Owner: { PartyId: 666 }, Region_c: "EMEA", RegionText_c: "LAD", Name: "Opportunity", CountryCode: "CA",
|
||||
CustomerAccount: { PartyId: 100000000000101, PartyUniqueName: "Customer", Region_c: "APAC", CountryCode: "AR", PrimaryAddress: { CountryCode: "BR" } }
|
||||
}];
|
||||
const posts = [];
|
||||
const notices = [];
|
||||
@@ -167,7 +167,7 @@ test("SR Save validates form, maps dynamic values, locks duplicate clicks and pr
|
||||
assert.equal(payload.PrimaryPillarNew_c, "CODE");
|
||||
assert.equal(payload.Language_c, "CODE");
|
||||
assert.equal(payload.TechPlatformLookUp_c, "CODE");
|
||||
assert.equal(payload.CountryText_c, "BR");
|
||||
assert.equal(payload.CountryText_c, "BR", "uses the selected opportunity's CustomerAccount.PrimaryAddress.CountryCode, not CustomerAccount, root or another opportunity");
|
||||
assert.equal(payload.RegionText_c, "APAC", "uses CustomerAccount.Region_c from the selected opportunity, not a root-level region or another opportunity");
|
||||
assert.equal(payload.ProblemDescription, "Dynamic description");
|
||||
assert.equal(payload.CustomerDeadline_c, "2027-01-31");
|
||||
@@ -230,7 +230,9 @@ test("SR Save validates form, maps dynamic values, locks duplicate clicks and pr
|
||||
posts[3].resolve({ ok: false, error: "Synthetic failure" });
|
||||
await failed;
|
||||
assert.equal(notices.at(-1)[1], true);
|
||||
delete h.app.opportunitiesTableState.opportunitiesTableState.items[1].CustomerAccount.CountryCode;
|
||||
const retry = submit();
|
||||
assert.equal(posts[4].message.payload.CountryText_c, "BR", "uses PrimaryAddress.CountryCode even without CustomerAccount.CountryCode");
|
||||
posts[4].resolve({ ok: true, payload: preparedPayload });
|
||||
await flush();
|
||||
confirm().click();
|
||||
|
||||
Reference in New Issue
Block a user