diff --git a/.commitmessage b/.commitmessage index 59caafe..9f67e46 100644 --- a/.commitmessage +++ b/.commitmessage @@ -1,5 +1,5 @@ -fix: keep background jobs alive +fix: tolerate same-day pickup profile check -Add a background job orchestrator that restores profile jobs from saved credentials, -keeps pickup and store-watch schedules alive without UI logins, and exposes job -status via health/admin endpoints. +Allow configured stores to ignore a visible same-day profile pickup when booking +a future pickup slot, starting with store 33875. Keep the default profile check +behavior unchanged for all other stores. diff --git a/services/pickupScheduler.js b/services/pickupScheduler.js index 46c88d1..8b28a49 100644 --- a/services/pickupScheduler.js +++ b/services/pickupScheduler.js @@ -14,6 +14,7 @@ const { ensureSession, withSessionRetry } = require('./sessionRefresh'); const maintenanceMode = require('./maintenanceMode'); const { computeReminderSchedule } = require('./journalReminderUtils'); const { startStoreMessageWatcher } = require('./storeMessageWatcher'); +const { getBlockingProfilePickup } = require('./profileCheckTolerance'); function wait(ms) { if (!ms || ms <= 0) { @@ -959,7 +960,7 @@ async function checkEntry(sessionId, entry, settings) { () => foodsharingClient.fetchPickups(entry.id, session.cookieHeader, session), { label: 'fetchPickups' } ); - let hasProfileId = false; + const profilePickups = []; let availablePickup = null; const desiredWeekday = entry.desiredWeekday ? weekdayMap[entry.desiredWeekday] || entry.desiredWeekday : null; @@ -970,7 +971,7 @@ async function checkEntry(sessionId, entry, settings) { entry.checkProfileId && pickup.occupiedSlots?.some((slot) => String(slot.profile?.id) === String(session.profile.id)) ) { - hasProfileId = true; + profilePickups.push(pickup); } if (!matchesDesiredDate(pickupDate, entry.desiredDate, entry.desiredDateRange)) { return; @@ -983,20 +984,35 @@ async function checkEntry(sessionId, entry, settings) { } }); - if (entry.checkProfileId && hasProfileId) { + if (!availablePickup) { + console.log( + `[INFO] Kein freier Slot für ${entry.label || entry.id} in dieser Runde gefunden. Profil bereits eingetragen: ${ + profilePickups.length > 0 ? 'ja' : 'nein' + }` + ); + return; + } + + const blockingProfilePickup = entry.checkProfileId + ? getBlockingProfilePickup({ + storeId: entry.id, + profilePickups, + targetPickup: availablePickup + }) + : null; + + if (entry.checkProfileId && blockingProfilePickup) { console.log( `[INFO] Profil bereits in einem Slot für ${entry.label || entry.id} eingetragen – überspringe Buchung.` ); return; } - if (!availablePickup) { + if (entry.checkProfileId && profilePickups.length > 0 && !blockingProfilePickup) { console.log( - `[INFO] Kein freier Slot für ${entry.label || entry.id} in dieser Runde gefunden. Profil bereits eingetragen: ${ - hasProfileId ? 'ja' : 'nein' - }` + `[INFO] Heutige Profileintragung für ${entry.label || entry.id} wird toleriert; ` + + `Zieltermin: ${new Date(availablePickup.date).toLocaleString('de-DE')}.` ); - return; } if (shouldIgnoreSlot(entry, availablePickup, settings)) { @@ -1004,7 +1020,7 @@ async function checkEntry(sessionId, entry, settings) { return; } - if (!entry.checkProfileId || !hasProfileId) { + if (!entry.checkProfileId || !blockingProfilePickup) { await processBooking(session, entry, availablePickup); } } catch (error) { diff --git a/services/profileCheckTolerance.js b/services/profileCheckTolerance.js new file mode 100644 index 0000000..8002101 --- /dev/null +++ b/services/profileCheckTolerance.js @@ -0,0 +1,39 @@ +const TIME_ZONE = 'Europe/Berlin'; +const TODAY_PROFILE_CHECK_TOLERANCE_STORE_IDS = new Set(['33875']); + +function getDateKeyInTimeZone(date, timeZone = TIME_ZONE) { + const parsed = date instanceof Date ? date : new Date(date); + if (Number.isNaN(parsed.getTime())) { + return null; + } + const formatter = new Intl.DateTimeFormat('en-CA', { + timeZone, + year: 'numeric', + month: '2-digit', + day: '2-digit' + }); + return formatter.format(parsed); +} + +function getBlockingProfilePickup({ storeId, profilePickups = [], targetPickup, now = new Date() }) { + const pickUpsWithDate = profilePickups.filter((pickup) => pickup?.date); + if (pickUpsWithDate.length === 0) { + return null; + } + if (!TODAY_PROFILE_CHECK_TOLERANCE_STORE_IDS.has(String(storeId))) { + return pickUpsWithDate[0]; + } + + const todayKey = getDateKeyInTimeZone(now); + const targetKey = getDateKeyInTimeZone(targetPickup?.date); + if (!todayKey || !targetKey || targetKey === todayKey) { + return pickUpsWithDate[0]; + } + + return pickUpsWithDate.find((pickup) => getDateKeyInTimeZone(pickup.date) !== todayKey) || null; +} + +module.exports = { + TODAY_PROFILE_CHECK_TOLERANCE_STORE_IDS, + getBlockingProfilePickup +}; diff --git a/src/utils/pickupProfileTolerance.test.js b/src/utils/pickupProfileTolerance.test.js new file mode 100644 index 0000000..2911794 --- /dev/null +++ b/src/utils/pickupProfileTolerance.test.js @@ -0,0 +1,67 @@ +const { getBlockingProfilePickup } = require('../../services/profileCheckTolerance'); + +const NOW = new Date('2026-07-16T10:00:00+02:00'); + +function pickup(date) { + return { date }; +} + +describe('pickup profile check tolerance', () => { + test('blocks today profile pickup for regular stores', () => { + const blocking = getBlockingProfilePickup({ + storeId: '44975', + profilePickups: [pickup('2026-07-16T17:00:00+02:00')], + targetPickup: pickup('2026-07-17T17:00:00+02:00'), + now: NOW + }); + + expect(blocking).toEqual(pickup('2026-07-16T17:00:00+02:00')); + }); + + test('allows future target for tolerated store when profile pickup is today', () => { + const blocking = getBlockingProfilePickup({ + storeId: '33875', + profilePickups: [pickup('2026-07-16T17:00:00+02:00')], + targetPickup: pickup('2026-07-17T17:00:00+02:00'), + now: NOW + }); + + expect(blocking).toBeNull(); + }); + + test('blocks same-day target for tolerated store', () => { + const blocking = getBlockingProfilePickup({ + storeId: '33875', + profilePickups: [pickup('2026-07-16T12:00:00+02:00')], + targetPickup: pickup('2026-07-16T18:00:00+02:00'), + now: NOW + }); + + expect(blocking).toEqual(pickup('2026-07-16T12:00:00+02:00')); + }); + + test('blocks non-today profile pickups for tolerated store', () => { + const blocking = getBlockingProfilePickup({ + storeId: '33875', + profilePickups: [pickup('2026-07-17T17:00:00+02:00')], + targetPickup: pickup('2026-07-18T17:00:00+02:00'), + now: NOW + }); + + expect(blocking).toEqual(pickup('2026-07-17T17:00:00+02:00')); + }); + + test('blocks when any profile pickup is outside the tolerated day', () => { + const blocking = getBlockingProfilePickup({ + storeId: '33875', + profilePickups: [ + pickup('2026-07-16T17:00:00+02:00'), + pickup('2026-07-18T17:00:00+02:00') + ], + targetPickup: pickup('2026-07-19T17:00:00+02:00'), + now: NOW + }); + + expect(blocking).toEqual(pickup('2026-07-18T17:00:00+02:00')); + }); +});