Implementation of the opportunity product view
This commit is contained in:
314
dist/chromium/background.js
vendored
314
dist/chromium/background.js
vendored
@@ -13,7 +13,28 @@
|
||||
const OPPORTUNITIES_LIST_URL = "https://eeho.fa.us2.oraclecloud.com/fscmUI/redwood/cx-sales/application/container/opportunities/opportunities-list";
|
||||
const COOKIE_REFRESH_TIMEOUT_MS = 15000;
|
||||
const COOKIE_REFRESH_POLL_MS = 250;
|
||||
const OPPTY_PRODUCTS_CACHE_KEY = "opportunitiesExtension.opptyProductsCache.v1";
|
||||
const OPPTY_PRODUCTS_PAGE_LIMIT = 15;
|
||||
const OPPTY_PRODUCTS_MAX_PAGES = 250;
|
||||
const OPPTY_PRODUCTS_CONCURRENCY = 4;
|
||||
const OPPTY_PRODUCTS_FIELDS = [
|
||||
"ProductType", "Description", "InventoryItemId", "ProdGroupName", "ProdGroupId", "OwnerLockAsgnFlag",
|
||||
"Quantity", "RecurTypeCode", "RevnAmountCurcyCode", "UnitPrice", "RevnAmount", "PriceTypeCode",
|
||||
"EffectiveDate", "Name1", "NonRecurringRevenue", "OpportunityOwnerPartyName", "OpportunityOwnerResourcePartyId",
|
||||
"OptyId", "OptyNumber", "PartyName2", "PrTerritoryVersionId", "PrTerritoryVersionIdForManual",
|
||||
"RecurEndDate", "RecurFrequencyCode", "RecurNumberPeriods", "RecurRevenue", "ResourcePartyId", "RevnId",
|
||||
"RevnNumber", "SplitPercent", "SalesCreditTypeCode", "SplitTypeCode", "TerrOwnerPartyName", "UpsideAmount",
|
||||
"UsageRevenue", "StatusCode", "BUOrgId", "TypeCode", "WinProb", "ForecastType_c", "ARRLocalCurrency_c",
|
||||
"CPQLastSyncDate_c", "CPQOperationType_c", "ProdGLID_c", "ProposalNumber_c", "QuoteNumber_c",
|
||||
"ServicesPeriod_c", "CPQIntLastUpdatedBy_c", "CPQUpdatedAmount_c", "CPQUpdatedQuantity_c",
|
||||
"CPQUpdatedRevnType_c", "CPQUpdatedServicePeriod_c", "CPQUpdatedStatus_c", "OrderNumber_c", "PrevAmount_c",
|
||||
"PrevQuantity_c", "PrevRevenueType_c", "PrevServicePeriod_c", "PrevStatus_c", "SubscriptionID_c",
|
||||
"WorkloadName_c", "ConsumptionStartDate_c", "RampMonths_c"
|
||||
];
|
||||
const runtimeApi = typeof browser !== "undefined" ? browser : chrome;
|
||||
let productsCachePromise = null;
|
||||
let productsCacheWritePromise = Promise.resolve();
|
||||
const inFlightProducts = new Map();
|
||||
|
||||
runtimeApi.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
||||
if (!message || !message.type) {
|
||||
@@ -61,9 +82,237 @@
|
||||
return true;
|
||||
}
|
||||
|
||||
if (message.type === "opportunitiesExtension.requestOpptyProducts") {
|
||||
requestOpptyProducts(message, sender)
|
||||
.then(sendResponse)
|
||||
.catch((error) => {
|
||||
sendResponse({
|
||||
ok: false,
|
||||
requestId: message.requestId || "",
|
||||
error: error.message || "Unable to load opportunity products."
|
||||
});
|
||||
});
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
});
|
||||
|
||||
async function requestOpptyProducts(message, sender) {
|
||||
const requestId = typeof message.requestId === "string" ? message.requestId : "";
|
||||
const accessToken = typeof message.accessToken === "string" ? message.accessToken : "";
|
||||
const cacheMaxAgeMs = normalizeCacheMaxAge(message.cacheMaxAgeMs);
|
||||
const optyNumbers = Array.from(new Set(
|
||||
(Array.isArray(message.optyNumbers) ? message.optyNumbers : [])
|
||||
.filter(Boolean)
|
||||
.map(String)
|
||||
));
|
||||
|
||||
if (!requestId || !accessToken) {
|
||||
throw new Error("A request id and access token are required for requestOpptyProducts.");
|
||||
}
|
||||
|
||||
let nextIndex = 0;
|
||||
let completed = 0;
|
||||
let failed = 0;
|
||||
let cacheHits = 0;
|
||||
|
||||
const runWorker = async () => {
|
||||
while (nextIndex < optyNumbers.length) {
|
||||
const index = nextIndex;
|
||||
nextIndex += 1;
|
||||
const optyNumber = optyNumbers[index];
|
||||
|
||||
try {
|
||||
const result = await getOpptyProducts(optyNumber, accessToken, cacheMaxAgeMs);
|
||||
completed += 1;
|
||||
|
||||
if (result.fromCache) {
|
||||
cacheHits += 1;
|
||||
}
|
||||
|
||||
await sendProductsProgress(sender, {
|
||||
type: "opportunitiesExtension.opptyProductsProgress",
|
||||
requestId,
|
||||
state: "running",
|
||||
optyNumber,
|
||||
items: result.items,
|
||||
fromCache: result.fromCache,
|
||||
cachedAt: result.cachedAt,
|
||||
completed,
|
||||
total: optyNumbers.length,
|
||||
failed,
|
||||
cacheHits
|
||||
});
|
||||
} catch (error) {
|
||||
completed += 1;
|
||||
failed += 1;
|
||||
await sendProductsProgress(sender, {
|
||||
type: "opportunitiesExtension.opptyProductsProgress",
|
||||
requestId,
|
||||
state: "running",
|
||||
optyNumber,
|
||||
items: [],
|
||||
error: error.message || "Unable to load products.",
|
||||
completed,
|
||||
total: optyNumbers.length,
|
||||
failed,
|
||||
cacheHits
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const workerCount = Math.min(OPPTY_PRODUCTS_CONCURRENCY, Math.max(optyNumbers.length, 1));
|
||||
await Promise.all(Array.from({ length: workerCount }, runWorker));
|
||||
|
||||
const state = failed > 0 ? "complete-with-errors" : "complete";
|
||||
await sendProductsProgress(sender, {
|
||||
type: "opportunitiesExtension.opptyProductsProgress",
|
||||
requestId,
|
||||
state,
|
||||
completed,
|
||||
total: optyNumbers.length,
|
||||
failed,
|
||||
cacheHits
|
||||
});
|
||||
|
||||
return {
|
||||
ok: failed === 0,
|
||||
requestId,
|
||||
completed,
|
||||
total: optyNumbers.length,
|
||||
failed,
|
||||
cacheHits
|
||||
};
|
||||
}
|
||||
|
||||
async function getOpptyProducts(optyNumber, accessToken, cacheMaxAgeMs) {
|
||||
const cache = await getProductsCache();
|
||||
const cachedEntry = cache[optyNumber];
|
||||
|
||||
if (cachedEntry && Array.isArray(cachedEntry.items) && Date.now() - Number(cachedEntry.fetchedAt) < cacheMaxAgeMs) {
|
||||
return { items: cachedEntry.items, fromCache: true, cachedAt: Number(cachedEntry.fetchedAt) };
|
||||
}
|
||||
|
||||
if (!inFlightProducts.has(optyNumber)) {
|
||||
const request = fetchAllOpptyProducts(optyNumber, accessToken)
|
||||
.then(async (items) => {
|
||||
const fetchedAt = Date.now();
|
||||
cache[optyNumber] = {
|
||||
fetchedAt,
|
||||
items
|
||||
};
|
||||
await saveProductsCache(cache).catch(() => {});
|
||||
return { items, fetchedAt };
|
||||
})
|
||||
.finally(() => {
|
||||
inFlightProducts.delete(optyNumber);
|
||||
});
|
||||
inFlightProducts.set(optyNumber, request);
|
||||
}
|
||||
|
||||
const fetchedResult = await inFlightProducts.get(optyNumber);
|
||||
return {
|
||||
items: fetchedResult.items,
|
||||
cachedAt: fetchedResult.fetchedAt,
|
||||
fromCache: false
|
||||
};
|
||||
}
|
||||
|
||||
async function fetchAllOpptyProducts(optyNumber, accessToken) {
|
||||
let offset = 0;
|
||||
let page = 0;
|
||||
const items = [];
|
||||
|
||||
while (page < OPPTY_PRODUCTS_MAX_PAGES) {
|
||||
const response = await fetch(createOpptyProductsUrl(optyNumber, offset), {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${accessToken}`
|
||||
}
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`requestOpptyProducts failed with status ${response.status}.`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
const pageItems = Array.isArray(responseData.items) ? responseData.items : [];
|
||||
items.push(...pageItems);
|
||||
page += 1;
|
||||
|
||||
if (!responseData.hasMore) {
|
||||
return items;
|
||||
}
|
||||
|
||||
const responseOffset = Number(responseData.offset);
|
||||
const responseCount = Number(responseData.count);
|
||||
const receivedCount = Number.isFinite(responseCount) ? responseCount : pageItems.length;
|
||||
const currentOffset = Number.isFinite(responseOffset) ? responseOffset : offset;
|
||||
|
||||
if (receivedCount <= 0) {
|
||||
throw new Error("requestOpptyProducts returned hasMore without additional results.");
|
||||
}
|
||||
|
||||
offset = currentOffset + receivedCount;
|
||||
}
|
||||
|
||||
throw new Error("requestOpptyProducts exceeded the pagination safety limit.");
|
||||
}
|
||||
|
||||
function createOpptyProductsUrl(optyNumber, offset) {
|
||||
const url = new URL(`https://${ORACLE_DOMAIN}/crmRestApi/rest/rv:be91c002-2e5d-4ed2-a37e-e0c837bf141f/en/11.13.18.05:9/opportunities/${encodeURIComponent(optyNumber)}/child/ChildRevenue`);
|
||||
url.searchParams.set("onlyData", "true");
|
||||
url.searchParams.set("q", "(SplitTypeCode!='DETAILCHILDSPLIT') AND (RecurTypeCode!='CHILDRECUR')");
|
||||
url.searchParams.set("totalResults", "false");
|
||||
url.searchParams.set("fields", OPPTY_PRODUCTS_FIELDS.join(","));
|
||||
url.searchParams.set("orderBy", "CreationDate:desc");
|
||||
url.searchParams.set("limit", String(OPPTY_PRODUCTS_PAGE_LIMIT));
|
||||
url.searchParams.set("offset", String(offset));
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function normalizeCacheMaxAge(value) {
|
||||
const maxAge = Number(value);
|
||||
const defaultMaxAge = 24 * 60 * 60 * 1000;
|
||||
const maximumMaxAge = 30 * 24 * 60 * 60 * 1000;
|
||||
return Number.isFinite(maxAge) && maxAge > 0
|
||||
? Math.min(maxAge, maximumMaxAge)
|
||||
: defaultMaxAge;
|
||||
}
|
||||
|
||||
async function getProductsCache() {
|
||||
if (!productsCachePromise) {
|
||||
productsCachePromise = storageLocalGet(OPPTY_PRODUCTS_CACHE_KEY)
|
||||
.then((result) => result && typeof result[OPPTY_PRODUCTS_CACHE_KEY] === "object"
|
||||
? result[OPPTY_PRODUCTS_CACHE_KEY]
|
||||
: {})
|
||||
.catch(() => ({}));
|
||||
}
|
||||
|
||||
return productsCachePromise;
|
||||
}
|
||||
|
||||
function saveProductsCache(cache) {
|
||||
productsCacheWritePromise = productsCacheWritePromise
|
||||
.catch(() => {})
|
||||
.then(() => storageLocalSet({ [OPPTY_PRODUCTS_CACHE_KEY]: cache }));
|
||||
return productsCacheWritePromise;
|
||||
}
|
||||
|
||||
function sendProductsProgress(sender, message) {
|
||||
if (!sender || !sender.tab || !Number.isInteger(sender.tab.id)) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
|
||||
const options = Number.isInteger(sender.frameId) ? { frameId: sender.frameId } : undefined;
|
||||
return tabsSendMessage(sender.tab.id, message, options).catch(() => {});
|
||||
}
|
||||
|
||||
async function refreshXsrfCookie(sender) {
|
||||
const startedAt = Date.now();
|
||||
const previous = (await getXsrfTokenCookie()).cookie;
|
||||
@@ -181,6 +430,71 @@
|
||||
});
|
||||
}
|
||||
|
||||
function tabsSendMessage(tabId, message, options) {
|
||||
if (typeof browser !== "undefined") {
|
||||
return options
|
||||
? runtimeApi.tabs.sendMessage(tabId, message, options)
|
||||
: runtimeApi.tabs.sendMessage(tabId, message);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const callback = (response) => {
|
||||
const lastError = runtimeApi.runtime.lastError;
|
||||
|
||||
if (lastError) {
|
||||
reject(new Error(lastError.message));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(response);
|
||||
};
|
||||
|
||||
if (options) {
|
||||
runtimeApi.tabs.sendMessage(tabId, message, options, callback);
|
||||
} else {
|
||||
runtimeApi.tabs.sendMessage(tabId, message, callback);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function storageLocalGet(key) {
|
||||
if (typeof browser !== "undefined") {
|
||||
return runtimeApi.storage.local.get(key);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
runtimeApi.storage.local.get(key, (result) => {
|
||||
const lastError = runtimeApi.runtime.lastError;
|
||||
|
||||
if (lastError) {
|
||||
reject(new Error(lastError.message));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve(result);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function storageLocalSet(value) {
|
||||
if (typeof browser !== "undefined") {
|
||||
return runtimeApi.storage.local.set(value);
|
||||
}
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
runtimeApi.storage.local.set(value, () => {
|
||||
const lastError = runtimeApi.runtime.lastError;
|
||||
|
||||
if (lastError) {
|
||||
reject(new Error(lastError.message));
|
||||
return;
|
||||
}
|
||||
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function wait(durationMs) {
|
||||
return new Promise((resolve) => setTimeout(resolve, durationMs));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user