General source code refactoring
This commit is contained in:
166
src/services/fusion/opportunities.js
Normal file
166
src/services/fusion/opportunities.js
Normal file
@@ -0,0 +1,166 @@
|
||||
function createFusionOpportunities({
|
||||
OPPORTUNITIES_PAGE_LIMIT,
|
||||
OPPORTUNITIES_QUERY_URL,
|
||||
isCurrentPeriodRequest,
|
||||
logRequestFailure,
|
||||
logRequestStart,
|
||||
logRequestSuccess,
|
||||
maskToken,
|
||||
getOpportunityTypeValues,
|
||||
readResponseBodyForDebug,
|
||||
environment
|
||||
}) {
|
||||
const { fetch } = environment;
|
||||
async function requestOpportunitiesPage(token, period, dateRange, offset, accumulatedItems, requestVersion, page) {
|
||||
if (!isCurrentPeriodRequest(requestVersion)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const payload = createOpportunitiesQueryPayload(dateRange, offset);
|
||||
const requestBody = JSON.stringify(payload);
|
||||
const requestEntry = logRequestStart({
|
||||
name: "requestOpportunities",
|
||||
method: "POST",
|
||||
url: OPPORTUNITIES_QUERY_URL,
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${maskToken(token)}`,
|
||||
"Content-Type": "application/json",
|
||||
Origin: "https://eeho.fa.us2.oraclecloud.com",
|
||||
Preference: "transient"
|
||||
},
|
||||
body: JSON.stringify(payload, null, 2),
|
||||
metadata: {
|
||||
period,
|
||||
page,
|
||||
offset,
|
||||
startCloseDate: dateRange.startCloseDate,
|
||||
endCloseDate: dateRange.endCloseDate
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
const response = await fetch(OPPORTUNITIES_QUERY_URL, {
|
||||
method: "POST",
|
||||
credentials: "include",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
Authorization: `Bearer ${token}`,
|
||||
"Content-Type": "application/json",
|
||||
Origin: "https://eeho.fa.us2.oraclecloud.com",
|
||||
Preference: "transient"
|
||||
},
|
||||
body: requestBody
|
||||
});
|
||||
|
||||
const responseBody = await readResponseBodyForDebug(response);
|
||||
logRequestSuccess(requestEntry, response, responseBody);
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Opportunities query failed with status ${response.status}.`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
const pageItems = Array.isArray(responseData.items) ? responseData.items : [];
|
||||
const allItems = accumulatedItems.concat(pageItems);
|
||||
|
||||
if (!responseData.hasMore) {
|
||||
return allItems;
|
||||
}
|
||||
|
||||
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("Opportunities query returned hasMore without additional results.");
|
||||
}
|
||||
|
||||
return requestOpportunitiesPage(
|
||||
token,
|
||||
period,
|
||||
dateRange,
|
||||
currentOffset + receivedCount,
|
||||
allItems,
|
||||
requestVersion,
|
||||
page + 1
|
||||
);
|
||||
} catch (error) {
|
||||
logRequestFailure(requestEntry, error.message || "Request failed.");
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
function createOpportunitiesQueryPayload(dateRange, offset) {
|
||||
return {
|
||||
aggregationResults: false,
|
||||
applications: ["ORACLE-ISS-APP"],
|
||||
onlyData: true,
|
||||
entity: "Opportunity",
|
||||
q: {
|
||||
op: "$and",
|
||||
criteria: [
|
||||
{
|
||||
op: "$or",
|
||||
criteria: Array.from(getOpportunityTypeValues(), (value) => ({
|
||||
op: "$eq",
|
||||
attribute: "RecordSet",
|
||||
value
|
||||
}))
|
||||
},
|
||||
{
|
||||
op: "$wi",
|
||||
attribute: "EffectiveDate",
|
||||
value1: dateRange.startCloseDate,
|
||||
value2: dateRange.endCloseDate,
|
||||
dynamicDate: false
|
||||
}
|
||||
]
|
||||
},
|
||||
keywords: null,
|
||||
keywordsFields: [
|
||||
"Name",
|
||||
"OptyNumber",
|
||||
"PrimaryRevenue.WinProb",
|
||||
"CustomerAccount",
|
||||
"PrimaryRevenue.RevnAmount",
|
||||
"EffectiveDate",
|
||||
"StatusCode",
|
||||
"DealRisk_c",
|
||||
"Owner",
|
||||
"ForecastGroup_c",
|
||||
"LastUpdateDate",
|
||||
"PrimaryRevenue.RevnAmountCurcyCode",
|
||||
"OptyId"
|
||||
],
|
||||
fields: [
|
||||
"Name",
|
||||
"OptyNumber",
|
||||
"PrimaryRevenue.WinProb",
|
||||
"CustomerAccount",
|
||||
"PrimaryRevenue.RevnAmount",
|
||||
"EffectiveDate",
|
||||
"StatusCode",
|
||||
"DealRisk_c",
|
||||
"Owner",
|
||||
"ForecastGroup_c",
|
||||
"LastUpdateDate",
|
||||
"PrimaryRevenue.RevnAmountCurcyCode",
|
||||
"OptyId",
|
||||
"CustomerAccount.PartyUniqueName",
|
||||
"CustomerAccount.PartyId",
|
||||
"CustomerAccount.PartyNumber"
|
||||
],
|
||||
sort: [],
|
||||
language: "en",
|
||||
skipInValidFields: true,
|
||||
skipHiddenFromUIFields: true,
|
||||
copiedFrom: "queries/d5dbcd01-9ff3-40be-9b5b-64744cbf7162",
|
||||
limit: OPPORTUNITIES_PAGE_LIMIT,
|
||||
offset
|
||||
};
|
||||
}
|
||||
|
||||
return { requestOpportunitiesPage, createOpportunitiesQueryPayload };
|
||||
}
|
||||
65
src/services/fusion/products.js
Normal file
65
src/services/fusion/products.js
Normal file
@@ -0,0 +1,65 @@
|
||||
function createFusionProducts({
|
||||
OPPTY_PRODUCTS_FIELDS,
|
||||
OPPTY_PRODUCTS_MAX_PAGES,
|
||||
OPPTY_PRODUCTS_PAGE_LIMIT,
|
||||
ORACLE_DOMAIN,
|
||||
environment
|
||||
}) {
|
||||
const { fetch } = environment;
|
||||
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();
|
||||
}
|
||||
|
||||
return { fetchAllOpptyProducts, createOpptyProductsUrl };
|
||||
}
|
||||
169
src/services/http/background-json.js
Normal file
169
src/services/http/background-json.js
Normal file
@@ -0,0 +1,169 @@
|
||||
function createHttpBackgroundJson({
|
||||
DEBUG_BODY_LIMIT,
|
||||
environment
|
||||
}) {
|
||||
const { fetch } = environment;
|
||||
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.`);
|
||||
}
|
||||
|
||||
try {
|
||||
return parseJsonResponse(responsePayload.text, requestName);
|
||||
} catch (error) {
|
||||
error.httpStatus = response.status;
|
||||
debugEntry.status = "failed";
|
||||
debugEntry.error = error.message;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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) {
|
||||
const invalidJsonError = new Error(`${requestName} returned an invalid JSON response.`);
|
||||
invalidJsonError.code = "INVALID_JSON_RESPONSE";
|
||||
throw invalidJsonError;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
return { fetchJson, createBackgroundDebugEntry, completeBackgroundDebugEntry, appendBackgroundDebugEntry, readResponsePayload, parseJsonResponse, createDebugBody, normalizeDebugHeaders, headersToObject, formatDebugRequestBody };
|
||||
}
|
||||
75
src/services/spa/accounts.js
Normal file
75
src/services/spa/accounts.js
Normal file
@@ -0,0 +1,75 @@
|
||||
function createSpaAccounts({
|
||||
ACCOUNT_PLANS_PAYLOAD,
|
||||
ACCOUNT_PLANS_URL,
|
||||
ACCOUNT_SUMMARY_URL,
|
||||
extractAccountPlans,
|
||||
fetchJson,
|
||||
findAccountPlanId,
|
||||
normalizeMessageAuthorization
|
||||
}) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
return { requestAccountPlans };
|
||||
}
|
||||
58
src/services/spa/consumption.js
Normal file
58
src/services/spa/consumption.js
Normal file
@@ -0,0 +1,58 @@
|
||||
function createSpaConsumption({
|
||||
CONSUMPTION_TREND_REQUESTS,
|
||||
CONSUMPTION_TREND_URL,
|
||||
fetchJson,
|
||||
normalizeMessageAuthorization
|
||||
}) {
|
||||
async function requestConsumptionTrend(message) {
|
||||
const subPlanNum = typeof message.subPlanNum === "string" ? message.subPlanNum.trim() : "";
|
||||
const period = typeof message.period === "string" ? message.period.toLowerCase() : "";
|
||||
const requestName = CONSUMPTION_TREND_REQUESTS[period];
|
||||
const authorization = normalizeMessageAuthorization(message.authorization, "g2m-authorization");
|
||||
const debugRequests = [];
|
||||
|
||||
if (!subPlanNum) {
|
||||
throw new Error("Sub plan number is required for the consumption trend.");
|
||||
}
|
||||
|
||||
if (!requestName) {
|
||||
throw new Error(`Unsupported consumption trend period: ${period || "(missing)"}.`);
|
||||
}
|
||||
|
||||
if (!authorization.value) {
|
||||
throw new Error("OCICONS-TS authorization is unavailable for the consumption trend.");
|
||||
}
|
||||
|
||||
const url = `${CONSUMPTION_TREND_URL}/${encodeURIComponent(subPlanNum)}?quarter=All&timePeriod=${period}`;
|
||||
|
||||
try {
|
||||
const response = await fetchJson(url, {
|
||||
method: "GET",
|
||||
credentials: "include",
|
||||
cache: "no-store",
|
||||
headers: {
|
||||
Accept: "application/json",
|
||||
"G2m-Authorization": authorization.value
|
||||
}
|
||||
}, requestName, debugRequests);
|
||||
const items = Array.isArray(response)
|
||||
? response
|
||||
: Array.isArray(response && response.items)
|
||||
? response.items
|
||||
: [];
|
||||
|
||||
return {
|
||||
ok: true,
|
||||
subPlanNum,
|
||||
period,
|
||||
items,
|
||||
debugRequests
|
||||
};
|
||||
} catch (error) {
|
||||
error.debugRequests = debugRequests;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return { requestConsumptionTrend };
|
||||
}
|
||||
61
src/services/spa/response-shapes.js
Normal file
61
src/services/spa/response-shapes.js
Normal file
@@ -0,0 +1,61 @@
|
||||
function createSpaResponseShapes() {
|
||||
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;
|
||||
}
|
||||
|
||||
return { findNestedObject, findAccountPlanId, extractAccountPlans };
|
||||
}
|
||||
Reference in New Issue
Block a user