Skip to content

Commit 9e50f2d

Browse files
committed
feat: added schema pref routes
1 parent 41ff02a commit 9e50f2d

File tree

9 files changed

+821
-44
lines changed

9 files changed

+821
-44
lines changed
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
/*
2+
Warnings:
3+
4+
- A unique constraint covering the columns `[user_id,event_id]` on the table `Guest` will be added. If there are existing duplicate values, this will fail.
5+
6+
*/
7+
-- CreateTable
8+
CREATE TABLE "RsvpPreferences" (
9+
"id" TEXT NOT NULL,
10+
"event_id" TEXT NOT NULL,
11+
"group_id" TEXT,
12+
"rsvp_lock_date" TIMESTAMP(3) NOT NULL,
13+
"collect_attendance" BOOLEAN NOT NULL DEFAULT true,
14+
"collect_guest_count" BOOLEAN NOT NULL DEFAULT true,
15+
"collect_food" BOOLEAN NOT NULL DEFAULT false,
16+
"collect_alcohol" BOOLEAN NOT NULL DEFAULT false,
17+
"collect_accommodation" BOOLEAN NOT NULL DEFAULT false,
18+
"accommodation_details" TEXT,
19+
"collect_transport" BOOLEAN NOT NULL DEFAULT false,
20+
"transport_details" TEXT,
21+
"additional_notes" TEXT,
22+
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
23+
"updated_at" TIMESTAMP(3) NOT NULL,
24+
25+
CONSTRAINT "RsvpPreferences_pkey" PRIMARY KEY ("id")
26+
);
27+
28+
-- CreateIndex
29+
CREATE INDEX "RsvpPreferences_event_id_idx" ON "RsvpPreferences"("event_id");
30+
31+
-- CreateIndex
32+
CREATE UNIQUE INDEX "RsvpPreferences_event_id_group_id_key" ON "RsvpPreferences"("event_id", "group_id");
33+
34+
-- CreateIndex
35+
CREATE UNIQUE INDEX "Guest_user_id_event_id_key" ON "Guest"("user_id", "event_id");
36+
37+
-- AddForeignKey
38+
ALTER TABLE "RsvpPreferences" ADD CONSTRAINT "RsvpPreferences_event_id_fkey" FOREIGN KEY ("event_id") REFERENCES "Event"("id") ON DELETE CASCADE ON UPDATE CASCADE;
39+
40+
-- AddForeignKey
41+
ALTER TABLE "RsvpPreferences" ADD CONSTRAINT "RsvpPreferences_group_id_fkey" FOREIGN KEY ("group_id") REFERENCES "GuestGroup"("id") ON DELETE CASCADE ON UPDATE CASCADE;

