diff --git a/packages/core/src/config.test.ts b/packages/core/src/config.test.ts index f58c62b..7b95c5a 100644 --- a/packages/core/src/config.test.ts +++ b/packages/core/src/config.test.ts @@ -9,6 +9,7 @@ describe('Config', () => { GOOGLE_AUTH_SUBJECT: 'test@example.com', CALENDAR_IDS: 'calendar1,calendar2', PRIVATE_CALENDAR_IDS: 'calendar3,calendar4', + TIMEZONE: 'Asia/Tokyo', REMINDER_SETTINGS: JSON.stringify([ { minutesBefore: 10, notificationType: 'email' }, { hour: 9, minute: 0, notificationType: 'sms', target: '+1234567890' }, @@ -27,7 +28,7 @@ describe('Config', () => { GOOGLE_AUTH_SUBJECT: 'test@example.com', CALENDAR_IDS: ['calendar1', 'calendar2'], PRIVATE_CALENDAR_IDS: ['calendar3', 'calendar4'], - TIMEZONE: 'UTC', + TIMEZONE: 'Asia/Tokyo', REMINDER_SETTINGS: [ { minutesBefore: 10, notificationType: 'email' }, { hour: 9, minute: 0, notificationType: 'sms', target: '+1234567890' }, diff --git a/packages/core/src/config.ts b/packages/core/src/config.ts index 417b23a..05bd2ed 100644 --- a/packages/core/src/config.ts +++ b/packages/core/src/config.ts @@ -63,6 +63,7 @@ export function parseConfig(env: NodeJS.ProcessEnv): Config { REMINDER_TEMPLATE: env.REMINDER_TEMPLATE, AUTH_PROVIDER: env.AUTH_PROVIDER, WEBHOOK_URL: env.WEBHOOK_URL, + TIMEZONE: env.TIMEZONE, GROUP_PROVIDER: env.GROUP_PROVIDER, GROUP_CUSTOMER_ID: env.GROUP_CUSTOMER_ID, }) diff --git a/packages/usecase/src/process_reminders.test.ts b/packages/usecase/src/process_reminders.test.ts index 20e7ce7..7c06420 100644 --- a/packages/usecase/src/process_reminders.test.ts +++ b/packages/usecase/src/process_reminders.test.ts @@ -124,14 +124,14 @@ describe('processReminders', () => { }) it('should send notifications for day before at specific hour', async () => { - const baseTime = parseISO('2023-06-01T19:00:00Z') // UTC 19:00 - const eventStart = parseISO('2023-06-02T10:00:00Z') + const baseTime = parseISO('2023-06-01T10:00:00Z') // UTC 10:00 = 19:00 JST + const eventStart = parseISO('2023-06-02T01:00:00Z') // UTC 01:00 = 10:00 JST vi.mocked(mockCalendarRepository.getEvents).mockResolvedValue([ { id: '1', start: eventStart.toISOString(), - end: parseISO('2023-06-02T11:00:00Z').toISOString(), + end: parseISO('2023-06-02T02:00:00Z').toISOString(), title: 'Event 1', people: [{ email: 'user1@example.com', organizer: false }], }, @@ -162,14 +162,14 @@ describe('processReminders', () => { }) it('should handle multiple attendees for the same event', async () => { - const baseTime = parseISO('2023-06-01T19:00:00Z') - const eventStart = parseISO('2023-06-02T10:10:00Z') + const baseTime = parseISO('2023-06-01T10:00:00Z') // UTC 10:00 = 19:00 JST + const eventStart = parseISO('2023-06-02T01:00:00Z') // UTC 01:00 = 10:00 JST vi.mocked(mockCalendarRepository.getEvents).mockResolvedValue([ { id: '1', start: eventStart.toISOString(), - end: parseISO('2023-06-02T11:10:00Z').toISOString(), + end: parseISO('2023-06-02T02:00:00Z').toISOString(), title: 'Event 1', people: [ { email: 'user1@example.com', organizer: false }, @@ -209,22 +209,22 @@ describe('processReminders', () => { }) it('should handle multiple reminder settings for the same user', async () => { - 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 + const baseTime = parseISO('2023-06-01T10:00:00Z') // UTC 10:00 = 19:00 JST + const event1Start = parseISO('2023-06-01T10:10:00Z') // For minutesBefore + const event2Start = parseISO('2023-06-02T01:00:00Z') // UTC 01:00 = 10:00 JST next day vi.mocked(mockCalendarRepository.getEvents).mockResolvedValue([ { id: '1', start: event1Start.toISOString(), - end: parseISO('2023-06-01T20:10:00Z').toISOString(), + end: parseISO('2023-06-01T11:10:00Z').toISOString(), title: 'Event 1', people: [{ email: 'user1@example.com', organizer: false }], }, { id: '2', start: event2Start.toISOString(), - end: parseISO('2023-06-02T11:10:00Z').toISOString(), + end: parseISO('2023-06-02T02:00:00Z').toISOString(), title: 'Event 2', people: [{ email: 'user1@example.com', organizer: false }], }, @@ -310,4 +310,95 @@ describe('processReminders', () => { expect(mockConsoleNotificationRepository.notify).not.toHaveBeenCalled() expect(mockWebhookNotificationRepository.notify).not.toHaveBeenCalled() }) + + describe('with Asia/Tokyo timezone', () => { + beforeEach(() => { + vi.resetModules() + 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: 'Asia/Tokyo', + }, + })) + }) + + it('should send notifications for day before at specific hour in JST', async () => { + // JST 19:00 = UTC 10:00 + const baseTime = parseISO('2023-06-01T10:00:00Z') + const eventStart = parseISO('2023-06-02T01:00:00Z') // JST 10:00 + + vi.mocked(mockCalendarRepository.getEvents).mockResolvedValue([ + { + id: '1', + start: eventStart.toISOString(), + end: parseISO('2023-06-02T02:00:00Z').toISOString(), + title: 'Event 1', + people: [{ email: 'user1@example.com', organizer: false }], + }, + ]) + + const reminderSettingsMap: Record = { + 'user1@example.com': [{ hour: 19, minute: 0, notificationType: 'console' }], + } + + vi.mocked(mockReminderSettingsRepository.getReminderSettings).mockImplementation(async (userKey) => { + return reminderSettingsMap[userKey] ?? [] + }) + + await processReminders({ + baseTime, + calendarRepositories: [mockCalendarRepository], + groupRepository: undefined, + notificationRepositories: { + console: mockConsoleNotificationRepository, + }, + reminderSettingsRepository: mockReminderSettingsRepository, + }) + + expect(mockConsoleNotificationRepository.notify).toHaveBeenCalledWith( + 'user1@example.com', + 'Custom reminder: Event 1 tomorrow at 19:00.', + ) + }) + + it('should handle date boundary cases in JST', async () => { + // JST 00:00 = UTC 15:00 previous day + const baseTime = parseISO('2023-06-01T15:00:00Z') + const eventStart = parseISO('2023-06-02T15:00:00Z') // JST 00:00 next day + + vi.mocked(mockCalendarRepository.getEvents).mockResolvedValue([ + { + id: '1', + start: eventStart.toISOString(), + end: parseISO('2023-06-02T16:00:00Z').toISOString(), + title: 'Event 1', + people: [{ email: 'user1@example.com', organizer: false }], + }, + ]) + + const reminderSettingsMap: Record = { + 'user1@example.com': [{ hour: 0, minute: 0, notificationType: 'console' }], + } + + vi.mocked(mockReminderSettingsRepository.getReminderSettings).mockImplementation(async (userKey) => { + return reminderSettingsMap[userKey] ?? [] + }) + + await processReminders({ + baseTime, + calendarRepositories: [mockCalendarRepository], + groupRepository: undefined, + notificationRepositories: { + console: mockConsoleNotificationRepository, + }, + reminderSettingsRepository: mockReminderSettingsRepository, + }) + + expect(mockConsoleNotificationRepository.notify).toHaveBeenCalledWith( + 'user1@example.com', + 'Custom reminder: Event 1 tomorrow at 00:00.', + ) + }) + }) })