Implemented the Account Plan request from the SPA
This commit is contained in:
@@ -25,6 +25,7 @@ The build recreates `dist/chromium` and `dist/firefox` from the same source.
|
|||||||
- Column sorting, links to opportunities and accounts, opportunity number copying, and stage indicators.
|
- Column sorting, links to opportunities and accounts, opportunity number copying, and stage indicators.
|
||||||
- Opportunity type view preferences persisted in the browser.
|
- Opportunity type view preferences persisted in the browser.
|
||||||
- Background product retrieval with configurable caching, progress tracking, and expandable product details.
|
- Background product retrieval with configurable caching, progress tracking, and expandable product details.
|
||||||
|
- Cloud consumption account-plan lookup from each opportunity, displayed in a tabbed detail modal.
|
||||||
- Light and dark themes based on Oracle Redwood.
|
- Light and dark themes based on Oracle Redwood.
|
||||||
- Separate outputs for Chromium/Edge and Firefox.
|
- Separate outputs for Chromium/Edge and Firefox.
|
||||||
|
|
||||||
|
|||||||
429
dist/chromium/background.js
vendored
429
dist/chromium/background.js
vendored
@@ -17,6 +17,31 @@
|
|||||||
const OPPTY_PRODUCTS_PAGE_LIMIT = 15;
|
const OPPTY_PRODUCTS_PAGE_LIMIT = 15;
|
||||||
const OPPTY_PRODUCTS_MAX_PAGES = 250;
|
const OPPTY_PRODUCTS_MAX_PAGES = 250;
|
||||||
const OPPTY_PRODUCTS_CONCURRENCY = 4;
|
const OPPTY_PRODUCTS_CONCURRENCY = 4;
|
||||||
|
const CUSTOMER_TOKEN_SERVICE_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/consumerTokenService";
|
||||||
|
const ACCOUNT_SUMMARY_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary";
|
||||||
|
const ACCOUNT_PLANS_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans";
|
||||||
|
const ACCOUNT_PLANS_PAYLOAD = {
|
||||||
|
hierarchy: "single",
|
||||||
|
products: "all",
|
||||||
|
language: "english",
|
||||||
|
planStatus: [
|
||||||
|
"active",
|
||||||
|
"awaiting_provisioning",
|
||||||
|
"awaiting_provisioning_error",
|
||||||
|
"draft",
|
||||||
|
"expired",
|
||||||
|
"final_billing",
|
||||||
|
"onhold",
|
||||||
|
"provisioned",
|
||||||
|
"signed",
|
||||||
|
"superseded",
|
||||||
|
"superseded_pending_final_billing",
|
||||||
|
"suspended",
|
||||||
|
"terminated",
|
||||||
|
"terminated_pending_final_billing"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const DEBUG_BODY_LIMIT = 100000;
|
||||||
const OPPTY_PRODUCTS_FIELDS = [
|
const OPPTY_PRODUCTS_FIELDS = [
|
||||||
"ProductType", "Description", "InventoryItemId", "ProdGroupName", "ProdGroupId", "OwnerLockAsgnFlag",
|
"ProductType", "Description", "InventoryItemId", "ProdGroupName", "ProdGroupId", "OwnerLockAsgnFlag",
|
||||||
"Quantity", "RecurTypeCode", "RevnAmountCurcyCode", "UnitPrice", "RevnAmount", "PriceTypeCode",
|
"Quantity", "RecurTypeCode", "RevnAmountCurcyCode", "UnitPrice", "RevnAmount", "PriceTypeCode",
|
||||||
@@ -96,9 +121,413 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === "opportunitiesExtension.requestConsumerTokenService") {
|
||||||
|
requestConsumerTokenService()
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
error: error.message || "Unable to load consumer token service.",
|
||||||
|
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "opportunitiesExtension.requestAccountPlans") {
|
||||||
|
requestAccountPlans(message)
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
error: error.message || "Unable to load account plans.",
|
||||||
|
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function requestConsumerTokenService() {
|
||||||
|
const debugRequests = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Pragma: "no-cache"
|
||||||
|
}
|
||||||
|
}, "requestConsumerTokenService", debugRequests);
|
||||||
|
const accountSummaryAuthorization = extractTokenServiceAuthorization(
|
||||||
|
tokenResponse,
|
||||||
|
"ACCP-TS",
|
||||||
|
"account-ti-authorization"
|
||||||
|
);
|
||||||
|
const accountPlansAuthorization = extractTokenServiceAuthorization(
|
||||||
|
tokenResponse,
|
||||||
|
"OCICONS-TS",
|
||||||
|
"g2m-authorization"
|
||||||
|
);
|
||||||
|
const tokenRequestDebugEntry = debugRequests[debugRequests.length - 1];
|
||||||
|
|
||||||
|
if (tokenRequestDebugEntry) {
|
||||||
|
tokenRequestDebugEntry.metadata = {
|
||||||
|
...tokenRequestDebugEntry.metadata,
|
||||||
|
accountSummaryTokenService: "ACCP-TS",
|
||||||
|
accountSummaryAuthorizationHeader: accountSummaryAuthorization.name || "(missing)",
|
||||||
|
accountSummaryAuthorizationValue: accountSummaryAuthorization.value
|
||||||
|
? `(present, ${accountSummaryAuthorization.value.length} characters)`
|
||||||
|
: "(missing)",
|
||||||
|
accountSummaryAuthorizationExpires: accountSummaryAuthorization.expires || "(not provided)",
|
||||||
|
accountPlansTokenService: "OCICONS-TS",
|
||||||
|
accountPlansAuthorizationHeader: accountPlansAuthorization.name || "(missing)",
|
||||||
|
accountPlansAuthorizationValue: accountPlansAuthorization.value
|
||||||
|
? `(present, ${accountPlansAuthorization.value.length} characters)`
|
||||||
|
: "(missing)",
|
||||||
|
accountPlansAuthorizationExpires: accountPlansAuthorization.expires || "(not provided)"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountSummaryAuthorization.value) {
|
||||||
|
throw new Error("ACCP-TS uiaasHeader.value was not found in requestConsumerTokenService.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountPlansAuthorization.value) {
|
||||||
|
throw new Error("OCICONS-TS uiaasHeader.value was not found in requestConsumerTokenService.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
authorizations: {
|
||||||
|
accountSummary: accountSummaryAuthorization,
|
||||||
|
accountPlans: accountPlansAuthorization
|
||||||
|
},
|
||||||
|
debugRequests
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
error.debugRequests = debugRequests;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestAccountPlans(message) {
|
||||||
|
const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : "";
|
||||||
|
const accountSummaryAuthorization = normalizeMessageAuthorization(
|
||||||
|
message.authorizations && message.authorizations.accountSummary,
|
||||||
|
"account-ti-authorization"
|
||||||
|
);
|
||||||
|
const accountPlansAuthorization = normalizeMessageAuthorization(
|
||||||
|
message.authorizations && message.authorizations.accountPlans,
|
||||||
|
"g2m-authorization"
|
||||||
|
);
|
||||||
|
const debugRequests = [];
|
||||||
|
|
||||||
|
if (!partyNumber) {
|
||||||
|
throw new Error("Customer Party Number is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountSummaryAuthorization.value) {
|
||||||
|
throw new Error("ACCP-TS authorization is unavailable. requestConsumerTokenService must complete first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountPlansAuthorization.value) {
|
||||||
|
throw new Error("OCICONS-TS authorization is unavailable. requestConsumerTokenService must complete first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const accountSummary = await fetchJson(ACCOUNT_SUMMARY_URL, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
[accountSummaryAuthorization.name]: accountSummaryAuthorization.value,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify([{ key: "regId", value: partyNumber }])
|
||||||
|
}, "requestAccountSummary", debugRequests);
|
||||||
|
const accountPlanId = findAccountPlanId(accountSummary);
|
||||||
|
|
||||||
|
if (!accountPlanId) {
|
||||||
|
throw new Error("Account plan id was not found in requestAccountSummary.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountPlansResponse = await fetchJson(`${ACCOUNT_PLANS_URL}/${encodeURIComponent(String(accountPlanId))}`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
[accountPlansAuthorization.name]: accountPlansAuthorization.value,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(ACCOUNT_PLANS_PAYLOAD)
|
||||||
|
}, "requestAccountPlans", debugRequests);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
accountPlanId: String(accountPlanId),
|
||||||
|
plans: extractAccountPlans(accountPlansResponse),
|
||||||
|
debugRequests
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
error.debugRequests = debugRequests;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMessageAuthorization(authorization, fallbackHeaderName) {
|
||||||
|
const headerName = authorization && typeof authorization.name === "string"
|
||||||
|
? authorization.name.trim()
|
||||||
|
: "";
|
||||||
|
const headerValue = authorization && typeof authorization.value === "string"
|
||||||
|
? authorization.value.trim()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: headerName || fallbackHeaderName,
|
||||||
|
value: headerValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTokenServiceAuthorization(response, tokenServiceName, fallbackHeaderName) {
|
||||||
|
const tokenService = Array.isArray(response)
|
||||||
|
? response.find((entry) => entry && entry.tokenService === tokenServiceName)
|
||||||
|
: findNestedObject(response, (entry) => entry.tokenService === tokenServiceName);
|
||||||
|
const authorizationHeader = tokenService && tokenService.uiaasHeader;
|
||||||
|
const headerName = authorizationHeader && typeof authorizationHeader.label === "string"
|
||||||
|
? authorizationHeader.label.trim()
|
||||||
|
: "";
|
||||||
|
const headerValue = authorizationHeader && typeof authorizationHeader.value === "string"
|
||||||
|
? authorizationHeader.value.trim()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: headerName || fallbackHeaderName,
|
||||||
|
value: headerValue,
|
||||||
|
expires: authorizationHeader && authorizationHeader.expires
|
||||||
|
? String(authorizationHeader.expires)
|
||||||
|
: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJson(url, options, requestName, debugRequests = null) {
|
||||||
|
const startedAt = new Date();
|
||||||
|
const debugEntry = createBackgroundDebugEntry(requestName, url, options, startedAt);
|
||||||
|
let response;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await fetch(url, options);
|
||||||
|
} catch (error) {
|
||||||
|
completeBackgroundDebugEntry(debugEntry, null, "", error.message || "Network request failed.");
|
||||||
|
appendBackgroundDebugEntry(debugRequests, debugEntry);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responsePayload = await readResponsePayload(response);
|
||||||
|
const responseBody = createDebugBody(responsePayload.text);
|
||||||
|
completeBackgroundDebugEntry(debugEntry, response, responseBody, "");
|
||||||
|
debugEntry.metadata.responseContentType = response.headers.get("content-type") || "(not provided)";
|
||||||
|
debugEntry.metadata.responseLength = String(responsePayload.text.length);
|
||||||
|
appendBackgroundDebugEntry(debugRequests, debugEntry);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`${requestName} failed with status ${response.status}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responsePayload.error) {
|
||||||
|
throw new Error(`${requestName} response body could not be read.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseJsonResponse(responsePayload.text, requestName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBackgroundDebugEntry(requestName, url, options, startedAt) {
|
||||||
|
return {
|
||||||
|
name: requestName,
|
||||||
|
method: options.method || "GET",
|
||||||
|
url,
|
||||||
|
status: "pending",
|
||||||
|
startedAt: startedAt.toISOString(),
|
||||||
|
completedAt: null,
|
||||||
|
durationMs: null,
|
||||||
|
httpStatus: null,
|
||||||
|
statusText: "",
|
||||||
|
requestHeaders: normalizeDebugHeaders(options.headers),
|
||||||
|
requestBody: formatDebugRequestBody(options.body),
|
||||||
|
metadata: {
|
||||||
|
executionContext: "background"
|
||||||
|
},
|
||||||
|
responseHeaders: {},
|
||||||
|
responseBody: "",
|
||||||
|
responseBodyTruncated: false,
|
||||||
|
error: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function completeBackgroundDebugEntry(entry, response, responseBody, errorMessage) {
|
||||||
|
const completedAt = new Date();
|
||||||
|
entry.completedAt = completedAt.toISOString();
|
||||||
|
entry.durationMs = completedAt.getTime() - new Date(entry.startedAt).getTime();
|
||||||
|
entry.error = errorMessage;
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
entry.status = "failed";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.status = response.ok ? "success" : "failed";
|
||||||
|
entry.httpStatus = response.status;
|
||||||
|
entry.statusText = response.statusText || "";
|
||||||
|
entry.responseHeaders = headersToObject(response.headers);
|
||||||
|
entry.responseBody = responseBody.value;
|
||||||
|
entry.responseBodyTruncated = responseBody.truncated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendBackgroundDebugEntry(debugRequests, entry) {
|
||||||
|
if (Array.isArray(debugRequests)) {
|
||||||
|
debugRequests.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readResponsePayload(response) {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
text: await response.text(),
|
||||||
|
error: ""
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
text: `Unable to read response body: ${error.message || "unknown error"}`,
|
||||||
|
error: error.message || "unknown error"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonResponse(text, requestName) {
|
||||||
|
const normalizedText = String(text)
|
||||||
|
.replace(/^\uFEFF/, "")
|
||||||
|
.trimStart()
|
||||||
|
.replace(/^\)\]\}',?\s*/, "")
|
||||||
|
.replace(/^while\s*\(1\);\s*/, "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(normalizedText);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`${requestName} returned an invalid JSON response.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDebugBody(text) {
|
||||||
|
if (text.length > DEBUG_BODY_LIMIT) {
|
||||||
|
return {
|
||||||
|
value: `${text.slice(0, DEBUG_BODY_LIMIT)}\n... truncated ${text.length - DEBUG_BODY_LIMIT} characters`,
|
||||||
|
truncated: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: text || "(empty response body)",
|
||||||
|
truncated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDebugHeaders(headers) {
|
||||||
|
const normalized = {};
|
||||||
|
|
||||||
|
Object.entries(headers || {}).forEach(([key, value]) => {
|
||||||
|
normalized[key] = String(value);
|
||||||
|
});
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function headersToObject(headers) {
|
||||||
|
const values = {};
|
||||||
|
|
||||||
|
if (headers && typeof headers.forEach === "function") {
|
||||||
|
headers.forEach((value, key) => {
|
||||||
|
values[key] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDebugRequestBody(body) {
|
||||||
|
if (!body) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(body), null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
return String(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNestedObject(value, predicate, visited = new Set()) {
|
||||||
|
if (!value || typeof value !== "object" || visited.has(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.add(value);
|
||||||
|
|
||||||
|
if (!Array.isArray(value) && predicate(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const child of Object.values(value)) {
|
||||||
|
const match = findNestedObject(child, predicate, visited);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAccountPlanId(response) {
|
||||||
|
const account = findNestedObject(response, (value) => (
|
||||||
|
Object.prototype.hasOwnProperty.call(value, "id")
|
||||||
|
&& value.id !== null
|
||||||
|
&& value.id !== undefined
|
||||||
|
&& value.id !== ""
|
||||||
|
));
|
||||||
|
return account ? account.id : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAccountPlans(response) {
|
||||||
|
const plans = [];
|
||||||
|
const visited = new Set();
|
||||||
|
|
||||||
|
const visit = (value) => {
|
||||||
|
if (!value || typeof value !== "object" || visited.has(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.add(value);
|
||||||
|
|
||||||
|
if (!Array.isArray(value) && (
|
||||||
|
Object.prototype.hasOwnProperty.call(value, "planType")
|
||||||
|
|| Object.prototype.hasOwnProperty.call(value, "subPlanNum")
|
||||||
|
)) {
|
||||||
|
plans.push(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.values(value).forEach(visit);
|
||||||
|
};
|
||||||
|
|
||||||
|
visit(response);
|
||||||
|
return plans;
|
||||||
|
}
|
||||||
|
|
||||||
async function requestOpptyProducts(message, sender) {
|
async function requestOpptyProducts(message, sender) {
|
||||||
const requestId = typeof message.requestId === "string" ? message.requestId : "";
|
const requestId = typeof message.requestId === "string" ? message.requestId : "";
|
||||||
const accessToken = typeof message.accessToken === "string" ? message.accessToken : "";
|
const accessToken = typeof message.accessToken === "string" ? message.accessToken : "";
|
||||||
|
|||||||
843
dist/chromium/content.js
vendored
843
dist/chromium/content.js
vendored
File diff suppressed because it is too large
Load Diff
3
dist/chromium/manifest.json
vendored
3
dist/chromium/manifest.json
vendored
@@ -8,7 +8,8 @@
|
|||||||
"storage"
|
"storage"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://eeho.fa.us2.oraclecloud.com/*"
|
"https://eeho.fa.us2.oraclecloud.com/*",
|
||||||
|
"https://spa.oracle.com/*"
|
||||||
],
|
],
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
|
|||||||
429
dist/firefox/background.js
vendored
429
dist/firefox/background.js
vendored
@@ -17,6 +17,31 @@
|
|||||||
const OPPTY_PRODUCTS_PAGE_LIMIT = 15;
|
const OPPTY_PRODUCTS_PAGE_LIMIT = 15;
|
||||||
const OPPTY_PRODUCTS_MAX_PAGES = 250;
|
const OPPTY_PRODUCTS_MAX_PAGES = 250;
|
||||||
const OPPTY_PRODUCTS_CONCURRENCY = 4;
|
const OPPTY_PRODUCTS_CONCURRENCY = 4;
|
||||||
|
const CUSTOMER_TOKEN_SERVICE_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/consumerTokenService";
|
||||||
|
const ACCOUNT_SUMMARY_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary";
|
||||||
|
const ACCOUNT_PLANS_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans";
|
||||||
|
const ACCOUNT_PLANS_PAYLOAD = {
|
||||||
|
hierarchy: "single",
|
||||||
|
products: "all",
|
||||||
|
language: "english",
|
||||||
|
planStatus: [
|
||||||
|
"active",
|
||||||
|
"awaiting_provisioning",
|
||||||
|
"awaiting_provisioning_error",
|
||||||
|
"draft",
|
||||||
|
"expired",
|
||||||
|
"final_billing",
|
||||||
|
"onhold",
|
||||||
|
"provisioned",
|
||||||
|
"signed",
|
||||||
|
"superseded",
|
||||||
|
"superseded_pending_final_billing",
|
||||||
|
"suspended",
|
||||||
|
"terminated",
|
||||||
|
"terminated_pending_final_billing"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const DEBUG_BODY_LIMIT = 100000;
|
||||||
const OPPTY_PRODUCTS_FIELDS = [
|
const OPPTY_PRODUCTS_FIELDS = [
|
||||||
"ProductType", "Description", "InventoryItemId", "ProdGroupName", "ProdGroupId", "OwnerLockAsgnFlag",
|
"ProductType", "Description", "InventoryItemId", "ProdGroupName", "ProdGroupId", "OwnerLockAsgnFlag",
|
||||||
"Quantity", "RecurTypeCode", "RevnAmountCurcyCode", "UnitPrice", "RevnAmount", "PriceTypeCode",
|
"Quantity", "RecurTypeCode", "RevnAmountCurcyCode", "UnitPrice", "RevnAmount", "PriceTypeCode",
|
||||||
@@ -96,9 +121,413 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === "opportunitiesExtension.requestConsumerTokenService") {
|
||||||
|
requestConsumerTokenService()
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
error: error.message || "Unable to load consumer token service.",
|
||||||
|
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "opportunitiesExtension.requestAccountPlans") {
|
||||||
|
requestAccountPlans(message)
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
error: error.message || "Unable to load account plans.",
|
||||||
|
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function requestConsumerTokenService() {
|
||||||
|
const debugRequests = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Pragma: "no-cache"
|
||||||
|
}
|
||||||
|
}, "requestConsumerTokenService", debugRequests);
|
||||||
|
const accountSummaryAuthorization = extractTokenServiceAuthorization(
|
||||||
|
tokenResponse,
|
||||||
|
"ACCP-TS",
|
||||||
|
"account-ti-authorization"
|
||||||
|
);
|
||||||
|
const accountPlansAuthorization = extractTokenServiceAuthorization(
|
||||||
|
tokenResponse,
|
||||||
|
"OCICONS-TS",
|
||||||
|
"g2m-authorization"
|
||||||
|
);
|
||||||
|
const tokenRequestDebugEntry = debugRequests[debugRequests.length - 1];
|
||||||
|
|
||||||
|
if (tokenRequestDebugEntry) {
|
||||||
|
tokenRequestDebugEntry.metadata = {
|
||||||
|
...tokenRequestDebugEntry.metadata,
|
||||||
|
accountSummaryTokenService: "ACCP-TS",
|
||||||
|
accountSummaryAuthorizationHeader: accountSummaryAuthorization.name || "(missing)",
|
||||||
|
accountSummaryAuthorizationValue: accountSummaryAuthorization.value
|
||||||
|
? `(present, ${accountSummaryAuthorization.value.length} characters)`
|
||||||
|
: "(missing)",
|
||||||
|
accountSummaryAuthorizationExpires: accountSummaryAuthorization.expires || "(not provided)",
|
||||||
|
accountPlansTokenService: "OCICONS-TS",
|
||||||
|
accountPlansAuthorizationHeader: accountPlansAuthorization.name || "(missing)",
|
||||||
|
accountPlansAuthorizationValue: accountPlansAuthorization.value
|
||||||
|
? `(present, ${accountPlansAuthorization.value.length} characters)`
|
||||||
|
: "(missing)",
|
||||||
|
accountPlansAuthorizationExpires: accountPlansAuthorization.expires || "(not provided)"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountSummaryAuthorization.value) {
|
||||||
|
throw new Error("ACCP-TS uiaasHeader.value was not found in requestConsumerTokenService.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountPlansAuthorization.value) {
|
||||||
|
throw new Error("OCICONS-TS uiaasHeader.value was not found in requestConsumerTokenService.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
authorizations: {
|
||||||
|
accountSummary: accountSummaryAuthorization,
|
||||||
|
accountPlans: accountPlansAuthorization
|
||||||
|
},
|
||||||
|
debugRequests
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
error.debugRequests = debugRequests;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestAccountPlans(message) {
|
||||||
|
const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : "";
|
||||||
|
const accountSummaryAuthorization = normalizeMessageAuthorization(
|
||||||
|
message.authorizations && message.authorizations.accountSummary,
|
||||||
|
"account-ti-authorization"
|
||||||
|
);
|
||||||
|
const accountPlansAuthorization = normalizeMessageAuthorization(
|
||||||
|
message.authorizations && message.authorizations.accountPlans,
|
||||||
|
"g2m-authorization"
|
||||||
|
);
|
||||||
|
const debugRequests = [];
|
||||||
|
|
||||||
|
if (!partyNumber) {
|
||||||
|
throw new Error("Customer Party Number is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountSummaryAuthorization.value) {
|
||||||
|
throw new Error("ACCP-TS authorization is unavailable. requestConsumerTokenService must complete first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountPlansAuthorization.value) {
|
||||||
|
throw new Error("OCICONS-TS authorization is unavailable. requestConsumerTokenService must complete first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const accountSummary = await fetchJson(ACCOUNT_SUMMARY_URL, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
[accountSummaryAuthorization.name]: accountSummaryAuthorization.value,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify([{ key: "regId", value: partyNumber }])
|
||||||
|
}, "requestAccountSummary", debugRequests);
|
||||||
|
const accountPlanId = findAccountPlanId(accountSummary);
|
||||||
|
|
||||||
|
if (!accountPlanId) {
|
||||||
|
throw new Error("Account plan id was not found in requestAccountSummary.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountPlansResponse = await fetchJson(`${ACCOUNT_PLANS_URL}/${encodeURIComponent(String(accountPlanId))}`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
[accountPlansAuthorization.name]: accountPlansAuthorization.value,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(ACCOUNT_PLANS_PAYLOAD)
|
||||||
|
}, "requestAccountPlans", debugRequests);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
accountPlanId: String(accountPlanId),
|
||||||
|
plans: extractAccountPlans(accountPlansResponse),
|
||||||
|
debugRequests
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
error.debugRequests = debugRequests;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMessageAuthorization(authorization, fallbackHeaderName) {
|
||||||
|
const headerName = authorization && typeof authorization.name === "string"
|
||||||
|
? authorization.name.trim()
|
||||||
|
: "";
|
||||||
|
const headerValue = authorization && typeof authorization.value === "string"
|
||||||
|
? authorization.value.trim()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: headerName || fallbackHeaderName,
|
||||||
|
value: headerValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTokenServiceAuthorization(response, tokenServiceName, fallbackHeaderName) {
|
||||||
|
const tokenService = Array.isArray(response)
|
||||||
|
? response.find((entry) => entry && entry.tokenService === tokenServiceName)
|
||||||
|
: findNestedObject(response, (entry) => entry.tokenService === tokenServiceName);
|
||||||
|
const authorizationHeader = tokenService && tokenService.uiaasHeader;
|
||||||
|
const headerName = authorizationHeader && typeof authorizationHeader.label === "string"
|
||||||
|
? authorizationHeader.label.trim()
|
||||||
|
: "";
|
||||||
|
const headerValue = authorizationHeader && typeof authorizationHeader.value === "string"
|
||||||
|
? authorizationHeader.value.trim()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: headerName || fallbackHeaderName,
|
||||||
|
value: headerValue,
|
||||||
|
expires: authorizationHeader && authorizationHeader.expires
|
||||||
|
? String(authorizationHeader.expires)
|
||||||
|
: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJson(url, options, requestName, debugRequests = null) {
|
||||||
|
const startedAt = new Date();
|
||||||
|
const debugEntry = createBackgroundDebugEntry(requestName, url, options, startedAt);
|
||||||
|
let response;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await fetch(url, options);
|
||||||
|
} catch (error) {
|
||||||
|
completeBackgroundDebugEntry(debugEntry, null, "", error.message || "Network request failed.");
|
||||||
|
appendBackgroundDebugEntry(debugRequests, debugEntry);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responsePayload = await readResponsePayload(response);
|
||||||
|
const responseBody = createDebugBody(responsePayload.text);
|
||||||
|
completeBackgroundDebugEntry(debugEntry, response, responseBody, "");
|
||||||
|
debugEntry.metadata.responseContentType = response.headers.get("content-type") || "(not provided)";
|
||||||
|
debugEntry.metadata.responseLength = String(responsePayload.text.length);
|
||||||
|
appendBackgroundDebugEntry(debugRequests, debugEntry);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`${requestName} failed with status ${response.status}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responsePayload.error) {
|
||||||
|
throw new Error(`${requestName} response body could not be read.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseJsonResponse(responsePayload.text, requestName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBackgroundDebugEntry(requestName, url, options, startedAt) {
|
||||||
|
return {
|
||||||
|
name: requestName,
|
||||||
|
method: options.method || "GET",
|
||||||
|
url,
|
||||||
|
status: "pending",
|
||||||
|
startedAt: startedAt.toISOString(),
|
||||||
|
completedAt: null,
|
||||||
|
durationMs: null,
|
||||||
|
httpStatus: null,
|
||||||
|
statusText: "",
|
||||||
|
requestHeaders: normalizeDebugHeaders(options.headers),
|
||||||
|
requestBody: formatDebugRequestBody(options.body),
|
||||||
|
metadata: {
|
||||||
|
executionContext: "background"
|
||||||
|
},
|
||||||
|
responseHeaders: {},
|
||||||
|
responseBody: "",
|
||||||
|
responseBodyTruncated: false,
|
||||||
|
error: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function completeBackgroundDebugEntry(entry, response, responseBody, errorMessage) {
|
||||||
|
const completedAt = new Date();
|
||||||
|
entry.completedAt = completedAt.toISOString();
|
||||||
|
entry.durationMs = completedAt.getTime() - new Date(entry.startedAt).getTime();
|
||||||
|
entry.error = errorMessage;
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
entry.status = "failed";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.status = response.ok ? "success" : "failed";
|
||||||
|
entry.httpStatus = response.status;
|
||||||
|
entry.statusText = response.statusText || "";
|
||||||
|
entry.responseHeaders = headersToObject(response.headers);
|
||||||
|
entry.responseBody = responseBody.value;
|
||||||
|
entry.responseBodyTruncated = responseBody.truncated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendBackgroundDebugEntry(debugRequests, entry) {
|
||||||
|
if (Array.isArray(debugRequests)) {
|
||||||
|
debugRequests.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readResponsePayload(response) {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
text: await response.text(),
|
||||||
|
error: ""
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
text: `Unable to read response body: ${error.message || "unknown error"}`,
|
||||||
|
error: error.message || "unknown error"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonResponse(text, requestName) {
|
||||||
|
const normalizedText = String(text)
|
||||||
|
.replace(/^\uFEFF/, "")
|
||||||
|
.trimStart()
|
||||||
|
.replace(/^\)\]\}',?\s*/, "")
|
||||||
|
.replace(/^while\s*\(1\);\s*/, "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(normalizedText);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`${requestName} returned an invalid JSON response.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDebugBody(text) {
|
||||||
|
if (text.length > DEBUG_BODY_LIMIT) {
|
||||||
|
return {
|
||||||
|
value: `${text.slice(0, DEBUG_BODY_LIMIT)}\n... truncated ${text.length - DEBUG_BODY_LIMIT} characters`,
|
||||||
|
truncated: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: text || "(empty response body)",
|
||||||
|
truncated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDebugHeaders(headers) {
|
||||||
|
const normalized = {};
|
||||||
|
|
||||||
|
Object.entries(headers || {}).forEach(([key, value]) => {
|
||||||
|
normalized[key] = String(value);
|
||||||
|
});
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function headersToObject(headers) {
|
||||||
|
const values = {};
|
||||||
|
|
||||||
|
if (headers && typeof headers.forEach === "function") {
|
||||||
|
headers.forEach((value, key) => {
|
||||||
|
values[key] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDebugRequestBody(body) {
|
||||||
|
if (!body) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(body), null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
return String(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNestedObject(value, predicate, visited = new Set()) {
|
||||||
|
if (!value || typeof value !== "object" || visited.has(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.add(value);
|
||||||
|
|
||||||
|
if (!Array.isArray(value) && predicate(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const child of Object.values(value)) {
|
||||||
|
const match = findNestedObject(child, predicate, visited);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAccountPlanId(response) {
|
||||||
|
const account = findNestedObject(response, (value) => (
|
||||||
|
Object.prototype.hasOwnProperty.call(value, "id")
|
||||||
|
&& value.id !== null
|
||||||
|
&& value.id !== undefined
|
||||||
|
&& value.id !== ""
|
||||||
|
));
|
||||||
|
return account ? account.id : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAccountPlans(response) {
|
||||||
|
const plans = [];
|
||||||
|
const visited = new Set();
|
||||||
|
|
||||||
|
const visit = (value) => {
|
||||||
|
if (!value || typeof value !== "object" || visited.has(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.add(value);
|
||||||
|
|
||||||
|
if (!Array.isArray(value) && (
|
||||||
|
Object.prototype.hasOwnProperty.call(value, "planType")
|
||||||
|
|| Object.prototype.hasOwnProperty.call(value, "subPlanNum")
|
||||||
|
)) {
|
||||||
|
plans.push(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.values(value).forEach(visit);
|
||||||
|
};
|
||||||
|
|
||||||
|
visit(response);
|
||||||
|
return plans;
|
||||||
|
}
|
||||||
|
|
||||||
async function requestOpptyProducts(message, sender) {
|
async function requestOpptyProducts(message, sender) {
|
||||||
const requestId = typeof message.requestId === "string" ? message.requestId : "";
|
const requestId = typeof message.requestId === "string" ? message.requestId : "";
|
||||||
const accessToken = typeof message.accessToken === "string" ? message.accessToken : "";
|
const accessToken = typeof message.accessToken === "string" ? message.accessToken : "";
|
||||||
|
|||||||
843
dist/firefox/content.js
vendored
843
dist/firefox/content.js
vendored
File diff suppressed because it is too large
Load Diff
3
dist/firefox/manifest.json
vendored
3
dist/firefox/manifest.json
vendored
@@ -8,7 +8,8 @@
|
|||||||
"storage"
|
"storage"
|
||||||
],
|
],
|
||||||
"host_permissions": [
|
"host_permissions": [
|
||||||
"https://eeho.fa.us2.oraclecloud.com/*"
|
"https://eeho.fa.us2.oraclecloud.com/*",
|
||||||
|
"https://spa.oracle.com/*"
|
||||||
],
|
],
|
||||||
"content_scripts": [
|
"content_scripts": [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -19,7 +19,8 @@ const baseManifest = {
|
|||||||
"storage"
|
"storage"
|
||||||
],
|
],
|
||||||
host_permissions: [
|
host_permissions: [
|
||||||
"https://eeho.fa.us2.oraclecloud.com/*"
|
"https://eeho.fa.us2.oraclecloud.com/*",
|
||||||
|
"https://spa.oracle.com/*"
|
||||||
],
|
],
|
||||||
content_scripts: [
|
content_scripts: [
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -17,6 +17,31 @@
|
|||||||
const OPPTY_PRODUCTS_PAGE_LIMIT = 15;
|
const OPPTY_PRODUCTS_PAGE_LIMIT = 15;
|
||||||
const OPPTY_PRODUCTS_MAX_PAGES = 250;
|
const OPPTY_PRODUCTS_MAX_PAGES = 250;
|
||||||
const OPPTY_PRODUCTS_CONCURRENCY = 4;
|
const OPPTY_PRODUCTS_CONCURRENCY = 4;
|
||||||
|
const CUSTOMER_TOKEN_SERVICE_URL = "https://spa.oracle.com/oalcrm/web/api/g2m-consumer-application/consumerTokenService";
|
||||||
|
const ACCOUNT_SUMMARY_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-account/service/accountsummary";
|
||||||
|
const ACCOUNT_PLANS_URL = "https://spa.oracle.com/oalcrm/web/api/provider-proxy/g2m-consumption/service/v1/account/accountPlans";
|
||||||
|
const ACCOUNT_PLANS_PAYLOAD = {
|
||||||
|
hierarchy: "single",
|
||||||
|
products: "all",
|
||||||
|
language: "english",
|
||||||
|
planStatus: [
|
||||||
|
"active",
|
||||||
|
"awaiting_provisioning",
|
||||||
|
"awaiting_provisioning_error",
|
||||||
|
"draft",
|
||||||
|
"expired",
|
||||||
|
"final_billing",
|
||||||
|
"onhold",
|
||||||
|
"provisioned",
|
||||||
|
"signed",
|
||||||
|
"superseded",
|
||||||
|
"superseded_pending_final_billing",
|
||||||
|
"suspended",
|
||||||
|
"terminated",
|
||||||
|
"terminated_pending_final_billing"
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const DEBUG_BODY_LIMIT = 100000;
|
||||||
const OPPTY_PRODUCTS_FIELDS = [
|
const OPPTY_PRODUCTS_FIELDS = [
|
||||||
"ProductType", "Description", "InventoryItemId", "ProdGroupName", "ProdGroupId", "OwnerLockAsgnFlag",
|
"ProductType", "Description", "InventoryItemId", "ProdGroupName", "ProdGroupId", "OwnerLockAsgnFlag",
|
||||||
"Quantity", "RecurTypeCode", "RevnAmountCurcyCode", "UnitPrice", "RevnAmount", "PriceTypeCode",
|
"Quantity", "RecurTypeCode", "RevnAmountCurcyCode", "UnitPrice", "RevnAmount", "PriceTypeCode",
|
||||||
@@ -96,9 +121,413 @@
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (message.type === "opportunitiesExtension.requestConsumerTokenService") {
|
||||||
|
requestConsumerTokenService()
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
error: error.message || "Unable to load consumer token service.",
|
||||||
|
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (message.type === "opportunitiesExtension.requestAccountPlans") {
|
||||||
|
requestAccountPlans(message)
|
||||||
|
.then(sendResponse)
|
||||||
|
.catch((error) => {
|
||||||
|
sendResponse({
|
||||||
|
ok: false,
|
||||||
|
error: error.message || "Unable to load account plans.",
|
||||||
|
debugRequests: Array.isArray(error.debugRequests) ? error.debugRequests : []
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
async function requestConsumerTokenService() {
|
||||||
|
const debugRequests = [];
|
||||||
|
|
||||||
|
try {
|
||||||
|
const tokenResponse = await fetchJson(CUSTOMER_TOKEN_SERVICE_URL, {
|
||||||
|
method: "GET",
|
||||||
|
credentials: "include",
|
||||||
|
cache: "no-store",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
"Cache-Control": "no-cache",
|
||||||
|
Pragma: "no-cache"
|
||||||
|
}
|
||||||
|
}, "requestConsumerTokenService", debugRequests);
|
||||||
|
const accountSummaryAuthorization = extractTokenServiceAuthorization(
|
||||||
|
tokenResponse,
|
||||||
|
"ACCP-TS",
|
||||||
|
"account-ti-authorization"
|
||||||
|
);
|
||||||
|
const accountPlansAuthorization = extractTokenServiceAuthorization(
|
||||||
|
tokenResponse,
|
||||||
|
"OCICONS-TS",
|
||||||
|
"g2m-authorization"
|
||||||
|
);
|
||||||
|
const tokenRequestDebugEntry = debugRequests[debugRequests.length - 1];
|
||||||
|
|
||||||
|
if (tokenRequestDebugEntry) {
|
||||||
|
tokenRequestDebugEntry.metadata = {
|
||||||
|
...tokenRequestDebugEntry.metadata,
|
||||||
|
accountSummaryTokenService: "ACCP-TS",
|
||||||
|
accountSummaryAuthorizationHeader: accountSummaryAuthorization.name || "(missing)",
|
||||||
|
accountSummaryAuthorizationValue: accountSummaryAuthorization.value
|
||||||
|
? `(present, ${accountSummaryAuthorization.value.length} characters)`
|
||||||
|
: "(missing)",
|
||||||
|
accountSummaryAuthorizationExpires: accountSummaryAuthorization.expires || "(not provided)",
|
||||||
|
accountPlansTokenService: "OCICONS-TS",
|
||||||
|
accountPlansAuthorizationHeader: accountPlansAuthorization.name || "(missing)",
|
||||||
|
accountPlansAuthorizationValue: accountPlansAuthorization.value
|
||||||
|
? `(present, ${accountPlansAuthorization.value.length} characters)`
|
||||||
|
: "(missing)",
|
||||||
|
accountPlansAuthorizationExpires: accountPlansAuthorization.expires || "(not provided)"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountSummaryAuthorization.value) {
|
||||||
|
throw new Error("ACCP-TS uiaasHeader.value was not found in requestConsumerTokenService.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountPlansAuthorization.value) {
|
||||||
|
throw new Error("OCICONS-TS uiaasHeader.value was not found in requestConsumerTokenService.");
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
authorizations: {
|
||||||
|
accountSummary: accountSummaryAuthorization,
|
||||||
|
accountPlans: accountPlansAuthorization
|
||||||
|
},
|
||||||
|
debugRequests
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
error.debugRequests = debugRequests;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function requestAccountPlans(message) {
|
||||||
|
const partyNumber = typeof message.partyNumber === "string" ? message.partyNumber.trim() : "";
|
||||||
|
const accountSummaryAuthorization = normalizeMessageAuthorization(
|
||||||
|
message.authorizations && message.authorizations.accountSummary,
|
||||||
|
"account-ti-authorization"
|
||||||
|
);
|
||||||
|
const accountPlansAuthorization = normalizeMessageAuthorization(
|
||||||
|
message.authorizations && message.authorizations.accountPlans,
|
||||||
|
"g2m-authorization"
|
||||||
|
);
|
||||||
|
const debugRequests = [];
|
||||||
|
|
||||||
|
if (!partyNumber) {
|
||||||
|
throw new Error("Customer Party Number is required.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountSummaryAuthorization.value) {
|
||||||
|
throw new Error("ACCP-TS authorization is unavailable. requestConsumerTokenService must complete first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!accountPlansAuthorization.value) {
|
||||||
|
throw new Error("OCICONS-TS authorization is unavailable. requestConsumerTokenService must complete first.");
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const accountSummary = await fetchJson(ACCOUNT_SUMMARY_URL, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
[accountSummaryAuthorization.name]: accountSummaryAuthorization.value,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify([{ key: "regId", value: partyNumber }])
|
||||||
|
}, "requestAccountSummary", debugRequests);
|
||||||
|
const accountPlanId = findAccountPlanId(accountSummary);
|
||||||
|
|
||||||
|
if (!accountPlanId) {
|
||||||
|
throw new Error("Account plan id was not found in requestAccountSummary.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const accountPlansResponse = await fetchJson(`${ACCOUNT_PLANS_URL}/${encodeURIComponent(String(accountPlanId))}`, {
|
||||||
|
method: "POST",
|
||||||
|
credentials: "include",
|
||||||
|
headers: {
|
||||||
|
Accept: "application/json",
|
||||||
|
[accountPlansAuthorization.name]: accountPlansAuthorization.value,
|
||||||
|
"Content-Type": "application/json"
|
||||||
|
},
|
||||||
|
body: JSON.stringify(ACCOUNT_PLANS_PAYLOAD)
|
||||||
|
}, "requestAccountPlans", debugRequests);
|
||||||
|
|
||||||
|
return {
|
||||||
|
ok: true,
|
||||||
|
accountPlanId: String(accountPlanId),
|
||||||
|
plans: extractAccountPlans(accountPlansResponse),
|
||||||
|
debugRequests
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
error.debugRequests = debugRequests;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMessageAuthorization(authorization, fallbackHeaderName) {
|
||||||
|
const headerName = authorization && typeof authorization.name === "string"
|
||||||
|
? authorization.name.trim()
|
||||||
|
: "";
|
||||||
|
const headerValue = authorization && typeof authorization.value === "string"
|
||||||
|
? authorization.value.trim()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: headerName || fallbackHeaderName,
|
||||||
|
value: headerValue
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractTokenServiceAuthorization(response, tokenServiceName, fallbackHeaderName) {
|
||||||
|
const tokenService = Array.isArray(response)
|
||||||
|
? response.find((entry) => entry && entry.tokenService === tokenServiceName)
|
||||||
|
: findNestedObject(response, (entry) => entry.tokenService === tokenServiceName);
|
||||||
|
const authorizationHeader = tokenService && tokenService.uiaasHeader;
|
||||||
|
const headerName = authorizationHeader && typeof authorizationHeader.label === "string"
|
||||||
|
? authorizationHeader.label.trim()
|
||||||
|
: "";
|
||||||
|
const headerValue = authorizationHeader && typeof authorizationHeader.value === "string"
|
||||||
|
? authorizationHeader.value.trim()
|
||||||
|
: "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
name: headerName || fallbackHeaderName,
|
||||||
|
value: headerValue,
|
||||||
|
expires: authorizationHeader && authorizationHeader.expires
|
||||||
|
? String(authorizationHeader.expires)
|
||||||
|
: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
async function fetchJson(url, options, requestName, debugRequests = null) {
|
||||||
|
const startedAt = new Date();
|
||||||
|
const debugEntry = createBackgroundDebugEntry(requestName, url, options, startedAt);
|
||||||
|
let response;
|
||||||
|
|
||||||
|
try {
|
||||||
|
response = await fetch(url, options);
|
||||||
|
} catch (error) {
|
||||||
|
completeBackgroundDebugEntry(debugEntry, null, "", error.message || "Network request failed.");
|
||||||
|
appendBackgroundDebugEntry(debugRequests, debugEntry);
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
|
||||||
|
const responsePayload = await readResponsePayload(response);
|
||||||
|
const responseBody = createDebugBody(responsePayload.text);
|
||||||
|
completeBackgroundDebugEntry(debugEntry, response, responseBody, "");
|
||||||
|
debugEntry.metadata.responseContentType = response.headers.get("content-type") || "(not provided)";
|
||||||
|
debugEntry.metadata.responseLength = String(responsePayload.text.length);
|
||||||
|
appendBackgroundDebugEntry(debugRequests, debugEntry);
|
||||||
|
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`${requestName} failed with status ${response.status}.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responsePayload.error) {
|
||||||
|
throw new Error(`${requestName} response body could not be read.`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parseJsonResponse(responsePayload.text, requestName);
|
||||||
|
}
|
||||||
|
|
||||||
|
function createBackgroundDebugEntry(requestName, url, options, startedAt) {
|
||||||
|
return {
|
||||||
|
name: requestName,
|
||||||
|
method: options.method || "GET",
|
||||||
|
url,
|
||||||
|
status: "pending",
|
||||||
|
startedAt: startedAt.toISOString(),
|
||||||
|
completedAt: null,
|
||||||
|
durationMs: null,
|
||||||
|
httpStatus: null,
|
||||||
|
statusText: "",
|
||||||
|
requestHeaders: normalizeDebugHeaders(options.headers),
|
||||||
|
requestBody: formatDebugRequestBody(options.body),
|
||||||
|
metadata: {
|
||||||
|
executionContext: "background"
|
||||||
|
},
|
||||||
|
responseHeaders: {},
|
||||||
|
responseBody: "",
|
||||||
|
responseBodyTruncated: false,
|
||||||
|
error: ""
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function completeBackgroundDebugEntry(entry, response, responseBody, errorMessage) {
|
||||||
|
const completedAt = new Date();
|
||||||
|
entry.completedAt = completedAt.toISOString();
|
||||||
|
entry.durationMs = completedAt.getTime() - new Date(entry.startedAt).getTime();
|
||||||
|
entry.error = errorMessage;
|
||||||
|
|
||||||
|
if (!response) {
|
||||||
|
entry.status = "failed";
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
entry.status = response.ok ? "success" : "failed";
|
||||||
|
entry.httpStatus = response.status;
|
||||||
|
entry.statusText = response.statusText || "";
|
||||||
|
entry.responseHeaders = headersToObject(response.headers);
|
||||||
|
entry.responseBody = responseBody.value;
|
||||||
|
entry.responseBodyTruncated = responseBody.truncated;
|
||||||
|
}
|
||||||
|
|
||||||
|
function appendBackgroundDebugEntry(debugRequests, entry) {
|
||||||
|
if (Array.isArray(debugRequests)) {
|
||||||
|
debugRequests.push(entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function readResponsePayload(response) {
|
||||||
|
try {
|
||||||
|
return {
|
||||||
|
text: await response.text(),
|
||||||
|
error: ""
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
return {
|
||||||
|
text: `Unable to read response body: ${error.message || "unknown error"}`,
|
||||||
|
error: error.message || "unknown error"
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function parseJsonResponse(text, requestName) {
|
||||||
|
const normalizedText = String(text)
|
||||||
|
.replace(/^\uFEFF/, "")
|
||||||
|
.trimStart()
|
||||||
|
.replace(/^\)\]\}',?\s*/, "")
|
||||||
|
.replace(/^while\s*\(1\);\s*/, "");
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.parse(normalizedText);
|
||||||
|
} catch (error) {
|
||||||
|
throw new Error(`${requestName} returned an invalid JSON response.`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function createDebugBody(text) {
|
||||||
|
if (text.length > DEBUG_BODY_LIMIT) {
|
||||||
|
return {
|
||||||
|
value: `${text.slice(0, DEBUG_BODY_LIMIT)}\n... truncated ${text.length - DEBUG_BODY_LIMIT} characters`,
|
||||||
|
truncated: true
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
value: text || "(empty response body)",
|
||||||
|
truncated: false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeDebugHeaders(headers) {
|
||||||
|
const normalized = {};
|
||||||
|
|
||||||
|
Object.entries(headers || {}).forEach(([key, value]) => {
|
||||||
|
normalized[key] = String(value);
|
||||||
|
});
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
|
function headersToObject(headers) {
|
||||||
|
const values = {};
|
||||||
|
|
||||||
|
if (headers && typeof headers.forEach === "function") {
|
||||||
|
headers.forEach((value, key) => {
|
||||||
|
values[key] = value;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return values;
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatDebugRequestBody(body) {
|
||||||
|
if (!body) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return JSON.stringify(JSON.parse(body), null, 2);
|
||||||
|
} catch (error) {
|
||||||
|
return String(body);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function findNestedObject(value, predicate, visited = new Set()) {
|
||||||
|
if (!value || typeof value !== "object" || visited.has(value)) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.add(value);
|
||||||
|
|
||||||
|
if (!Array.isArray(value) && predicate(value)) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const child of Object.values(value)) {
|
||||||
|
const match = findNestedObject(child, predicate, visited);
|
||||||
|
|
||||||
|
if (match) {
|
||||||
|
return match;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findAccountPlanId(response) {
|
||||||
|
const account = findNestedObject(response, (value) => (
|
||||||
|
Object.prototype.hasOwnProperty.call(value, "id")
|
||||||
|
&& value.id !== null
|
||||||
|
&& value.id !== undefined
|
||||||
|
&& value.id !== ""
|
||||||
|
));
|
||||||
|
return account ? account.id : "";
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractAccountPlans(response) {
|
||||||
|
const plans = [];
|
||||||
|
const visited = new Set();
|
||||||
|
|
||||||
|
const visit = (value) => {
|
||||||
|
if (!value || typeof value !== "object" || visited.has(value)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
visited.add(value);
|
||||||
|
|
||||||
|
if (!Array.isArray(value) && (
|
||||||
|
Object.prototype.hasOwnProperty.call(value, "planType")
|
||||||
|
|| Object.prototype.hasOwnProperty.call(value, "subPlanNum")
|
||||||
|
)) {
|
||||||
|
plans.push(value);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Object.values(value).forEach(visit);
|
||||||
|
};
|
||||||
|
|
||||||
|
visit(response);
|
||||||
|
return plans;
|
||||||
|
}
|
||||||
|
|
||||||
async function requestOpptyProducts(message, sender) {
|
async function requestOpptyProducts(message, sender) {
|
||||||
const requestId = typeof message.requestId === "string" ? message.requestId : "";
|
const requestId = typeof message.requestId === "string" ? message.requestId : "";
|
||||||
const accessToken = typeof message.accessToken === "string" ? message.accessToken : "";
|
const accessToken = typeof message.accessToken === "string" ? message.accessToken : "";
|
||||||
|
|||||||
843
src/content.js
843
src/content.js
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user