UBER-EATS-Wrapper/src/modules/proxy/menuValidation.js

167 lines
4.8 KiB
JavaScript

function parseTimeToMinutes(time) {
if (typeof time !== "string" || !/^\d{2}:\d{2}$/.test(time)) {
return null;
}
const [hourStr, minuteStr] = time.split(":");
const hour = Number(hourStr);
const minute = Number(minuteStr);
if (
Number.isNaN(hour) ||
Number.isNaN(minute) ||
hour < 0 ||
hour > 23 ||
minute < 0 ||
minute > 59
) {
return null;
}
return hour * 60 + minute;
}
const DAYS = [
"monday",
"tuesday",
"wednesday",
"thursday",
"friday",
"saturday",
"sunday"
];
function dayIndex(day) {
return DAYS.indexOf(String(day || "").toLowerCase());
}
function buildWeeklyIntervals(menuPayload) {
const menus = Array.isArray(menuPayload?.menus) ? menuPayload.menus : [];
const intervals = [];
menus.forEach((menu, menuIndex) => {
const availability = Array.isArray(menu?.service_availability) ? menu.service_availability : [];
availability.forEach((dayConfig, dayConfigIndex) => {
const dIdx = dayIndex(dayConfig?.day_of_week);
if (dIdx < 0) {
return;
}
const periods = Array.isArray(dayConfig?.time_periods) ? dayConfig.time_periods : [];
periods.forEach((period, periodIndex) => {
const start = parseTimeToMinutes(period?.start_time);
const end = parseTimeToMinutes(period?.end_time);
if (start === null || end === null || end < start) {
return;
}
const base = dIdx * 24 * 60;
intervals.push({
start: base + start,
end: base + end,
menuIndex,
dayConfigIndex,
periodIndex,
day: dayConfig?.day_of_week
});
});
});
});
return intervals.sort((a, b) => a.start - b.start);
}
function mergeContiguousIntervals(intervals) {
if (!intervals.length) {
return [];
}
const merged = [Object.assign({}, intervals[0])];
for (let i = 1; i < intervals.length; i += 1) {
const curr = intervals[i];
const last = merged[merged.length - 1];
if (curr.start <= last.end + 1) {
last.end = Math.max(last.end, curr.end);
} else {
merged.push(Object.assign({}, curr));
}
}
return merged;
}
function collectVisibilityOverlapErrors(menuPayload) {
const items = Array.isArray(menuPayload?.items) ? menuPayload.items : [];
const errors = [];
items.forEach((item) => {
const visibilityHours = Array.isArray(item?.visibility_info?.hours)
? item.visibility_info.hours
: [];
visibilityHours.forEach((hourRule) => {
const hoursOfWeek = Array.isArray(hourRule?.hours_of_week) ? hourRule.hours_of_week : [];
const perDay = new Map();
hoursOfWeek.forEach((dayHours) => {
const d = String(dayHours?.day_of_week || "").toLowerCase();
if (!DAYS.includes(d)) {
return;
}
const list = perDay.get(d) || [];
const periods = Array.isArray(dayHours?.time_periods) ? dayHours.time_periods : [];
periods.forEach((p) => {
const s = parseTimeToMinutes(p?.start_time);
const e = parseTimeToMinutes(p?.end_time);
if (s === null || e === null || e < s) {
return;
}
list.push({ start: s, end: e, raw: p });
});
perDay.set(d, list);
});
perDay.forEach((periods, d) => {
const sorted = periods.sort((a, b) => a.start - b.start);
for (let i = 1; i < sorted.length; i += 1) {
const prev = sorted[i - 1];
const curr = sorted[i];
if (curr.start <= prev.end) {
errors.push(
`Invalid visibility overlap for item ${item?.id || "<unknown>"} on ${d}: ` +
`${String(prev.raw?.start_time)}-${String(prev.raw?.end_time)} overlaps ` +
`${String(curr.raw?.start_time)}-${String(curr.raw?.end_time)}`
);
return;
}
}
});
});
});
return errors;
}
function validateUploadMenuPayload(menuPayload) {
const errors = [];
const menus = Array.isArray(menuPayload?.menus) ? menuPayload.menus : [];
if (menus.length === 0) {
errors.push("No Menus Errors: menu.menus must contain at least one menu.");
return errors;
}
const intervals = buildWeeklyIntervals(menuPayload);
if (intervals.length === 0) {
errors.push(
"No Hours Errors: at least one service_availability interval is required across the week."
);
} else {
const merged = mergeContiguousIntervals(intervals);
const shortBlocks = merged.filter((block) => block.end - block.start + 1 < 60);
if (shortBlocks.length > 0) {
errors.push(
"Short Hours Errors: each effective contiguous service_availability interval must be at least 60 minutes."
);
}
}
errors.push(...collectVisibilityOverlapErrors(menuPayload));
return errors;
}
module.exports = {
validateUploadMenuPayload
};