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 || ""} 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 };