Files
Pickup-Config/services/pickupScheduler.js
2026-01-04 00:11:29 +01:00

779 lines
24 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

const cron = require('node-cron');
const foodsharingClient = require('./foodsharingClient');
const sessionStore = require('./sessionStore');
const { DEFAULT_SETTINGS } = require('./adminConfig');
const notificationService = require('./notificationService');
const { readConfig, writeConfig } = require('./configStore');
const { readStoreWatch, writeStoreWatch } = require('./storeWatchStore');
const { setStoreStatus, persistStoreStatusCache } = require('./storeStatusCache');
const { sendDormantPickupWarning } = require('./notificationService');
const { ensureSession, withSessionRetry } = require('./sessionRefresh');
function wait(ms) {
if (!ms || ms <= 0) {
return Promise.resolve();
}
return new Promise((resolve) => setTimeout(resolve, ms));
}
const weekdayMap = {
Montag: 'Monday',
Dienstag: 'Tuesday',
Mittwoch: 'Wednesday',
Donnerstag: 'Thursday',
Freitag: 'Friday',
Samstag: 'Saturday',
Sonntag: 'Sunday'
};
function randomDelayMs(minSeconds = 10, maxSeconds = 120) {
const min = minSeconds * 1000;
const max = maxSeconds * 1000;
return Math.floor(Math.random() * (max - min + 1)) + min;
}
function resolveSettings(settings) {
if (!settings) {
return { ...DEFAULT_SETTINGS };
}
return {
scheduleCron: settings.scheduleCron || DEFAULT_SETTINGS.scheduleCron,
randomDelayMinSeconds: Number.isFinite(settings.randomDelayMinSeconds)
? settings.randomDelayMinSeconds
: DEFAULT_SETTINGS.randomDelayMinSeconds,
randomDelayMaxSeconds: Number.isFinite(settings.randomDelayMaxSeconds)
? settings.randomDelayMaxSeconds
: DEFAULT_SETTINGS.randomDelayMaxSeconds,
initialDelayMinSeconds: Number.isFinite(settings.initialDelayMinSeconds)
? settings.initialDelayMinSeconds
: DEFAULT_SETTINGS.initialDelayMinSeconds,
initialDelayMaxSeconds: Number.isFinite(settings.initialDelayMaxSeconds)
? settings.initialDelayMaxSeconds
: DEFAULT_SETTINGS.initialDelayMaxSeconds,
storeWatchCron: settings.storeWatchCron || DEFAULT_SETTINGS.storeWatchCron,
storeWatchInitialDelayMinSeconds: Number.isFinite(settings.storeWatchInitialDelayMinSeconds)
? settings.storeWatchInitialDelayMinSeconds
: DEFAULT_SETTINGS.storeWatchInitialDelayMinSeconds,
storeWatchInitialDelayMaxSeconds: Number.isFinite(settings.storeWatchInitialDelayMaxSeconds)
? settings.storeWatchInitialDelayMaxSeconds
: DEFAULT_SETTINGS.storeWatchInitialDelayMaxSeconds,
storeWatchRequestDelayMs: Number.isFinite(settings.storeWatchRequestDelayMs)
? settings.storeWatchRequestDelayMs
: DEFAULT_SETTINGS.storeWatchRequestDelayMs,
ignoredSlots: Array.isArray(settings.ignoredSlots) ? settings.ignoredSlots : DEFAULT_SETTINGS.ignoredSlots,
notifications: {
ntfy: {
enabled: !!settings.notifications?.ntfy?.enabled,
serverUrl: settings.notifications?.ntfy?.serverUrl || DEFAULT_SETTINGS.notifications.ntfy.serverUrl,
topicPrefix: settings.notifications?.ntfy?.topicPrefix || DEFAULT_SETTINGS.notifications.ntfy.topicPrefix,
username: settings.notifications?.ntfy?.username || DEFAULT_SETTINGS.notifications.ntfy.username,
password: settings.notifications?.ntfy?.password || DEFAULT_SETTINGS.notifications.ntfy.password
},
telegram: {
enabled: !!settings.notifications?.telegram?.enabled,
botToken: settings.notifications?.telegram?.botToken || DEFAULT_SETTINGS.notifications.telegram.botToken
}
}
};
}
function deactivateEntryInMemory(entry) {
if (entry) {
entry.active = false;
}
}
function persistEntryDeactivation(profileId, entryId, options = {}) {
if (!profileId || !entryId) {
return;
}
try {
const config = readConfig(profileId);
let changed = false;
const updated = config.map((item) => {
if (String(item?.id) === String(entryId)) {
let mutated = false;
const updatedEntry = { ...item };
if (item?.active !== false) {
updatedEntry.active = false;
mutated = true;
}
if (options.resetDesiredWindow) {
if (updatedEntry.desiredDate !== undefined) {
delete updatedEntry.desiredDate;
mutated = true;
}
if (updatedEntry.desiredDateRange !== undefined) {
delete updatedEntry.desiredDateRange;
mutated = true;
}
}
if (mutated) {
changed = true;
return updatedEntry;
}
}
return item;
});
if (changed) {
writeConfig(profileId, updated);
}
} catch (error) {
console.error(`[CONFIG] Konnte Eintrag ${entryId} für Profil ${profileId} nicht deaktivieren:`, error.message);
}
}
function toDateValue(input) {
if (!input) {
return null;
}
const date = input instanceof Date ? new Date(input.getTime()) : new Date(input);
if (Number.isNaN(date.getTime())) {
return null;
}
return new Date(date.getFullYear(), date.getMonth(), date.getDate()).getTime();
}
function formatDesiredWindowLabel(entry) {
if (!entry) {
return null;
}
const formatValue = (value) => {
if (!value) {
return null;
}
const date = new Date(value);
if (Number.isNaN(date.getTime())) {
return null;
}
return date.toLocaleDateString('de-DE', {
day: '2-digit',
month: '2-digit',
year: 'numeric'
});
};
if (entry.desiredDateRange) {
const startLabel = formatValue(entry.desiredDateRange.start);
const endLabel = formatValue(entry.desiredDateRange.end);
if (startLabel && endLabel) {
return startLabel === endLabel ? startLabel : `${startLabel} ${endLabel}`;
}
return startLabel || endLabel;
}
return formatValue(entry.desiredDate);
}
function desiredWindowExpired(entry) {
if (!entry?.active) {
return false;
}
const todayValue = toDateValue(new Date());
if (todayValue === null) {
return false;
}
if (entry.desiredDateRange?.start || entry.desiredDateRange?.end) {
const endValue = toDateValue(entry.desiredDateRange.end) ?? toDateValue(entry.desiredDateRange.start);
return endValue !== null && endValue < todayValue;
}
const desiredValue = toDateValue(entry.desiredDate);
return desiredValue !== null && desiredValue < todayValue;
}
function resetDesiredWindow(entry) {
if (!entry) {
return;
}
if (entry.desiredDate !== undefined) {
delete entry.desiredDate;
}
if (entry.desiredDateRange !== undefined) {
delete entry.desiredDateRange;
}
}
async function handleExpiredDesiredWindow(session, entry) {
const profileId = session?.profile?.id;
const storeName = entry.label || entry.id;
const desiredLabel = formatDesiredWindowLabel(entry);
console.log(
`[INFO] Wunschzeitraum abgelaufen für ${storeName}${desiredLabel ? ` (${desiredLabel})` : ''}. Eintrag wird deaktiviert.`
);
deactivateEntryInMemory(entry);
resetDesiredWindow(entry);
persistEntryDeactivation(profileId, entry.id, { resetDesiredWindow: true });
if (profileId) {
try {
await notificationService.sendDesiredWindowMissedNotification({
profileId,
storeName,
desiredWindowLabel: desiredLabel
});
} catch (error) {
console.error(
`[NOTIFY] Benachrichtigung zum verpassten Zeitraum für ${storeName} fehlgeschlagen:`,
error.message
);
}
}
}
function matchesDesiredDate(pickupDate, desiredDate, desiredDateRange) {
const pickupValue = toDateValue(pickupDate);
if (pickupValue === null) {
return false;
}
const hasRange = Boolean(desiredDateRange && (desiredDateRange.start || desiredDateRange.end));
if (hasRange) {
const startValue = toDateValue(desiredDateRange.start);
const endValue = toDateValue(desiredDateRange.end);
const normalizedStart = startValue !== null ? startValue : endValue;
const normalizedEnd = endValue !== null ? endValue : startValue;
if (normalizedStart !== null && pickupValue < normalizedStart) {
return false;
}
if (normalizedEnd !== null && pickupValue > normalizedEnd) {
return false;
}
return true;
}
const desiredValue = toDateValue(desiredDate);
if (desiredValue === null) {
return true;
}
return pickupValue === desiredValue;
}
function matchesDesiredWeekday(pickupDate, desiredWeekday) {
if (!desiredWeekday) {
return true;
}
const weekday = pickupDate.toLocaleDateString('en-US', { weekday: 'long' });
return weekday === desiredWeekday;
}
function shouldIgnoreSlot(entry, pickup, settings) {
const rules = settings.ignoredSlots || [];
return rules.some((rule) => {
if (!rule?.storeId) {
return false;
}
if (String(rule.storeId) !== entry.id) {
return false;
}
if (rule.description) {
return pickup.description === rule.description;
}
return true;
});
}
async function processBooking(session, entry, pickup) {
const readableDate = new Date(pickup.date).toLocaleString('de-DE');
const storeName = entry.label || entry.id;
if (entry.onlyNotify) {
console.log(`[INFO] Slot gefunden (nur Hinweis) für ${storeName} am ${readableDate}`);
await notificationService.sendSlotNotification({
profileId: session.profile.id,
storeName,
pickupDate: pickup.date,
onlyNotify: true,
booked: false,
storeId: entry.id
});
deactivateEntryInMemory(entry);
persistEntryDeactivation(session.profile.id, entry.id);
return;
}
const utcDate = new Date(pickup.date).toISOString();
try {
const allowed = await withSessionRetry(
session,
() => foodsharingClient.pickupRuleCheck(entry.id, utcDate, session.profile.id, session),
{ label: 'pickupRuleCheck' }
);
if (!allowed) {
console.warn(`[WARN] Rule-Check fehlgeschlagen für ${storeName} am ${readableDate}`);
return;
}
await withSessionRetry(
session,
() => foodsharingClient.bookSlot(entry.id, utcDate, session.profile.id, session),
{ label: 'bookSlot' }
);
console.log(`[SUCCESS] Slot gebucht für ${storeName} am ${readableDate}`);
await notificationService.sendSlotNotification({
profileId: session.profile.id,
storeName,
pickupDate: pickup.date,
onlyNotify: false,
booked: true,
storeId: entry.id
});
deactivateEntryInMemory(entry);
persistEntryDeactivation(session.profile.id, entry.id);
} catch (error) {
console.error(`[ERROR] Buchung für ${storeName} am ${readableDate} fehlgeschlagen:`, error.message);
try {
await notificationService.sendAdminBookingErrorNotification({
profileId: session.profile.id,
profileEmail: session.profile.email,
storeName,
storeId: entry.id,
pickupDate: pickup.date,
error: error.message
});
} catch (notifyError) {
console.error(
`[NOTIFY] Admin-Benachrichtigung für fehlgeschlagene Buchung bei ${storeName} fehlgeschlagen:`,
notifyError.message
);
}
}
}
async function checkEntry(sessionId, entry, settings) {
if (entry?.active === false) {
return;
}
const session = sessionStore.get(sessionId);
if (!session) {
return;
}
if (desiredWindowExpired(entry)) {
await handleExpiredDesiredWindow(session, entry);
return;
}
const ready = await ensureSession(session);
if (!ready) {
return;
}
try {
const pickups = await withSessionRetry(
session,
() => foodsharingClient.fetchPickups(entry.id, session.cookieHeader, session),
{ label: 'fetchPickups' }
);
let hasProfileId = false;
let availablePickup = null;
const desiredWeekday = entry.desiredWeekday ? weekdayMap[entry.desiredWeekday] || entry.desiredWeekday : null;
pickups.forEach((pickup) => {
const pickupDate = new Date(pickup.date);
if (!matchesDesiredDate(pickupDate, entry.desiredDate, entry.desiredDateRange)) {
return;
}
if (!matchesDesiredWeekday(pickupDate, desiredWeekday)) {
return;
}
if (entry.checkProfileId && pickup.occupiedSlots?.some((slot) => slot.profile?.id === session.profile.id)) {
hasProfileId = true;
return;
}
if (pickup.isAvailable && !availablePickup) {
availablePickup = pickup;
}
});
if (!availablePickup) {
console.log(
`[INFO] Kein freier Slot für ${entry.label || entry.id} in dieser Runde gefunden. Profil bereits eingetragen: ${
hasProfileId ? 'ja' : 'nein'
}`
);
return;
}
if (shouldIgnoreSlot(entry, availablePickup, settings)) {
console.log(`[INFO] Slot für ${entry.id} aufgrund einer Admin-Regel ignoriert.`);
return;
}
if (!entry.checkProfileId || !hasProfileId) {
await processBooking(session, entry, availablePickup);
}
} catch (error) {
console.error(`[ERROR] Pickup-Check für Store ${entry.id} fehlgeschlagen:`, error.message);
}
}
async function checkWatchedStores(sessionId, settings = DEFAULT_SETTINGS, options = {}) {
const session = sessionStore.get(sessionId);
if (!session?.profile?.id) {
return [];
}
const watchers = readStoreWatch(session.profile.id);
if (!Array.isArray(watchers) || watchers.length === 0) {
return [];
}
const ready = await ensureSession(session);
if (!ready) {
return [];
}
const perRequestDelay = Math.max(0, Number(settings?.storeWatchRequestDelayMs) || 0);
let changed = false;
let statusCacheUpdated = false;
const summary = [];
for (let index = 0; index < watchers.length; index += 1) {
const watcher = watchers[index];
try {
const details = await withSessionRetry(
session,
() => foodsharingClient.fetchStoreDetails(watcher.storeId, session.cookieHeader, session),
{ label: 'fetchStoreDetails' }
);
const status = details?.teamSearchStatus === 1 ? 1 : 0;
const checkedAt = Date.now();
if (status === 1 && watcher.lastTeamSearchStatus !== 1) {
await notificationService.sendStoreWatchNotification({
profileId: session.profile.id,
storeName: watcher.storeName,
storeId: watcher.storeId,
regionName: watcher.regionName
});
}
if (watcher.lastTeamSearchStatus !== status) {
watcher.lastTeamSearchStatus = status;
changed = true;
}
watcher.lastStatusCheckAt = checkedAt;
changed = true;
setStoreStatus(watcher.storeId, { teamSearchStatus: status, fetchedAt: checkedAt });
statusCacheUpdated = true;
summary.push({
storeId: watcher.storeId,
storeName: watcher.storeName,
regionName: watcher.regionName,
status,
checkedAt
});
} catch (error) {
console.error(`[WATCH] Prüfung für Store ${watcher.storeId} fehlgeschlagen:`, error.message);
summary.push({
storeId: watcher.storeId,
storeName: watcher.storeName,
regionName: watcher.regionName,
status: null,
checkedAt: Date.now(),
error: error.message || 'Unbekannter Fehler'
});
} finally {
const hasNext = index < watchers.length - 1;
if (hasNext && perRequestDelay > 0) {
await wait(perRequestDelay);
}
}
}
if (changed) {
writeStoreWatch(session.profile.id, watchers);
}
if (statusCacheUpdated) {
persistStoreStatusCache();
}
if (options.sendSummary && summary.length > 0) {
try {
await notificationService.sendStoreWatchSummaryNotification({
profileId: session.profile.id,
entries: summary,
triggeredBy: options.triggeredBy || 'manual'
});
} catch (error) {
console.error('[WATCH] Zusammenfassung konnte nicht versendet werden:', error.message);
}
}
return summary;
}
function scheduleStoreWatchers(sessionId, settings) {
const effectiveSettings = settings || DEFAULT_SETTINGS;
const session = sessionStore.get(sessionId);
if (!session?.profile?.id) {
return false;
}
const watchers = readStoreWatch(session.profile.id);
if (!Array.isArray(watchers) || watchers.length === 0) {
return false;
}
const cronExpression = effectiveSettings.storeWatchCron || DEFAULT_SETTINGS.storeWatchCron;
const job = cron.schedule(
cronExpression,
() => {
checkWatchedStores(sessionId, effectiveSettings).catch((error) => {
console.error('[WATCH] Regelmäßige Prüfung fehlgeschlagen:', error.message);
});
},
{ timezone: 'Europe/Berlin' }
);
sessionStore.attachJob(sessionId, job);
setTimeout(
() => checkWatchedStores(sessionId, effectiveSettings),
randomDelayMs(
effectiveSettings.storeWatchInitialDelayMinSeconds,
effectiveSettings.storeWatchInitialDelayMaxSeconds
)
);
console.log(
`[WATCH] Überwache ${watchers.length} Betriebe für Session ${sessionId} (Cron: ${cronExpression}).`
);
return true;
}
function scheduleEntry(sessionId, entry, settings) {
const cronExpression = settings.scheduleCron || DEFAULT_SETTINGS.scheduleCron;
const job = cron.schedule(
cronExpression,
() => {
const delay = randomDelayMs(
settings.randomDelayMinSeconds,
settings.randomDelayMaxSeconds
);
setTimeout(() => checkEntry(sessionId, entry, settings), delay);
},
{
timezone: 'Europe/Berlin'
}
);
sessionStore.attachJob(sessionId, job);
setTimeout(
() => checkEntry(sessionId, entry, settings),
randomDelayMs(settings.initialDelayMinSeconds, settings.initialDelayMaxSeconds)
);
}
function scheduleConfig(sessionId, config, settings) {
const resolvedSettings = resolveSettings(settings);
sessionStore.clearJobs(sessionId);
scheduleDormantMembershipCheck(sessionId);
const watchScheduled = scheduleStoreWatchers(sessionId, resolvedSettings);
const entries = Array.isArray(config) ? config : [];
const activeEntries = entries.filter((entry) => entry.active);
if (activeEntries.length === 0) {
if (watchScheduled) {
console.log(
`[INFO] Keine aktiven Pickup-Einträge für Session ${sessionId} Store-Watch bleibt aktiv.`
);
} else {
console.log(`[INFO] Keine aktiven Einträge für Session ${sessionId} Scheduler ruht.`);
}
return;
}
activeEntries.forEach((entry) => scheduleEntry(sessionId, entry, resolvedSettings));
console.log(
`[INFO] Scheduler für Session ${sessionId} mit ${activeEntries.length} Jobs aktiv (Cron: ${resolvedSettings.scheduleCron}).`
);
}
async function runStoreWatchCheck(sessionId, settings, options = {}) {
const resolvedSettings = resolveSettings(settings);
return checkWatchedStores(sessionId, resolvedSettings, options);
}
async function runImmediatePickupCheck(sessionId, config, settings) {
const resolvedSettings = resolveSettings(settings);
const entries = Array.isArray(config) ? config : [];
const activeEntries = entries.filter((entry) => entry?.active);
if (activeEntries.length === 0) {
return { checked: 0 };
}
for (const entry of activeEntries) {
await checkEntry(sessionId, entry, resolvedSettings);
}
return { checked: activeEntries.length };
}
function setMonthOffset(date, offset) {
const copy = new Date(date.getTime());
copy.setMonth(copy.getMonth() + offset);
return copy;
}
function getMissingLastPickupStoreIds(config = []) {
if (!Array.isArray(config)) {
return [];
}
return config
.filter((entry) => entry && entry.id && !entry.hidden && !entry.skipDormantCheck && !entry.lastPickupAt)
.map((entry) => String(entry.id));
}
async function checkDormantMembers(sessionId, options = {}) {
const session = sessionStore.get(sessionId);
if (!session?.profile?.id) {
return;
}
const storeIdSet = Array.isArray(options.storeIds)
? new Set(options.storeIds.map((storeId) => String(storeId)))
: null;
const profileId = session.profile.id;
const ensured = await ensureSession(session);
if (!ensured) {
return;
}
const config = readConfig(profileId);
const skipMap = new Map();
const configEntryMap = new Map();
config.forEach((entry, index) => {
if (entry?.id) {
const id = String(entry.id);
skipMap.set(id, !!entry.skipDormantCheck);
configEntryMap.set(id, { entry, index });
}
});
let configChanged = false;
const storeTargets = new Map();
config.forEach((entry) => {
if (!entry?.id || entry.hidden) {
return;
}
const storeId = String(entry.id);
if (storeIdSet && !storeIdSet.has(storeId)) {
return;
}
if (skipMap.get(storeId)) {
return;
}
storeTargets.set(storeId, {
storeId,
storeName: entry.label || `Store ${storeId}`
});
});
const stores = Array.isArray(session.storesCache?.data) ? session.storesCache.data : [];
if (stores.length === 0) {
console.warn(`[DORMANT] Keine Stores für Session ${sessionId} im Cache gefunden.`);
} else {
stores.forEach((store) => {
const storeId = store?.id ? String(store.id) : null;
if (!storeId || !storeTargets.has(storeId)) {
return;
}
const target = storeTargets.get(storeId);
storeTargets.set(storeId, {
...target,
storeName: store.name || target.storeName
});
});
}
if (storeTargets.size === 0) {
return;
}
const fourMonthsAgo = setMonthOffset(new Date(), -4).getTime();
const hygieneCutoff = Date.now() + 6 * 7 * 24 * 60 * 60 * 1000;
for (const target of storeTargets.values()) {
const storeId = target.storeId;
let members = [];
try {
members = await withSessionRetry(
session,
() => foodsharingClient.fetchStoreMembers(storeId, session.cookieHeader, session),
{ label: 'fetchStoreMembers' }
);
} catch (error) {
console.warn(`[DORMANT] Mitglieder von Store ${storeId} konnten nicht geladen werden:`, error.message);
continue;
}
const memberEntry = members.find((m) => String(m?.id) === String(profileId));
if (!memberEntry) {
continue;
}
const reasons = [];
const lastFetchMs = memberEntry.last_fetch ? Number(memberEntry.last_fetch) * 1000 : null;
if (Number.isFinite(lastFetchMs)) {
const configEntry = configEntryMap.get(storeId)?.entry;
const lastPickupAt = new Date(lastFetchMs).toISOString();
if (configEntry && configEntry.lastPickupAt !== lastPickupAt) {
configEntry.lastPickupAt = lastPickupAt;
configChanged = true;
}
}
if (!lastFetchMs || lastFetchMs < fourMonthsAgo) {
const lastFetchLabel = lastFetchMs ? new Date(lastFetchMs).toLocaleDateString('de-DE') : 'unbekannt';
reasons.push(`Letzte Abholung: ${lastFetchLabel} (älter als 4 Monate)`);
}
if (memberEntry.hygiene_certificate_until) {
const expiry = new Date(memberEntry.hygiene_certificate_until.replace(' ', 'T'));
if (!Number.isNaN(expiry.getTime()) && expiry.getTime() < hygieneCutoff) {
reasons.push(
`Hygiene-Nachweis läuft bald ab: ${expiry.toLocaleDateString('de-DE')} (unter 6 Wochen)`
);
}
}
if (reasons.length > 0) {
try {
await sendDormantPickupWarning({
profileId,
storeName: target.storeName,
storeId,
reasonLines: reasons
});
} catch (error) {
console.error(`[DORMANT] Warnung für Store ${storeId} konnte nicht versendet werden:`, error.message);
}
}
}
if (configChanged) {
try {
writeConfig(profileId, config);
} catch (error) {
console.error(`[DORMANT] Letzte Abholung für Profil ${profileId} konnte nicht gespeichert werden:`, error.message);
}
}
}
function scheduleDormantMembershipCheck(sessionId) {
const cronExpression = '0 4 */14 * *';
const job = cron.schedule(
cronExpression,
() => {
checkDormantMembers(sessionId).catch((error) => {
console.error('[DORMANT] Prüfung fehlgeschlagen:', error.message);
});
},
{ timezone: 'Europe/Berlin' }
);
sessionStore.attachJob(sessionId, job);
const session = sessionStore.get(sessionId);
const profileId = session?.profile?.id;
if (!profileId) {
return;
}
const config = readConfig(profileId);
const missingIds = getMissingLastPickupStoreIds(config);
if (missingIds.length === 0) {
if (session.dormantBootstrapSignature) {
sessionStore.update(sessionId, { dormantBootstrapSignature: null });
}
return;
}
const signature = missingIds.sort().join(',');
if (session.dormantBootstrapSignature === signature) {
return;
}
sessionStore.update(sessionId, { dormantBootstrapSignature: signature });
setTimeout(() => checkDormantMembers(sessionId), randomDelayMs(30, 180));
}
async function runDormantMembershipCheck(sessionId, options = {}) {
await checkDormantMembers(sessionId, options);
}
module.exports = {
scheduleConfig,
runStoreWatchCheck,
runImmediatePickupCheck,
runDormantMembershipCheck
};