prisma/schema.prisma

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,6 +116,7 @@ model Event {
116116
// Relations
117117
co_hosts User[] @relation("CoHostEvents")
118118
sub_events SubEvent[] @relation("ParentEvent")
119+
rsvpPreferences RsvpPreferences[]
119120
guests Guest[]
120121
messages Message[]
121122
invites Invite[]
@@ -249,6 +250,7 @@ model GuestGroup {
249250
inviteLinks InviteLink[]
250251
invites Invite[]
251252
events EventGuestGroup[]
253+
rsvpPreferences RsvpPreferences[]
252254
}
253255

254256
model EventGuestGroup {
@@ -261,6 +263,38 @@ model EventGuestGroup {
261263
@@id([event_id, guest_group_id])
262264
}
263265

266+
model RsvpPreferences {
267+
id String @id @default(uuid())
268+
event_id String
269+
group_id String? // null means applies to all groups
270+
271+
// General RSVP settings
272+
rsvp_lock_date DateTime // Last date to submit/update RSVP
273+
collect_attendance Boolean @default(true)
274+
collect_guest_count Boolean @default(true)
275+
collect_food Boolean @default(false)
276+
collect_alcohol Boolean @default(false)
277+
278+
// Group-specific settings
279+
collect_accommodation Boolean @default(false)
280+
accommodation_details String? // Custom text for accommodation instructions
281+
collect_transport Boolean @default(false)
282+
transport_details String? // Custom text for transport instructions
283+
284+
// Additional notes
285+
additional_notes String? // General notes for all guests in this group
286+
287+
created_at DateTime @default(now())
288+
updated_at DateTime @updatedAt
289+
290+
// Relations
291+
event Event @relation(fields: [event_id], references: [id], onDelete: Cascade)
292+
group GuestGroup? @relation(fields: [group_id], references: [id], onDelete: Cascade)
293+
294+
@@unique([event_id, group_id])
295+
@@index([event_id])
296+
}
297+
264298
model GuestGroupUsers {
265299
id String @id @default(uuid())
266300
guest_group_id String

src/routes/eventRoutes.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,15 @@ router.post('/create', verifyIdToken, async (req: Request, res: Response) => {
141141
});
142142

143143
if (success) {
144-
res.status(200).json({ message: 'Event created successfully', event })
144+
res.status(200).json({
145+
message: 'Event created successfully',
146+
event,
147+
nextSteps: {
148+
setupRsvpPreferences: `Use POST /api/events/${event?.id}/preferences to configure RSVP settings`,
149+
createGuestGroups: `Use POST /api/events/${event?.id}/groups to create guest groups`,
150+
generateInvites: `Use POST /api/invites/generate/${event?.id}/{groupId} to create invite links`
151+
}
152+
})
145153
} else {
146154
res.status(500).json({ message: error ?? 'Internal Server Error' })
147155
}

src/routes/inviteRoutes.ts

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from '../services/inviteService';
1616
import { verifyIdToken } from '../middleware/verifyIdToken';
1717
import { isEventHostOrCoHost } from '../services/guestService';
18+
import { getRsvpPreferencesForGroup } from '../services/rsvpPreferencesService';
1819

1920
const router = express.Router();
2021
const prisma = new PrismaClient(); // Add this line
@@ -406,4 +407,59 @@ router.get('/:eventId/:groupId/status/:phoneNo', async (_req: Request, res: Resp
406407
res.status(403).json({ message: 'RSVP status can be viewed and managed in the app. Please download the app to continue.' });
407408
});
408409

410+
router.get('/:eventId/:groupId/preferences', async (req: Request, res: Response) => {
411+
try {
412+
const { eventId, groupId } = req.params;
413+
414+
const result = await getRsvpPreferencesForGroup(eventId, groupId);
415+
416+
if (!result.success) {
417+
if (result.error?.includes('not found')) {
418+
res.status(404).json({ message: result.error });
419+
} else {
420+
res.status(400).json({ message: result.error });
421+
}
422+
return;
423+
}
424+
425+
// Only return the preferences data needed for form generation
426+
const preferences = result.preferences;
427+
if (!preferences) {
428+
res.status(404).json({ message: 'RSVP preferences not found' });
429+
return;
430+
}
431+
432+
res.status(200).json({
433+
eventId,
434+
groupId,
435+
formConfig: {
436+
collectAttendance: preferences.collect_attendance,
437+
collectGuestCount: preferences.collect_guest_count,
438+
collectFood: preferences.collect_food,
439+
collectAlcohol: preferences.collect_alcohol,
440+
collectAccommodation: preferences.collect_accommodation,
441+
accommodationDetails: preferences.accommodation_details,
442+
collectTransport: preferences.collect_transport,
443+
transportDetails: preferences.transport_details,
444+
additionalNotes: preferences.additional_notes,
445+
isRsvpAllowed: preferences.isRsvpAllowed,
446+
rsvpLockDate: preferences.rsvp_lock_date,
447+
daysUntilLock: preferences.daysUntilLock
448+
},
449+
event: {
450+
id: preferences.event.id,
451+
title: preferences.event.title,
452+
startDateTime: preferences.event.start_date_time
453+
},
454+
group: preferences.group ? {
455+
id: preferences.group.id,
456+
name: preferences.group.name
457+
} : null
458+
});
459+
} catch (error) {
460+
console.error('Get RSVP preferences error:', error);
461+
res.status(500).json({ message: 'Internal Server Error' });
462+
}
463+
});
464+
409465
export default router;
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
import express, { Request, Response } from 'express';
2+
import {
3+
setEventRsvpPreferences,
4+
getRsvpPreferencesForGroup,
5+
getEventRsvpPreferences,
6+
updateEventRsvpPreferences,
7+
deleteEventRsvpPreferences
8+
} from '../services/rsvpPreferencesService';
9+
import { verifyIdToken } from '../middleware/verifyIdToken';
10+
11+
const router = express.Router();
12+
13+
// Set RSVP preferences for an event (HOST/CO-HOST ONLY)
14+
router.post('/:eventId/preferences', verifyIdToken, async (req: Request, res: Response) => {
15+
try {
16+
const { eventId } = req.params;
17+
const userId = req.userId;
18+
19+
if (!userId) {
20+
res.status(401).json({ message: 'Unauthorized' });
21+
return;
22+
}
23+
24+
const {
25+
rsvp_lock_date,
26+
collect_attendance,
27+
collect_guest_count,
28+
collect_food,
29+
collect_alcohol,
30+
additional_notes,
31+
group_preferences
32+
} = req.body;
33+
34+
// Validation
35+
if (!rsvp_lock_date) {
36+
res.status(400).json({ message: 'RSVP lock date is required' });
37+
return;
38+
}
39+
40+
if (collect_attendance === undefined || collect_guest_count === undefined) {
41+
res.status(400).json({ message: 'collect_attendance and collect_guest_count are required' });
42+
return;
43+
}
44+
45+
const result = await setEventRsvpPreferences(eventId, userId, {
46+
rsvp_lock_date,
47+
collect_attendance,
48+
collect_guest_count,
49+
collect_food: collect_food || false,
50+
collect_alcohol: collect_alcohol || false,
51+
additional_notes,
52+
group_preferences: group_preferences || []
53+
});
54+
55+
if (!result.success) {
56+
res.status(400).json({ message: result.error });
57+
return;
58+
}
59+
60+
res.status(201).json({
61+
message: result.message,
62+
preferences: result.preferences
63+
});
64+
} catch (error) {
65+
console.error('Set RSVP preferences error:', error);
66+
res.status(500).json({ message: 'Internal Server Error' });
67+
}
68+
});
69+
70+
// Get RSVP preferences for an event (HOST/CO-HOST ONLY)
71+
router.get('/:eventId/preferences', verifyIdToken, async (req: Request, res: Response) => {
72+
try {
73+
const { eventId } = req.params;
74+
const userId = req.userId;
75+
76+
if (!userId) {
77+
res.status(401).json({ message: 'Unauthorized' });
78+
return;
79+
}
80+
81+
const result = await getEventRsvpPreferences(eventId, userId);
82+
83+
if (!result.success) {
84+
if (result.error?.includes('Access denied') || result.error?.includes('Only event hosts')) {
85+
res.status(403).json({ message: result.error });
86+
} else if (result.error?.includes('not found') || result.error?.includes('No RSVP preferences')) {
87+
res.status(404).json({ message: result.error });
88+
} else {
89+
res.status(400).json({ message: result.error });
90+
}
91+
return;
92+
}
93+
94+
res.status(200).json({
95+
globalPreferences: result.globalPreferences,
96+
groupPreferences: result.groupPreferences,
97+
allPreferences: result.allPreferences
98+
});
99+
} catch (error) {
100+
console.error('Get RSVP preferences error:', error);
101+
res.status(500).json({ message: 'Internal Server Error' });
102+
}
103+
});
104+
105+
// Update RSVP preferences for an event (HOST/CO-HOST ONLY)
106+
router.patch('/:eventId/preferences', verifyIdToken, async (req: Request, res: Response) => {
107+
try {
108+
const { eventId } = req.params;
109+
const userId = req.userId;
110+
111+
if (!userId) {
112+
res.status(401).json({ message: 'Unauthorized' });
113+
return;
114+
}
115+
116+
const result = await updateEventRsvpPreferences(eventId, userId, req.body);
117+
118+
if (!result.success) {
119+
if (result.error?.includes('Access denied') || result.error?.includes('Only event hosts')) {
120+
res.status(403).json({ message: result.error });
121+
} else {
122+
res.status(400).json({ message: result.error });
123+
}
124+
return;
125+
}
126+
127+
res.status(200).json({ message: result.message });
128+
} catch (error) {
129+
console.error('Update RSVP preferences error:', error);
130+
res.status(500).json({ message: 'Internal Server Error' });
131+
}
132+
});
133+
134+
// Delete RSVP preferences for an event (HOST/CO-HOST ONLY)
135+
router.delete('/:eventId/preferences', verifyIdToken, async (req: Request, res: Response) => {
136+
try {
137+
const { eventId } = req.params;
138+
const userId = req.userId;
139+
140+
if (!userId) {
141+
res.status(401).json({ message: 'Unauthorized' });
142+
return;
143+
}
144+
145+
const result = await deleteEventRsvpPreferences(eventId, userId);
146+
147+
if (!result.success) {
148+
if (result.error?.includes('Access denied') || result.error?.includes('Only event hosts')) {
149+
res.status(403).json({ message: result.error });
150+
} else {
151+
res.status(400).json({ message: result.error });
152+
}
153+
return;
154+
}
155+
156+
res.status(200).json({ message: result.message });
157+
} catch (error) {
158+
console.error('Delete RSVP preferences error:', error);
159+
res.status(500).json({ message: 'Internal Server Error' });
160+
}
161+
});
162+
163+
export default router;

src/server.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import guestRoutes from './routes/guestRoutes'
99
import subEventRoutes from './routes/subEventRoutes'
1010
import onboardingRoutes from './routes/onboardingRoutes';
1111
import inviteRoutes from './routes/inviteRoutes';
12+
import rsvpPreferencesRoutes from './routes/rsvpPreferencesRoutes';
1213
import { PrismaClient } from '@prisma/client';
1314

1415

@@ -65,7 +66,7 @@ app.use('/api/guests', guestRoutes)
6566
app.use('/api/:eventId/subEvent', subEventRoutes)
6667
app.use('/api', onboardingRoutes);
6768
app.use('/api/invite', inviteRoutes);
68-
69+
app.use('/api/rsvp', rsvpPreferencesRoutes);
6970
app.get('/', (_req, res) => {
7071
res.json({ message: 'Get lost' });
7172
});

0 commit comments

Comments
 (0)