diff --git a/src/functions/attend-event/handler.ts b/src/functions/attend-event/handler.ts index bd3bca6..cd33631 100644 --- a/src/functions/attend-event/handler.ts +++ b/src/functions/attend-event/handler.ts @@ -5,7 +5,8 @@ import { middyfy } from '@libs/lambda'; import schema from './schema'; import { MongoDB, validateToken, ensureRoles } from '../../util'; -import type { UserDocument } from 'src/types'; +import type { UserDocument } from '../../types'; +import { RegistrationStatus } from '../../types'; import * as path from 'path'; import * as dotenv from 'dotenv'; dotenv.config({ path: path.resolve(process.cwd(), '.env') }); @@ -63,7 +64,7 @@ const attendEvent: ValidatedEventAPIGatewayProxyEvent = async (ev }; } - if (attendEvent.registration_status != 'checked_in') { + if (attendEvent.registration_status != RegistrationStatus.CHECKED_IN) { return { statusCode: 409, body: JSON.stringify({ diff --git a/src/functions/discord/handler.ts b/src/functions/discord/handler.ts index bd75306..3b07833 100644 --- a/src/functions/discord/handler.ts +++ b/src/functions/discord/handler.ts @@ -4,6 +4,7 @@ import { middyfy } from '@libs/lambda'; import schema from './schema'; import { MongoDB, validateToken } from '../../util'; +import { RegistrationStatus } from '../../types'; import * as discordAPI from '@libs/discord'; import * as path from 'path'; @@ -41,7 +42,7 @@ const discord: ValidatedEventAPIGatewayProxyEvent = async (event) const discordUser = await discordAPI.getDiscordUser(tokens.accessToken); await discordAPI.updateDiscordMetadata(tokens.accessToken, user.first_name + ' ' + user.last_name, { verified: new Date().toISOString(), - checked_in: user.registration_status == 'checked_in' ? 1 : 0, + checked_in: user.registration_status == RegistrationStatus.CHECKED_IN ? 1 : 0, }); await users.updateOne( diff --git a/src/functions/teams/create/handler.ts b/src/functions/teams/create/handler.ts index 974e972..c29227c 100644 --- a/src/functions/teams/create/handler.ts +++ b/src/functions/teams/create/handler.ts @@ -2,7 +2,7 @@ import type { ValidatedEventAPIGatewayProxyEvent } from '@libs/api-gateway'; import { middyfy } from '@libs/lambda'; import schema from './schema'; import { MongoDB, validateToken, teamInviteLogic } from '../../../util'; -import { UserDocument, TeamDocument } from 'src/types'; +import { UserDocument, TeamDocument, RegistrationStatus, TeamStatus, TeamRole } from '../../../types'; import { v4 as uuidv4 } from 'uuid'; import * as path from 'path'; import * as dotenv from 'dotenv'; @@ -75,7 +75,11 @@ const teamsCreate: ValidatedEventAPIGatewayProxyEvent = async (ev } // Check if auth user has valid registration status for team creation - const validStatesForTeamCreation = ['registered', 'confirmation', 'coming']; + const validStatesForTeamCreation = [ + RegistrationStatus.REGISTERED, + RegistrationStatus.CONFIRMATION, + RegistrationStatus.COMING, + ]; if (!validStatesForTeamCreation.includes(authUser.registration_status)) { return { statusCode: 400, @@ -87,7 +91,7 @@ const teamsCreate: ValidatedEventAPIGatewayProxyEvent = async (ev } // Check if user already leads a team - if (authUser.team_info?.role === 'leader') { + if (authUser.team_info?.role === TeamRole.LEADER) { return { statusCode: 400, body: JSON.stringify({ @@ -188,7 +192,7 @@ const teamsCreate: ValidatedEventAPIGatewayProxyEvent = async (ev team_id: teamId, leader_email: event.body.auth_email.toLowerCase(), members: [], - status: 'Active' as const, + status: TeamStatus.ACTIVE, team_name: teamName, created: new Date(), updated: new Date(), @@ -210,7 +214,7 @@ const teamsCreate: ValidatedEventAPIGatewayProxyEvent = async (ev confirmed_team: true, team_info: { team_id: teamId, - role: 'leader' as const, + role: TeamRole.LEADER, pending_invites: [], }, }, diff --git a/src/functions/teams/join/handler.ts b/src/functions/teams/join/handler.ts index 40dd377..2f6b6c4 100644 --- a/src/functions/teams/join/handler.ts +++ b/src/functions/teams/join/handler.ts @@ -2,7 +2,7 @@ import type { ValidatedEventAPIGatewayProxyEvent } from '@libs/api-gateway'; import { middyfy } from '@libs/lambda'; import schema from './schema'; import { MongoDB, validateToken } from '../../../util'; -import { UserDocument, TeamDocument } from '../../../types'; +import { UserDocument, TeamDocument, TeamStatus, TeamRole } from '../../../types'; import * as path from 'path'; import * as dotenv from 'dotenv'; dotenv.config({ path: path.resolve(process.cwd(), '.env') }); @@ -76,7 +76,7 @@ const teamsJoin: ValidatedEventAPIGatewayProxyEvent = async (even }; } - if (team.status !== 'Active') { + if (team.status !== TeamStatus.ACTIVE) { return { statusCode: 400, body: JSON.stringify({ @@ -117,7 +117,7 @@ const teamsJoin: ValidatedEventAPIGatewayProxyEvent = async (even confirmed_team: true, team_info: { team_id: event.body.team_id, - role: 'member', + role: TeamRole.MEMBER, pending_invites: remainingInvites, }, }, diff --git a/src/functions/teams/member-removal/handler.ts b/src/functions/teams/member-removal/handler.ts index 683154c..647cfa4 100644 --- a/src/functions/teams/member-removal/handler.ts +++ b/src/functions/teams/member-removal/handler.ts @@ -4,7 +4,7 @@ import schema from './schema'; import { MongoDB, validateToken } from '../../../util'; import * as path from 'path'; import * as dotenv from 'dotenv'; -import { UserDocument, TeamDocument } from '../../../types'; +import { UserDocument, TeamDocument, TeamStatus } from '../../../types'; dotenv.config({ path: path.resolve(process.cwd(), '.env') }); const teamsMemberRemoval: ValidatedEventAPIGatewayProxyEvent = async (event) => { @@ -52,7 +52,7 @@ const teamsMemberRemoval: ValidatedEventAPIGatewayProxyEvent = as } // Check if team is active - if (team.status !== 'Active') { + if (team.status !== TeamStatus.ACTIVE) { return { statusCode: 400, body: JSON.stringify({ diff --git a/src/functions/teams/read/handler.ts b/src/functions/teams/read/handler.ts index a29c59b..bbe4593 100644 --- a/src/functions/teams/read/handler.ts +++ b/src/functions/teams/read/handler.ts @@ -5,6 +5,7 @@ import schema from './schema'; import { ensureRoles, MongoDB, validateToken } from '../../../util'; import type { UserDocument, TeamDocument } from '../../../types'; +import { TeamStatus } from '../../../types'; import * as path from 'path'; import * as dotenv from 'dotenv'; @@ -98,7 +99,7 @@ const teamsRead: ValidatedEventAPIGatewayProxyEvent = async (even }; } - if (team.status !== 'Active') { + if (team.status !== TeamStatus.ACTIVE) { return { statusCode: 400, body: JSON.stringify({ diff --git a/src/functions/update/handler.ts b/src/functions/update/handler.ts index c5e1c00..9759ad2 100644 --- a/src/functions/update/handler.ts +++ b/src/functions/update/handler.ts @@ -7,6 +7,7 @@ import schema from './schema'; import { validateEmail } from '../../helper'; import { MongoDB, validateToken, ensureRoles } from '../../util'; +import { RegistrationStatus } from '../../types'; import * as path from 'path'; import * as dotenv from 'dotenv'; import { Document, WithId } from 'mongodb'; @@ -94,7 +95,7 @@ const update: ValidatedEventAPIGatewayProxyEvent = async (event) } // add registered_at time if status is updated - if (event.body.updates?.$set?.registration_status == 'registered') + if (event.body.updates?.$set?.registration_status == RegistrationStatus.REGISTERED) event.body.updates.$set['registered_at'] = new Date().toISOString(); // call updates @@ -150,49 +151,68 @@ interface Updates { // 2. no alteration to fields such as: _id, password, const registrationStatusGraph = { - unregistered: ['registered'], - registered: ['rejected', 'confirmation', 'waitlist'], - confirmation: ['coming', 'not_coming'], - rejected: ['checked_in'], - coming: ['not_coming', 'confirmed'], - not_coming: ['coming', 'waitlist'], - confirmed: ['checked_in'], - waitlist: ['checked_in'], - checked_in: [], + [RegistrationStatus.UNREGISTERED]: [RegistrationStatus.REGISTERED], + [RegistrationStatus.REGISTERED]: [ + RegistrationStatus.REJECTED, + RegistrationStatus.CONFIRMATION, + RegistrationStatus.WAITLIST, + ], + [RegistrationStatus.CONFIRMATION]: [RegistrationStatus.COMING, RegistrationStatus.NOT_COMING], + [RegistrationStatus.REJECTED]: [RegistrationStatus.CHECKED_IN], + [RegistrationStatus.COMING]: [RegistrationStatus.NOT_COMING, RegistrationStatus.CONFIRMED], + [RegistrationStatus.NOT_COMING]: [RegistrationStatus.COMING, RegistrationStatus.WAITLIST], + [RegistrationStatus.CONFIRMED]: [RegistrationStatus.CHECKED_IN], + [RegistrationStatus.WAITLIST]: [RegistrationStatus.CHECKED_IN], + [RegistrationStatus.CHECKED_IN]: [], }; -function isValidRegistrationStatusUpdate(current: string, goal: string): boolean { +function isValidRegistrationStatusUpdate(current: RegistrationStatus, goal: RegistrationStatus): boolean { if (current in registrationStatusGraph) return registrationStatusGraph[current].includes(goal); return false; } // return true or false whether the proposed update is valid or not -function validateUpdates(updates: Updates, registrationStatus?: string, user?: WithId): boolean | string { +function validateUpdates( + updates: Updates, + registrationStatus?: RegistrationStatus, + user?: WithId +): boolean | string { const setUpdates = updates.$set; if (setUpdates) { if ('registration_status' in setUpdates) { const currentDate = new Date(); const goalStatus = setUpdates.registration_status as string; - if (goalStatus === 'checked_in') { + if (goalStatus === RegistrationStatus.CHECKED_IN) { if (currentDate > CHECK_IN_CUT_OFF) return `Registration is closed. The cutoff date was ${CHECK_IN_CUT_OFF.toLocaleString()}.`; } - const atleastRegistered = ['confirmed', 'waitlist', 'registered', 'coming'].includes( - registrationStatus || 'unregistered' - ); - if (goalStatus === 'checked_in' && atleastRegistered) { - if (currentDate >= CHECK_IN_START_DATE || registrationStatus === 'confirmed') return true; + const atleastRegistered = [ + RegistrationStatus.CONFIRMED, + RegistrationStatus.WAITLIST, + RegistrationStatus.REGISTERED, + RegistrationStatus.COMING, + ].includes(registrationStatus || RegistrationStatus.UNREGISTERED); + if (goalStatus === RegistrationStatus.CHECKED_IN && atleastRegistered) { + if (currentDate >= CHECK_IN_START_DATE || registrationStatus === RegistrationStatus.CONFIRMED) return true; else return `Current status of this user is ${registrationStatus}. Check-in will be available after ${CHECK_IN_START_DATE.toLocaleString()}.`; } - if (!isValidRegistrationStatusUpdate(registrationStatus || 'unregistered', goalStatus)) + if ( + !isValidRegistrationStatusUpdate( + registrationStatus || RegistrationStatus.UNREGISTERED, + goalStatus as RegistrationStatus + ) + ) return `Invalid registration status update from ${registrationStatus} to ${goalStatus}`; - if ((registrationStatus === undefined || registrationStatus == 'unregistered') && goalStatus === 'registered') { + if ( + (registrationStatus === undefined || registrationStatus == RegistrationStatus.UNREGISTERED) && + goalStatus === RegistrationStatus.REGISTERED + ) { if ( [ 'email', diff --git a/src/types.ts b/src/types.ts index 920fb78..61ce0c3 100644 --- a/src/types.ts +++ b/src/types.ts @@ -1,3 +1,4 @@ +/* eslint-disable @typescript-eslint/naming-convention */ export interface TeamInvite { team_id: string; invited_by: string; @@ -12,7 +13,7 @@ export interface Failure { export interface UserTeamInfo { team_id: string | null; - role: 'leader' | 'member' | null; + role: TeamRole | null; pending_invites: TeamInvite[]; } @@ -20,7 +21,7 @@ export interface TeamDocument { team_id: string; leader_email: string; members: string[]; - status: 'Active' | 'Disbanded'; + status: TeamStatus; team_name: string; created: Date; updated: Date; @@ -78,13 +79,24 @@ export interface UserDocument { registered_at: string; } -type RegistrationStatus = - | 'unregistered' - | 'registered' - | 'rejected' - | 'confirmation' - | 'waitlist' - | 'coming' - | 'not_coming' - | 'confirmed' - | 'checked_in'; +export enum RegistrationStatus { + UNREGISTERED = 'unregistered', + REGISTERED = 'registered', + REJECTED = 'rejected', + CONFIRMATION = 'confirmation', + WAITLIST = 'waitlist', + COMING = 'coming', + NOT_COMING = 'not_coming', + CONFIRMED = 'confirmed', + CHECKED_IN = 'checked_in', +} + +export enum TeamRole { + LEADER = 'leader', + MEMBER = 'member', +} + +export enum TeamStatus { + ACTIVE = 'Active', + DISBANDED = 'Disbanded', +} diff --git a/tests/teams-create.test.ts b/tests/teams-create.test.ts index a55ac54..800c4c7 100644 --- a/tests/teams-create.test.ts +++ b/tests/teams-create.test.ts @@ -43,6 +43,7 @@ jest.mock('../src/util', () => ({ import { main } from '../src/functions/teams/create/handler'; import { createEvent, mockContext } from './helper'; import * as util from '../src/util'; +import { RegistrationStatus, TeamStatus, TeamRole } from '../src/types'; describe('Teams Create Handler', () => { const validateTokenMock = util.validateToken as jest.Mock; @@ -78,14 +79,14 @@ describe('Teams Create Handler', () => { email: 'leader@test.com', confirmed_team: false, team_info: null, - registration_status: 'registered', + registration_status: RegistrationStatus.REGISTERED, }; const mockMemberUser = { email: 'member1@test.com', confirmed_team: false, team_info: null, - registration_status: 'registered', + registration_status: RegistrationStatus.REGISTERED, }; it('should successfully create team and send invitations', async () => { @@ -109,7 +110,7 @@ describe('Teams Create Handler', () => { leader_email: 'leader@test.com', team_name: 'Test Team', members: [], - status: 'Active', + status: TeamStatus.ACTIVE, }), expect.objectContaining({ session: mockSession }) ); @@ -121,7 +122,7 @@ describe('Teams Create Handler', () => { $set: { confirmed_team: true, team_info: expect.objectContaining({ - role: 'leader', + role: TeamRole.LEADER, pending_invites: [], }), }, @@ -387,7 +388,7 @@ describe('Teams Create Handler', () => { it('should return 400 when leader has unregistered status', async () => { const unregisteredLeader = { ...mockLeaderUser, - registration_status: 'unregistered', + registration_status: RegistrationStatus.UNREGISTERED, }; mockUsersCollection.findOne.mockResolvedValue(unregisteredLeader); @@ -404,7 +405,7 @@ describe('Teams Create Handler', () => { it('should return 400 when leader has rejected status', async () => { const rejectedLeader = { ...mockLeaderUser, - registration_status: 'rejected', + registration_status: RegistrationStatus.REJECTED, }; mockUsersCollection.findOne.mockResolvedValue(rejectedLeader); @@ -422,13 +423,13 @@ describe('Teams Create Handler', () => { const unregisteredMember = { ...mockMemberUser, email: 'member1@test.com', - registration_status: 'unregistered', + registration_status: RegistrationStatus.UNREGISTERED, }; const waitlistMember = { ...mockMemberUser, email: 'member2@test.com', - registration_status: 'waitlist', + registration_status: RegistrationStatus.WAITLIST, }; mockUsersCollection.findOne @@ -443,28 +444,32 @@ describe('Teams Create Handler', () => { const body = JSON.parse(result.body); expect(body.message).toBe('Some users have invalid registration status for team creation'); expect(body.invalid_status_users).toEqual([ - { email: 'member1@test.com', status: 'unregistered' }, - { email: 'member2@test.com', status: 'waitlist' }, + { email: 'member1@test.com', status: RegistrationStatus.UNREGISTERED }, + { email: 'member2@test.com', status: RegistrationStatus.WAITLIST }, + ]); + expect(body.required_status).toEqual([ + RegistrationStatus.REGISTERED, + RegistrationStatus.CONFIRMATION, + RegistrationStatus.COMING, ]); - expect(body.required_status).toEqual(['registered', 'confirmation', 'coming']); }); it('should successfully create team when all users have valid registration statuses', async () => { const confirmationLeader = { ...mockLeaderUser, - registration_status: 'confirmation', + registration_status: RegistrationStatus.CONFIRMATION, }; const comingMember = { ...mockMemberUser, email: 'member1@test.com', - registration_status: 'coming', + registration_status: RegistrationStatus.COMING, }; const registeredMember = { ...mockMemberUser, email: 'member2@test.com', - registration_status: 'registered', + registration_status: RegistrationStatus.REGISTERED, }; mockUsersCollection.findOne diff --git a/tests/teams-member-removal.test.ts b/tests/teams-member-removal.test.ts index 8d4846f..45256a5 100644 --- a/tests/teams-member-removal.test.ts +++ b/tests/teams-member-removal.test.ts @@ -11,7 +11,7 @@ const mockTeamsCollection = { const mockDbInstance = { connect: jest.fn(), - getCollection: jest.fn((name: string) => { + getCollection: jest.fn((name) => { if (name === 'users') return mockUsersCollection; if (name === 'teams') return mockTeamsCollection; return null;