Skip to content

Commit

Permalink
handle time zone
Browse files Browse the repository at this point in the history
  • Loading branch information
yamitzky committed Feb 13, 2025
1 parent 0fce051 commit 20e07ae
Show file tree
Hide file tree
Showing 7 changed files with 57 additions and 25 deletions.
6 changes: 1 addition & 5 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,7 @@
"bin": {
"synk-cal": "./bin/server.js"
},
"files": [
"bin",
"public",
"build"
],
"files": ["bin", "public", "build"],
"scripts": {
"build": "remix vite:build",
"dev": "remix vite:dev",
Expand Down
2 changes: 2 additions & 0 deletions packages/core/src/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ describe('Config', () => {
expect(parsedConfig).toEqual({
GOOGLE_AUTH_SUBJECT: '[email protected]',
CALENDAR_IDS: ['calendar1', 'calendar2'],
TIMEZONE: 'UTC',
REMINDER_SETTINGS: [
{ minutesBefore: 10, notificationType: 'email' },
{ hour: 9, minute: 0, notificationType: 'sms', target: '+1234567890' },
Expand All @@ -49,6 +50,7 @@ describe('Config', () => {
expect(parsedConfig).toEqual({
GOOGLE_AUTH_SUBJECT: undefined,
CALENDAR_IDS: ['calendar1'],
TIMEZONE: 'UTC',
REMINDER_SETTINGS: [],
REMINDER_TEMPLATE: undefined,
WEBHOOK_URL: undefined,
Expand Down
1 change: 1 addition & 0 deletions packages/core/src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ export const ConfigSchema = v.object({
v.optional(v.string()),
v.transform((value) => value?.split(',') ?? []),
),
TIMEZONE: v.optional(v.string(), 'UTC'),
REMINDER_SETTINGS: v.pipe(
v.optional(v.string()),
v.transform((value) => {
Expand Down
1 change: 1 addition & 0 deletions packages/usecase/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
"@synk-cal/core": "workspace:*",
"@synk-cal/repository": "workspace:*",
"date-fns": "^4.1.0",
"date-fns-tz": "^3.2.0",
"eta": "^3.5.0"
},
"devDependencies": {
Expand Down
27 changes: 18 additions & 9 deletions packages/usecase/src/process_reminders.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { processReminders } from './process_reminders'
vi.mock('@synk-cal/core', () => ({
config: {
REMINDER_TEMPLATE: 'Custom reminder: <%= it.title %> <%= it.minutesBefore ? `in ${it.minutesBefore} minutes` : `tomorrow at ${String(it.hour).padStart(2, "0")}:${String(it.minute).padStart(2, "0")}` %>.',
TIMEZONE: 'UTC',
},
}))

Expand Down Expand Up @@ -116,7 +117,7 @@ describe('processReminders', () => {
})

it('should send notifications for day before at specific hour', async () => {
const baseTime = parseISO('2023-06-01T10:00:00Z') // 19:00 JST
const baseTime = parseISO('2023-06-01T19:00:00Z') // UTC 19:00
const eventStart = parseISO('2023-06-02T10:00:00Z')

vi.mocked(mockCalendarRepository.getEvents).mockResolvedValue([
Expand Down Expand Up @@ -153,14 +154,14 @@ describe('processReminders', () => {
})

it('should handle multiple attendees for the same event', async () => {
const baseTime = parseISO('2023-06-01T10:00:00Z')
const eventStart = parseISO('2023-06-01T10:10:00Z')
const baseTime = parseISO('2023-06-01T19:00:00Z')
const eventStart = parseISO('2023-06-02T10:10:00Z')

vi.mocked(mockCalendarRepository.getEvents).mockResolvedValue([
{
id: '1',
start: eventStart.toISOString(),
end: parseISO('2023-06-01T11:10:00Z').toISOString(),
end: parseISO('2023-06-02T11:10:00Z').toISOString(),
title: 'Event 1',
people: [
{ email: '[email protected]', organizer: false },
Expand Down Expand Up @@ -200,17 +201,25 @@ describe('processReminders', () => {
})

it('should handle multiple reminder settings for the same user', async () => {
const baseTime = parseISO('2023-06-01T10:00:00Z')
const eventStart = parseISO('2023-06-01T10:10:00Z')
const baseTime = parseISO('2023-06-01T19:00:00Z')
const event1Start = parseISO('2023-06-01T19:10:00Z') // For minutesBefore
const event2Start = parseISO('2023-06-02T10:10:00Z') // For hour/minute

vi.mocked(mockCalendarRepository.getEvents).mockResolvedValue([
{
id: '1',
start: eventStart.toISOString(),
end: parseISO('2023-06-01T11:10:00Z').toISOString(),
start: event1Start.toISOString(),
end: parseISO('2023-06-01T20:10:00Z').toISOString(),
title: 'Event 1',
people: [{ email: '[email protected]', organizer: false }],
},
{
id: '2',
start: event2Start.toISOString(),
end: parseISO('2023-06-02T11:10:00Z').toISOString(),
title: 'Event 2',
people: [{ email: '[email protected]', organizer: false }],
},
])

const reminderSettingsMap: Record<string, ReminderSetting[]> = {
Expand Down Expand Up @@ -241,7 +250,7 @@ describe('processReminders', () => {

expect(mockWebhookNotificationRepository.notify).toHaveBeenCalledWith(
'[email protected]',
'Custom reminder: Event 1 tomorrow at 19:00.',
'Custom reminder: Event 2 tomorrow at 19:00.',
)
})

Expand Down
33 changes: 22 additions & 11 deletions packages/usecase/src/process_reminders.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,10 +2,11 @@ import type {
CalendarRepository,
NotificationRepository,
ReminderSettingsRepository,
ReminderTiming,
ReminderSetting,
} from '@synk-cal/core'
import { config } from '@synk-cal/core'
import { addDays, isSameHour, isSameMinute, parseISO, setHours, setMinutes, subMinutes } from 'date-fns'
import { addDays, addMinutes, isSameMinute, parseISO, subMinutes } from 'date-fns'
import { formatInTimeZone, toZonedTime, fromZonedTime } from 'date-fns-tz'
import { Eta } from 'eta'

const DEFAULT_TEMPLATE = `Reminder: "<%= it.title %>" starts <%=
Expand All @@ -14,11 +15,22 @@ const DEFAULT_TEMPLATE = `Reminder: "<%= it.title %>" starts <%=
: \`tomorrow at \${String(it.hour).padStart(2, '0')}:\${String(it.minute).padStart(2, '0')}\`
%>.`

const getReminderTime = (eventStart: Date, setting: ReminderTiming) => {
const getReminderTime = (eventStart: Date, setting: ReminderSetting) => {
const timezone = config.TIMEZONE

if ('minutesBefore' in setting) {
return subMinutes(eventStart, setting.minutesBefore)
} else {
return setMinutes(setHours(addDays(eventStart, -1), setting.hour), setting.minute)
// イベント開始時刻の前日の指定時刻を設定
const reminderDateStr = formatInTimeZone(
addDays(eventStart, -1),
timezone,
'yyyy-MM-dd'
)
// 指定された時刻を設定
const reminderTimeStr = `${reminderDateStr}T${String(setting.hour).padStart(2, '0')}:${String(setting.minute).padStart(2, '0')}:00`
// タイムゾーンを考慮してUTCに変換
return fromZonedTime(reminderTimeStr, timezone)
}
}

Expand All @@ -31,15 +43,18 @@ export async function processReminders(
const eta = new Eta()

// Get all events first
const maxMinutesAfter = 24 * 60 // 1日後まで
const events = (
await Promise.all(
calendarRepositories.map((calendarRepository) => calendarRepository.getEvents(baseTime, addDays(baseTime, 1))),
calendarRepositories.map((calendarRepository) =>
calendarRepository.getEvents(baseTime, addDays(baseTime, 1)),
),
)
).flat()
console.debug('Fetched events', events.map((e) => `${e.title} (${e.start})`).join('\n'))

const notifications: Array<{ repository: NotificationRepository; target: string; message: string }> = []
const reminderSettingsCache = new Map<string, Array<ReminderTiming & { notificationType: string }>>()
const reminderSettingsCache = new Map<string, ReminderSetting[]>()

// Process each event
for (const event of events) {
Expand Down Expand Up @@ -75,11 +90,7 @@ export async function processReminders(
}

const reminderTime = getReminderTime(parseISO(event.start), setting)
const isTimeMatch =
'minutesBefore' in setting
? isSameMinute(reminderTime, baseTime)
: isSameHour(baseTime, setHours(baseTime, setting.hour)) &&
isSameMinute(baseTime, setMinutes(setHours(baseTime, setting.hour), setting.minute))
const isTimeMatch = isSameMinute(reminderTime, baseTime)
if (!isTimeMatch) {
console.debug(
`Event "${event.title}" reminder time ${reminderTime} does not match current time ${baseTime} for ${attendee.email}`,
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 20e07ae

Please sign in to comment.