217 lines
6.0 KiB
JavaScript
217 lines
6.0 KiB
JavaScript
(() => {
|
|
function create(dependencies) {
|
|
const {
|
|
toNumber,
|
|
normalizeString,
|
|
parseDatePreservingDateOnly,
|
|
formatWeekNumber,
|
|
} = dependencies;
|
|
|
|
function buildRampForecastComparisonRows(workloads) {
|
|
return (workloads || [])
|
|
.flatMap((workload) => {
|
|
const startDate = parseDatePreservingDateOnly(
|
|
workload.consumptionStartDate
|
|
);
|
|
|
|
if (Number.isNaN(startDate.getTime())) {
|
|
return [];
|
|
}
|
|
|
|
const startMonth = startDate.getMonth() + 1;
|
|
const startYear = startDate.getFullYear();
|
|
const workloadLabel = `${workload.name || "Unnamed workload"}${
|
|
workload.description ? ` | ${workload.description}` : ""
|
|
}`;
|
|
|
|
return (workload.forecast || [])
|
|
.map((forecastItem) => {
|
|
const forecastMonth = parseForecastMonthNumber(forecastItem.month);
|
|
const forecastYear = toNumber(forecastItem.year);
|
|
|
|
if (!forecastMonth || !forecastYear) {
|
|
return null;
|
|
}
|
|
|
|
const monthIndex =
|
|
(forecastYear - startYear) * 12 +
|
|
(forecastMonth - startMonth) +
|
|
1;
|
|
|
|
const currentValue = toNumber(
|
|
forecastItem.adjustedConsumptionAmount
|
|
);
|
|
const calculatedValue =
|
|
monthIndex >= 1 && monthIndex <= 12
|
|
? calculateRampForecastAmount({
|
|
adjustedACR: workload.adjustedACR,
|
|
rampMonths: workload.rampMonths,
|
|
monthIndex,
|
|
consumptionStartDate: startDate,
|
|
})
|
|
: 0;
|
|
|
|
return {
|
|
workloadLabel,
|
|
periodLabel: formatForecastPeriodLabel(forecastYear, forecastMonth),
|
|
currentValue,
|
|
calculatedValue,
|
|
delta: currentValue - calculatedValue,
|
|
monthIndex,
|
|
year: forecastYear,
|
|
month: forecastMonth,
|
|
sortKey: `${workloadLabel}|${String(forecastYear).padStart(
|
|
4,
|
|
"0"
|
|
)}-${String(forecastMonth).padStart(2, "0")}`,
|
|
};
|
|
})
|
|
.filter(Boolean);
|
|
})
|
|
.sort((left, right) => left.sortKey.localeCompare(right.sortKey));
|
|
}
|
|
|
|
function hasRampForecastMismatch(workload) {
|
|
return buildRampForecastComparisonRows([workload]).some((row) =>
|
|
isNonZeroAmount(row.delta)
|
|
);
|
|
}
|
|
|
|
function calculateRampForecastAmount({
|
|
adjustedACR,
|
|
rampMonths,
|
|
monthIndex,
|
|
consumptionStartDate,
|
|
}) {
|
|
const annualValue = toNumber(adjustedACR);
|
|
const ramp = Math.max(0, toNumber(rampMonths));
|
|
const month = Math.max(1, toNumber(monthIndex));
|
|
|
|
if (!annualValue) {
|
|
return 0;
|
|
}
|
|
|
|
if (ramp <= 0) {
|
|
return Math.round(
|
|
(annualValue / 12) *
|
|
getStartMonthProrationFactor(month, consumptionStartDate)
|
|
);
|
|
}
|
|
|
|
const steadyStateMonthlyAmount = (2 * annualValue) / (24 - ramp);
|
|
const rampStepCount = ramp + 1;
|
|
const calculatedAmount =
|
|
month <= rampStepCount
|
|
? steadyStateMonthlyAmount * (month / rampStepCount)
|
|
: steadyStateMonthlyAmount;
|
|
|
|
return Math.round(
|
|
calculatedAmount * getStartMonthProrationFactor(month, consumptionStartDate)
|
|
);
|
|
}
|
|
|
|
function getStartMonthProrationFactor(monthIndex, consumptionStartDate) {
|
|
if (toNumber(monthIndex) !== 1) {
|
|
return 1;
|
|
}
|
|
|
|
const startDate = parseDatePreservingDateOnly(consumptionStartDate);
|
|
|
|
if (Number.isNaN(startDate.getTime())) {
|
|
return 1;
|
|
}
|
|
|
|
const daysInMonth = new Date(
|
|
startDate.getFullYear(),
|
|
startDate.getMonth() + 1,
|
|
0
|
|
).getDate();
|
|
const remainingDays = daysInMonth - startDate.getDate() + 1;
|
|
|
|
return Math.max(0, Math.min(1, remainingDays / daysInMonth));
|
|
}
|
|
|
|
function isNonZeroAmount(value) {
|
|
return Math.abs(toNumber(value)) > 0.000001;
|
|
}
|
|
|
|
function parseForecastMonthNumber(value) {
|
|
if (typeof value === "number") {
|
|
return value >= 1 && value <= 12 ? value : 0;
|
|
}
|
|
|
|
const normalized = normalizeString(value);
|
|
const numericMonth = Number.parseInt(normalized, 10);
|
|
|
|
if (numericMonth >= 1 && numericMonth <= 12) {
|
|
return numericMonth;
|
|
}
|
|
|
|
const monthMap = {
|
|
JAN: 1,
|
|
JANUARY: 1,
|
|
FEB: 2,
|
|
FEBRUARY: 2,
|
|
MAR: 3,
|
|
MARCH: 3,
|
|
APR: 4,
|
|
APRIL: 4,
|
|
MAY: 5,
|
|
JUN: 6,
|
|
JUNE: 6,
|
|
JUL: 7,
|
|
JULY: 7,
|
|
AUG: 8,
|
|
AUGUST: 8,
|
|
SEP: 9,
|
|
SEPT: 9,
|
|
SEPTEMBER: 9,
|
|
OCT: 10,
|
|
OCTOBER: 10,
|
|
NOV: 11,
|
|
NOVEMBER: 11,
|
|
DEC: 12,
|
|
DECEMBER: 12,
|
|
};
|
|
|
|
return monthMap[normalized] || 0;
|
|
}
|
|
|
|
function formatForecastPeriodLabel(year, month) {
|
|
const fiscalYear = month >= 6 ? year + 1 : year;
|
|
const fiscalYearLabel = formatWeekNumber(fiscalYear % 100);
|
|
const monthLabel = new Intl.DateTimeFormat("en-US", {
|
|
month: "short",
|
|
}).format(new Date(year, month - 1, 1));
|
|
|
|
return `FY${fiscalYearLabel} ${monthLabel}`;
|
|
}
|
|
|
|
function createForecastUpdatePayload(rows, userEmail = "") {
|
|
return (rows || [])
|
|
.filter((row) => row.year && row.month)
|
|
.map((row) => ({
|
|
year: toNumber(row.year),
|
|
month: toNumber(row.month),
|
|
amount: toNumber(row.calculatedValue),
|
|
updatedBy: userEmail,
|
|
}));
|
|
}
|
|
|
|
return {
|
|
buildRampForecastComparisonRows,
|
|
hasRampForecastMismatch,
|
|
calculateRampForecastAmount,
|
|
getStartMonthProrationFactor,
|
|
isNonZeroAmount,
|
|
parseForecastMonthNumber,
|
|
formatForecastPeriodLabel,
|
|
createForecastUpdatePayload,
|
|
};
|
|
}
|
|
|
|
globalThis.ArchPanelRampForecastDomain = Object.freeze({
|
|
create,
|
|
});
|
|
})();
|