diff --git a/api/src/controllers/eventControllers/createEvent.controller.js b/api/src/controllers/eventControllers/createEvent.controller.js new file mode 100644 index 0000000..eb7105d --- /dev/null +++ b/api/src/controllers/eventControllers/createEvent.controller.js @@ -0,0 +1,111 @@ +const { Event } = require('./../../utils/db'); +const { serverConfig } = require('./../../config/index'); +const googleCalendar = require('./../../utils/calendar/googleCalendar'); +const scheduleDailyMessage = require('./../../utils/schedule/scheduleDailyMessage'); + +const VALID_EVENT_TYPES = ['daily message', 'celebration', 'meet']; + +const getEventType = ({ type, daily_message }) => { + if (type) { + return type; + } + return daily_message ? 'daily message' : 'celebration'; +}; + +const getDate = (date) => { + const parsedDate = new Date(date); + return Number.isNaN(parsedDate.getTime()) ? null : parsedDate; +}; + +const createCalendarEvent = async ({ name, startDate, endDate, calendar_description, eventType }) => { + const requestBody = { + summary: name, + description: calendar_description, + start: { + dateTime: startDate.toISOString() + }, + end: { + dateTime: endDate.toISOString() + } + }; + + const calendarRequest = { + calendarId: serverConfig.calendar_celebration_id, + requestBody + }; + + if (eventType === 'meet') { + calendarRequest.conferenceDataVersion = 1; + requestBody.conferenceData = { + createRequest: { + requestId: `meet-${Date.now()}` + } + }; + } + + return googleCalendar.events.insert(calendarRequest); +}; + +module.exports = async (req, res) => { + try { + const { name, start, end, calendar_description, daily_message, slack_message } = req.body; + const eventType = getEventType(req.body); + const startDate = getDate(start); + const endDate = getDate(end); + + if (!name || !start || !end || !calendar_description) { + return res.status(200).json({ successful: false, message: 'missing data' }); + } + + if (!VALID_EVENT_TYPES.includes(eventType)) { + return res.status(200).json({ successful: false, message: 'invalid event type' }); + } + + if (!startDate || !endDate || endDate <= startDate) { + return res.status(200).json({ successful: false, message: 'invalid event dates' }); + } + + if (!serverConfig.calendar_celebration_id) { + return res.status(200).json({ successful: false, message: 'missing calendar configuration' }); + } + + if (eventType === 'daily message' && !daily_message) { + return res.status(200).json({ successful: false, message: 'missing daily message' }); + } + + const responseCalendarEvent = await createCalendarEvent({ + name, + startDate, + endDate, + calendar_description, + eventType + }); + + const calendarEvent = responseCalendarEvent.data; + const eventRecord = await Event.create({ + name, + calendar_id: serverConfig.calendar_celebration_id, + calendar_event_id: calendarEvent.id, + start: startDate, + end: endDate, + type: eventType, + calendar_description, + daily_message, + slack_message, + link_meet: eventType === 'meet' ? calendarEvent.hangoutLink : req.body.link_meet + }); + + if (eventType === 'daily message') { + scheduleDailyMessage(eventRecord); + } + + return res.status(200).json({ + successful: true, + message: 'event created successfully', + event: eventRecord + }); + } catch (error) { + console.log(error); + return res.status(400).json({ successful: false, message: error.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..4a3d1d4 --- /dev/null +++ b/api/src/controllers/eventControllers/index.js @@ -0,0 +1,5 @@ +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..ce5a920 --- /dev/null +++ b/api/src/routes/event.router.js @@ -0,0 +1,9 @@ +const express = require('express'); +const router = express.Router(); +const { createEvent } = require('./../controllers/eventControllers'); +const auth = require('./../middlewares/auth'); +const checkPermissions = require('./../middlewares/checkPermissions'); + +router.post('/', auth, checkPermissions(['write:event']), 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/src/utils/schedule/scheduleDailyMessage.js b/api/src/utils/schedule/scheduleDailyMessage.js new file mode 100644 index 0000000..597424a --- /dev/null +++ b/api/src/utils/schedule/scheduleDailyMessage.js @@ -0,0 +1,27 @@ +const schedule = require('node-schedule'); +const { serverConfig } = require('./../../config/index'); + +module.exports = (eventRecord) => { + const runAt = new Date(eventRecord.start); + + if (!serverConfig.channel_slack_celebration || Number.isNaN(runAt.getTime())) { + return null; + } + + return schedule.scheduleJob(runAt, async () => { + try { + const appSlack = require('./../slack/appSlack'); + const response = await appSlack.client.chat.postMessage({ + channel: serverConfig.channel_slack_celebration, + text: eventRecord.daily_message + }); + + await eventRecord.update({ + ts_daily_message: response.ts, + sent: true + }); + } catch (error) { + console.log(error); + } + }); +}; diff --git a/api/tests/event.controller.spec.js b/api/tests/event.controller.spec.js new file mode 100644 index 0000000..99638a6 --- /dev/null +++ b/api/tests/event.controller.spec.js @@ -0,0 +1,117 @@ +jest.mock('./../src/utils/db', () => ({ + Event: { + create: jest.fn() + } +})); + +jest.mock('./../src/config/index', () => ({ + serverConfig: { + calendar_celebration_id: 'calendar-id', + channel_slack_celebration: 'slack-channel-id' + } +})); + +jest.mock('./../src/utils/calendar/googleCalendar', () => ({ + events: { + insert: jest.fn() + } +})); + +jest.mock('./../src/utils/schedule/scheduleDailyMessage', () => jest.fn()); + +const { Event } = require('./../src/utils/db'); +const googleCalendar = require('./../src/utils/calendar/googleCalendar'); +const scheduleDailyMessage = require('./../src/utils/schedule/scheduleDailyMessage'); +const createEvent = require('./../src/controllers/eventControllers/createEvent.controller'); + +const createResponse = () => { + 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: 'google-event-id', + hangoutLink: 'https://meet.google.com/abc-defg-hij' + } + }); + Event.create.mockImplementation(async (event) => ({ + ...event, + id: 'event-id', + update: jest.fn() + })); + }); + + test('creates a daily message event and schedules the Slack message', async () => { + const req = { + body: { + name: 'Fullstack', + start: '2026-05-09T23:19:34.000Z', + end: '2026-05-09T23:29:34.000Z', + calendar_description: 'calendar text', + daily_message: 'daily text', + slack_message: 'slack text' + } + }; + const res = createResponse(); + + await createEvent(req, res); + + expect(googleCalendar.events.insert).toHaveBeenCalledWith( + expect.objectContaining({ + calendarId: 'calendar-id', + requestBody: expect.objectContaining({ + summary: 'Fullstack', + description: 'calendar text' + }) + }) + ); + expect(Event.create).toHaveBeenCalledWith( + expect.objectContaining({ + calendar_event_id: 'google-event-id', + type: 'daily message', + daily_message: 'daily text', + slack_message: 'slack text' + }) + ); + expect(scheduleDailyMessage).toHaveBeenCalled(); + expect(res.status).toHaveBeenCalledWith(200); + expect(res.json).toHaveBeenCalledWith(expect.objectContaining({ successful: true })); + }); + + test('asks Google Calendar to create a Meet link for meet events', async () => { + const req = { + body: { + name: 'Meet', + type: 'meet', + start: '2026-05-09T23:19:34.000Z', + end: '2026-05-09T23:29:34.000Z', + calendar_description: 'calendar text' + } + }; + const res = createResponse(); + + await createEvent(req, res); + + expect(googleCalendar.events.insert).toHaveBeenCalledWith( + expect.objectContaining({ + conferenceDataVersion: 1, + requestBody: expect.objectContaining({ + conferenceData: expect.any(Object) + }) + }) + ); + expect(Event.create).toHaveBeenCalledWith( + expect.objectContaining({ + type: 'meet', + link_meet: 'https://meet.google.com/abc-defg-hij' + }) + ); + expect(scheduleDailyMessage).not.toHaveBeenCalled(); + }); +});