Files
arch-panel-extension-browser/src/domain/ramp-forecast.js

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,
});
})();