diff --git a/api/src/controllers/eventControllers/createEvent.controller.js b/api/src/controllers/eventControllers/createEvent.controller.js new file mode 100644 index 0000000..9914e1b --- /dev/null +++ b/api/src/controllers/eventControllers/createEvent.controller.js @@ -0,0 +1,122 @@ +const schedule = require('node-schedule'); +const { Event } = require('./../../utils/db'); +const googleCalendar = require('./../../utils/calendar/googleCalendar'); +const appSlack = require('./../../utils/slack/appSlack'); +const { serverConfig } = require('./../../config'); + +const EVENT_TYPES = ['daily message', 'celebration', 'meet']; + +const sendDailyMessage = async (eventId, dailyMessage) => { + const response = await appSlack.client.chat.postMessage({ + channel: serverConfig.channel_slack_celebration, + text: dailyMessage + }); + + if (response && response.ts) { + const eventRecord = await Event.findByPk(eventId); + if (eventRecord) { + eventRecord.ts_daily_message = response.ts; + eventRecord.sent = true; + await eventRecord.save(); + } + } +}; + +const scheduleDailyMessage = (eventRecord) => { + if (!eventRecord.daily_message) { + return; + } + + schedule.scheduleJob(new Date(eventRecord.start), () => + sendDailyMessage(eventRecord.id, eventRecord.daily_message) + ); +}; + +const buildCalendarEvent = ({ name, start, end, calendar_description, type, slack_message }) => { + const requestBody = { + summary: name, + description: calendar_description || slack_message || '', + start: { dateTime: start }, + end: { dateTime: end } + }; + + if (type === 'meet') { + requestBody.conferenceData = { + createRequest: { + requestId: `meet-${Date.now()}` + } + }; + } + + return requestBody; +}; + +module.exports = async (req, res) => { + const { name, start, end, type, calendar_description, daily_message, slack_message } = req.body; + + if (!name || !start || !end || !type) { + return res.status(200).json({ + successful: false, + message: 'missing event data' + }); + } + + if (!EVENT_TYPES.includes(type)) { + return res.status(200).json({ + successful: false, + message: 'invalid event type' + }); + } + + if (type === 'daily message' && !daily_message) { + return res.status(200).json({ + successful: false, + message: 'daily message is required' + }); + } + + try { + const calendarResponse = await googleCalendar.events.insert({ + calendarId: serverConfig.calendar_celebration_id, + conferenceDataVersion: type === 'meet' ? 1 : 0, + requestBody: buildCalendarEvent({ + name, + start, + end, + calendar_description, + type, + slack_message + }) + }); + + const googleEvent = calendarResponse.data; + const eventRecord = await Event.create({ + name, + start, + end, + type, + calendar_description, + daily_message, + slack_message, + calendar_id: serverConfig.calendar_celebration_id, + calendar_event_id: googleEvent.id, + link_meet: type === 'meet' ? googleEvent.hangoutLink : null + }); + + if (type === 'daily message') { + scheduleDailyMessage(eventRecord); + } + + return res.status(200).json({ + successful: true, + message: 'event created successfully', + event: eventRecord + }); + } catch (error) { + console.error(error); + return res.status(200).json({ + successful: false, + message: 'error server' + }); + } +}; diff --git a/api/src/controllers/eventControllers/index.js b/api/src/controllers/eventControllers/index.js new file mode 100644 index 0000000..fccd460 --- /dev/null +++ b/api/src/controllers/eventControllers/index.js @@ -0,0 +1,3 @@ +const createEvent = require('./createEvent.controller'); + +module.exports = { createEvent }; diff --git a/api/src/routes/event.router.js b/api/src/routes/event.router.js new file mode 100644 index 0000000..38ef0b1 --- /dev/null +++ b/api/src/routes/event.router.js @@ -0,0 +1,8 @@ +const express = require('express'); +const router = express.Router(); +const { createEvent } = require('./../controllers/eventControllers'); +const auth = require('./../middlewares/auth'); + +router.post('/', auth, createEvent); + +module.exports = router; diff --git a/api/src/routes/index.js b/api/src/routes/index.js index 438a559..b4c0546 100644 --- a/api/src/routes/index.js +++ b/api/src/routes/index.js @@ -9,5 +9,6 @@ router.use('/template', require('./template.router.js')); router.use('/role', require('./role.router.js')); router.use('/permission', require('./permission.router.js')); router.use('/session', require('./session.router.js')); +router.use('/event', require('./event.router.js')); module.exports = router; diff --git a/api/tests/event.controller.spec.js b/api/tests/event.controller.spec.js new file mode 100644 index 0000000..d3f1e5c --- /dev/null +++ b/api/tests/event.controller.spec.js @@ -0,0 +1,190 @@ +jest.mock('../src/utils/db', () => ({ + Event: { + create: jest.fn(), + findByPk: jest.fn() + } +})); + +jest.mock('../src/utils/calendar/googleCalendar', () => ({ + events: { + insert: jest.fn() + } +})); + +jest.mock('../src/utils/slack/appSlack', () => ({ + client: { + chat: { + postMessage: jest.fn() + } + } +})); + +jest.mock('node-schedule', () => ({ + scheduleJob: jest.fn() +})); + +jest.mock('../src/config', () => ({ + serverConfig: { + calendar_celebration_id: 'calendar-123', + channel_slack_celebration: 'channel-123' + } +})); + +const createEvent = require('../src/controllers/eventControllers/createEvent.controller'); +const { Event } = require('../src/utils/db'); +const googleCalendar = require('../src/utils/calendar/googleCalendar'); +const appSlack = require('../src/utils/slack/appSlack'); +const schedule = require('node-schedule'); + +const response = () => { + const res = {}; + res.status = jest.fn(() => res); + res.json = jest.fn(() => res); + return res; +}; + +describe('createEvent controller', () => { + beforeEach(() => { + jest.clearAllMocks(); + googleCalendar.events.insert.mockResolvedValue({ + data: { + id: 'calendar-event-123', + hangoutLink: 'https://meet.google.com/abc-defg-hij' + } + }); + }); + + test('creates a celebration event in the configured calendar', async () => { + const eventRecord = { id: 'event-1', type: 'celebration' }; + Event.create.mockResolvedValue(eventRecord); + + const req = { + body: { + name: 'Graduation', + type: 'celebration', + start: '2026-06-20T10:00:00.000Z', + end: '2026-06-20T11:00:00.000Z', + calendar_description: 'Celebrate the cohort', + slack_message: 'Congratulations' + } + }; + const res = response(); + + await createEvent(req, res); + + expect(googleCalendar.events.insert).toHaveBeenCalledWith( + expect.objectContaining({ + calendarId: 'calendar-123', + conferenceDataVersion: 0, + requestBody: expect.objectContaining({ + summary: 'Graduation', + description: 'Celebrate the cohort' + }) + }) + ); + expect(Event.create).toHaveBeenCalledWith( + expect.objectContaining({ + calendar_id: 'calendar-123', + calendar_event_id: 'calendar-event-123', + link_meet: null + }) + ); + expect(res.json).toHaveBeenCalledWith({ + successful: true, + message: 'event created successfully', + event: eventRecord + }); + }); + + test('stores Google Meet link for meet events', async () => { + Event.create.mockResolvedValue({ id: 'event-2', type: 'meet' }); + + await createEvent( + { + body: { + name: 'Mentor call', + type: 'meet', + start: '2026-06-20T10:00:00.000Z', + end: '2026-06-20T11:00:00.000Z' + } + }, + response() + ); + + expect(googleCalendar.events.insert).toHaveBeenCalledWith( + expect.objectContaining({ + conferenceDataVersion: 1, + requestBody: expect.objectContaining({ + conferenceData: expect.any(Object) + }) + }) + ); + expect(Event.create).toHaveBeenCalledWith( + expect.objectContaining({ + link_meet: 'https://meet.google.com/abc-defg-hij' + }) + ); + }); + + test('schedules daily Slack message and saves ts when job runs', async () => { + const eventRecord = { + id: 'event-3', + type: 'daily message', + start: '2026-06-20T10:00:00.000Z', + daily_message: 'Daily reminder' + }; + const savedRecord = { save: jest.fn() }; + Event.create.mockResolvedValue(eventRecord); + Event.findByPk.mockResolvedValue(savedRecord); + appSlack.client.chat.postMessage.mockResolvedValue({ ts: '171000.42' }); + let scheduledJob; + schedule.scheduleJob.mockImplementation((date, job) => { + scheduledJob = job; + }); + + await createEvent( + { + body: { + name: 'Daily standup', + type: 'daily message', + start: '2026-06-20T10:00:00.000Z', + end: '2026-06-20T10:15:00.000Z', + daily_message: 'Daily reminder' + } + }, + response() + ); + + expect(schedule.scheduleJob).toHaveBeenCalledWith(expect.any(Date), expect.any(Function)); + await scheduledJob(); + expect(appSlack.client.chat.postMessage).toHaveBeenCalledWith({ + channel: 'channel-123', + text: 'Daily reminder' + }); + expect(savedRecord.ts_daily_message).toBe('171000.42'); + expect(savedRecord.sent).toBe(true); + expect(savedRecord.save).toHaveBeenCalled(); + }); + + test('rejects unsupported event types', async () => { + const res = response(); + + await createEvent( + { + body: { + name: 'Other event', + type: 'workshop', + start: '2026-06-20T10:00:00.000Z', + end: '2026-06-20T11:00:00.000Z' + } + }, + res + ); + + expect(Event.create).not.toHaveBeenCalled(); + expect(res.json).toHaveBeenCalledWith({ + successful: false, + message: 'invalid event type' + }); + }); +});