fix: calendar event UTC times and slot query range

CalDAV DTSTART/DTEND now use UTC (Z-suffix) instead of TZID local
time. Without a VTIMEZONE component, some CalDAV servers strip the
TZID on return, causing ical.js to read the times as UTC — shifting
every event 4 hours early and letting taken slots appear free.

Slot query range changed from ±6h around UTC midnight (36-hour window)
to 3AM–6AM UTC the next day (~27h) which covers the full ET business
day without pulling in afternoon events from the previous day.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
chris 2026-05-05 14:52:06 -04:00
parent bb6c8a03a7
commit 175305a28f
2 changed files with 10 additions and 26 deletions

View File

@ -20,11 +20,12 @@ export async function GET(req: NextRequest) {
return NextResponse.json({ error: 'Invalid tier' }, { status: 400 })
try {
// Widen the CalDAV query by ±6 h around the day to handle DST edge cases.
const dayStart = new Date(`${date}T00:00:00Z`)
dayStart.setUTCHours(dayStart.getUTCHours() - 6)
const dayEnd = new Date(`${date}T23:59:59Z`)
dayEnd.setUTCHours(dayEnd.getUTCHours() + 6)
// Cover the full ET day: EDT=UTC-4 (midnight=4AM UTC), EST=UTC-5 (midnight=5AM UTC).
// Start at 3AM UTC (before midnight ET in any DST state) to avoid pulling in
// afternoon events from the previous calendar day.
const dayStart = new Date(`${date}T03:00:00Z`)
const dayEnd = new Date(`${date}T06:00:00Z`)
dayEnd.setUTCDate(dayEnd.getUTCDate() + 1) // next day 6AM UTC (after last EDT slot)
const CALDAV_TIMEOUT_MS = 6_000

View File

@ -60,23 +60,6 @@ function toIcalDate(d: Date): string {
return d.toISOString().replace(/[-:]/g, '').replace(/\.\d{3}/, '')
}
/** Local time in America/New_York for DTSTART/DTEND — used with TZID property */
function toIcalDateET(d: Date): string {
const parts = new Intl.DateTimeFormat('en-US', {
timeZone: 'America/New_York',
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit',
hour12: false,
}).formatToParts(d)
const p: Record<string, string> = {}
for (const { type, value } of parts) p[type] = value
const h = p.hour === '24' ? '00' : p.hour // Intl can emit '24' for midnight
return `${p.year}${p.month}${p.day}T${h}${p.minute}${p.second}`
}
export interface CalendarLineItem {
name: string
@ -152,8 +135,8 @@ export async function createDeliveryEvent(params: {
'BEGIN:VEVENT',
`UID:${uid}`,
`DTSTAMP:${toIcalDate(new Date())}`,
`DTSTART;TZID=America/New_York:${toIcalDateET(startTime)}`,
`DTEND;TZID=America/New_York:${toIcalDateET(endTime)}`,
`DTSTART:${toIcalDate(startTime)}`,
`DTEND:${toIcalDate(endTime)}`,
foldLine(`SUMMARY:${icalEscape(customerName)}`),
foldLine(`LOCATION:${icalEscape(address)}`),
foldLine(`DESCRIPTION:${icalEscape(descParts)}`),
@ -213,8 +196,8 @@ export async function createPickupEvent(params: {
'BEGIN:VEVENT',
`UID:${uid}`,
`DTSTAMP:${toIcalDate(new Date())}`,
`DTSTART;TZID=America/New_York:${toIcalDateET(startTime)}`,
`DTEND;TZID=America/New_York:${toIcalDateET(endTime)}`,
`DTSTART:${toIcalDate(startTime)}`,
`DTEND:${toIcalDate(endTime)}`,
foldLine(`SUMMARY:${icalEscape(customerName)}`),
'LOCATION:Beach Party Balloons\\, 554 Boston Post Rd\\, Milford CT',
foldLine(`DESCRIPTION:${icalEscape(descParts)}`),