Implemented the Account Plan request from the SPA

This commit is contained in:
2026-09-07 14:10:29 -03:00
parent e4cfb3fa9c
commit f0fb99d1ef
10 changed files with 3802 additions and 72 deletions

View File

@@ -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.
- Opportunity type view preferences persisted in the browser.
- 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.
- Separate outputs for Chromium/Edge and Firefox.

View File

@@ -17,6 +17,31 @@
const OPPTY_PRODUCTS_PAGE_LIMIT = 15;
const OPPTY_PRODUCTS_MAX_PAGES = 250;
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 = [
"ProductType", "Description", "InventoryItemId", "ProdGroupName", "ProdGroupId", "OwnerLockAsgnFlag",
"Quantity", "RecurTypeCode", "RevnAmountCurcyCode", "UnitPrice", "RevnAmount", "PriceTypeCode",
@@ -96,9 +121,413 @@
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;
});
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) {
const requestId = typeof message.requestId === "string" ? message.requestId : "";
const accessToken = typeof message.accessToken === "string" ? message.accessToken : "";

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,8 @@
"storage"
],
"host_permissions": [
"https://eeho.fa.us2.oraclecloud.com/*"
"https://eeho.fa.us2.oraclecloud.com/*",
"https://spa.oracle.com/*"
],
"content_scripts": [
{

View File

@@ -17,6 +17,31 @@
const OPPTY_PRODUCTS_PAGE_LIMIT = 15;
const OPPTY_PRODUCTS_MAX_PAGES = 250;
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 = [
"ProductType", "Description", "InventoryItemId", "ProdGroupName", "ProdGroupId", "OwnerLockAsgnFlag",
"Quantity", "RecurTypeCode", "RevnAmountCurcyCode", "UnitPrice", "RevnAmount", "PriceTypeCode",
@@ -96,9 +121,413 @@
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;
});
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) {
const requestId = typeof message.requestId === "string" ? message.requestId : "";
const accessToken = typeof message.accessToken === "string" ? message.accessToken : "";

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,8 @@
"storage"
],
"host_permissions": [
"https://eeho.fa.us2.oraclecloud.com/*"
"https://eeho.fa.us2.oraclecloud.com/*",
"https://spa.oracle.com/*"
],
"content_scripts": [
{

View File

@@ -19,7 +19,8 @@ const baseManifest = {
"storage"
],
host_permissions: [
"https://eeho.fa.us2.oraclecloud.com/*"
"https://eeho.fa.us2.oraclecloud.com/*",
"https://spa.oracle.com/*"
],
content_scripts: [
{

View File

@@ -17,6 +17,31 @@
const OPPTY_PRODUCTS_PAGE_LIMIT = 15;
const OPPTY_PRODUCTS_MAX_PAGES = 250;
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 = [
"ProductType", "Description", "InventoryItemId", "ProdGroupName", "ProdGroupId", "OwnerLockAsgnFlag",
"Quantity", "RecurTypeCode", "RevnAmountCurcyCode", "UnitPrice", "RevnAmount", "PriceTypeCode",
@@ -96,9 +121,413 @@
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;
});
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) {
const requestId = typeof message.requestId === "string" ? message.requestId : "";
const accessToken = typeof message.accessToken === "string" ? message.accessToken : "";

File diff suppressed because it is too large Load Diff