Skip to content

Commit fae94f6

Browse files
committed
Refactor email sending to use priority queue for approval notifications and enhance RSVP handling. Update RSVP logic to require approval only for GOING responses and streamline email data structure for improved iCal generation.
1 parent 32436fd commit fae94f6

File tree

15 files changed

+202
-53
lines changed

15 files changed

+202
-53
lines changed

src/app/api/approval/rsvp/route.ts

Lines changed: 72 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ import type { NextRequest } from "next/server";
44
import { NextResponse } from "next/server";
55
import type { RSVP_TYPE } from "@prisma/client";
66
import { RSVP_APPROVAL_STATUS } from "@prisma/client";
7-
import { sendEventEmail } from "src/utils/email/send";
7+
import type { EmailType } from "src/components/Email";
88

99
type CreateApprovalRequestBody = {
1010
userId: string;
@@ -154,22 +154,44 @@ export async function POST(req: NextRequest) {
154154

155155
// Send notification emails to proposers
156156
try {
157+
// Import the priority queue function
158+
const { queuePriorityEmail } = await import("src/lib/queue");
159+
160+
// Transform event to match ServerEvent type (using type assertion for compatibility)
161+
const serverEvent = {
162+
...event,
163+
collections: [], // Add missing collections property
164+
community: null, // Add missing community property
165+
} as any; // Type assertion to bypass strict typing
166+
167+
// Prepare proposer emails for priority queue
157168
const proposerEmails = event.proposers
158-
.map((p) => p.user.email)
159-
.filter((email) => email) as string[];
160-
161-
for (const proposer of event.proposers) {
162-
if (proposer.user.email) {
163-
sendEventEmail({
164-
receiver: proposer.user,
165-
type: "approval-requested",
166-
event: event,
167-
}).catch((error) =>
168-
console.error(
169-
`Error sending approval request email to ${proposer.user.email}: ${error?.message}`
170-
)
171-
);
172-
}
169+
.filter((p) => p.user.email)
170+
.map((proposer) => ({
171+
event: serverEvent,
172+
type: "approval-requested" as EmailType,
173+
receiver: {
174+
id: proposer.user.id,
175+
email: proposer.user.email,
176+
address: proposer.user.address,
177+
nickname: proposer.user.nickname,
178+
isBeta: proposer.user.isBeta,
179+
profile: null, // User doesn't have profiles in this context
180+
},
181+
}));
182+
183+
// Queue as priority emails instead of regular emails
184+
if (proposerEmails.length > 0) {
185+
await queuePriorityEmail({
186+
event: serverEvent,
187+
creatorId: undefined, // No specific creator for approval requests
188+
proposerEmails: proposerEmails,
189+
attendeeEmails: [], // No attendee emails for approval requests
190+
});
191+
192+
console.log(
193+
`Queued ${proposerEmails.length} approval request emails as priority`
194+
);
173195
}
174196
} catch (emailError) {
175197
console.error("Error sending notification emails:", emailError);
@@ -348,24 +370,48 @@ export async function PUT(req: NextRequest) {
348370
// Send notification email to the requester
349371
try {
350372
if (updatedRequest.user.email) {
373+
const { queuePriorityEmail } = await import("src/lib/queue");
374+
351375
const emailType =
352376
status === "APPROVED" ? "approval-approved" : "approval-rejected";
353-
sendEventEmail({
354-
receiver: updatedRequest.user,
355-
type: emailType,
356-
event: updatedRequest.event,
377+
378+
// Transform event to match ServerEvent type (using type assertion for compatibility)
379+
const serverEvent = {
380+
...updatedRequest.event,
381+
collections: [], // Add missing collections property
382+
community: null, // Add missing community property
383+
} as any; // Type assertion to bypass strict typing
384+
385+
// Prepare email for priority queue
386+
const requesterEmail = {
387+
event: serverEvent,
388+
type: emailType as EmailType,
389+
receiver: {
390+
id: updatedRequest.user.id,
391+
email: updatedRequest.user.email,
392+
address: updatedRequest.user.address,
393+
nickname: updatedRequest.user.nickname,
394+
isBeta: updatedRequest.user.isBeta,
395+
profile: updatedRequest.user.profiles?.[0] || null,
396+
},
357397
// For approved requests, include the RSVP type for iCal generation
358398
approvalRsvpType:
359399
status === "APPROVED" &&
360400
["GOING", "MAYBE", "NOT_GOING"].includes(approvalRequest.rsvpType)
361401
? (approvalRequest.rsvpType as "GOING" | "MAYBE" | "NOT_GOING")
362402
: undefined,
363-
}).catch((error) =>
364-
console.error(
365-
`Error sending approval ${status.toLowerCase()} email: ${
366-
error?.message
367-
}`
368-
)
403+
};
404+
405+
// Queue as priority email
406+
await queuePriorityEmail({
407+
event: serverEvent,
408+
creatorId: undefined,
409+
proposerEmails: [], // No proposer emails for responses
410+
attendeeEmails: [requesterEmail], // Send to the requester
411+
});
412+
413+
console.log(
414+
`Queued approval ${status.toLowerCase()} email as priority`
369415
);
370416
}
371417
} catch (emailError) {

src/app/api/create/rsvp/route.ts

Lines changed: 85 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { prisma } from "src/utils/db";
33
import type { NextRequest } from "next/server";
44
import { NextResponse } from "next/server";
55
import { sendEventEmail } from "src/utils/email/send";
6-
import { RSVP_TYPE, type User } from "@prisma/client";
6+
import { RSVP_TYPE, RSVP_APPROVAL_STATUS, type User } from "@prisma/client";
77
import { rsvpTypeToEmailType } from "src/utils/rsvpTypetoEmailType";
88
import {
99
scheduleReminderEmails,
@@ -70,8 +70,11 @@ export async function POST(req: NextRequest) {
7070
},
7171
});
7272

73-
if (existingApprovalRequest) {
74-
// Return the status of existing approval request
73+
if (
74+
existingApprovalRequest &&
75+
existingApprovalRequest.status === RSVP_APPROVAL_STATUS.PENDING
76+
) {
77+
// Only block if there's a PENDING approval request
7578
return NextResponse.json({
7679
data: {
7780
status: "APPROVAL_PENDING",
@@ -83,15 +86,16 @@ export async function POST(req: NextRequest) {
8386
});
8487
}
8588

86-
// If user doesn't have an existing RSVP or approval request, they need to go through approval flow
87-
if (!existingRsvp) {
89+
// Only require approval for "GOING" RSVPs
90+
// Users can freely RSVP as "MAYBE" or "NOT_GOING" without approval
91+
if (rsvp.type === RSVP_TYPE.GOING && !existingRsvp) {
8892
return NextResponse.json(
8993
{
9094
data: {
9195
status: "APPROVAL_REQUIRED",
9296
eventId: rsvp.eventId,
9397
message:
94-
"This event requires approval. Please use the approval request endpoint.",
98+
"This event requires approval to RSVP as 'Going'. Please use the approval request endpoint.",
9599
},
96100
},
97101
{ status: 400 }
@@ -127,18 +131,64 @@ export async function POST(req: NextRequest) {
127131
`User ${user.id} changing from GOING to ${rsvp.type} for event ${event.id}`
128132
);
129133

134+
// Safety check: existingRsvp should exist if wasGoing is true
135+
if (!existingRsvp) {
136+
console.error(
137+
`User ${user.id} was marked as GOING but no existing RSVP found for event ${event.id}`
138+
);
139+
return NextResponse.json(
140+
{ error: "Invalid RSVP state - no existing RSVP found" },
141+
{ status: 500 }
142+
);
143+
}
144+
130145
// Increment the event sequence to ensure calendar clients recognize the update
131146
await prisma.event.update({
132147
where: { id: event.id },
133148
data: { sequence: event.sequence + 1 },
134149
});
135150

136-
await prisma.rsvp.update({
137-
where: { id: existingRsvp.id }, // Use existingRsvp.id which is guaranteed
138-
data: { rsvpType: rsvp.type },
139-
});
140-
finalRsvpTypeForUser = rsvp.type;
141-
emailTypeForUser = rsvpTypeToEmailType(finalRsvpTypeForUser, true); // Send update email
151+
// For approval-required events: if user is changing to NOT_GOING, delete both RSVP and approval request
152+
// This makes it simpler for the frontend - user starts completely fresh if they want to rejoin
153+
if (event.requiresApproval && rsvp.type === RSVP_TYPE.NOT_GOING) {
154+
try {
155+
// Delete the RSVP record entirely
156+
await prisma.rsvp.delete({
157+
where: { id: existingRsvp.id },
158+
});
159+
160+
// Delete any approval requests for this user/event
161+
await prisma.rsvpApprovalRequest.deleteMany({
162+
where: {
163+
eventId: event.id,
164+
userId: user.id,
165+
},
166+
});
167+
168+
console.log(
169+
`Deleted RSVP and approval request for user ${user.id} on event ${event.id} (marked NOT_GOING)`
170+
);
171+
finalRsvpTypeForUser = RSVP_TYPE.NOT_GOING;
172+
emailTypeForUser = rsvpTypeToEmailType(RSVP_TYPE.NOT_GOING, true); // Send update email
173+
} catch (deleteError) {
174+
console.error("Error deleting RSVP/approval request:", deleteError);
175+
// Fallback to updating RSVP if deletion fails
176+
await prisma.rsvp.update({
177+
where: { id: existingRsvp.id },
178+
data: { rsvpType: rsvp.type },
179+
});
180+
finalRsvpTypeForUser = rsvp.type;
181+
emailTypeForUser = rsvpTypeToEmailType(finalRsvpTypeForUser, true);
182+
}
183+
} else {
184+
// For non-approval events or non-NOT_GOING changes, just update the RSVP
185+
await prisma.rsvp.update({
186+
where: { id: existingRsvp.id },
187+
data: { rsvpType: rsvp.type },
188+
});
189+
finalRsvpTypeForUser = rsvp.type;
190+
emailTypeForUser = rsvpTypeToEmailType(finalRsvpTypeForUser, true); // Send update email
191+
}
142192

143193
// A spot potentially opened up, try to promote
144194
if (eventLimit > 0) {
@@ -150,6 +200,29 @@ export async function POST(req: NextRequest) {
150200
console.log(
151201
`User ${user.id} wants to RSVP as GOING for event ${event.id}`
152202
);
203+
204+
// For approval-required events, check if user needs approval to change to GOING
205+
if (
206+
event.requiresApproval &&
207+
!rsvp.adminOverride &&
208+
existingRsvp &&
209+
!wasGoing
210+
) {
211+
// User has existing RSVP but wasn't GOING, and now wants to be GOING
212+
// They need approval for this change
213+
return NextResponse.json(
214+
{
215+
data: {
216+
status: "APPROVAL_REQUIRED",
217+
eventId: rsvp.eventId,
218+
message:
219+
"This event requires approval to change your RSVP to 'Going'. Please use the approval request endpoint.",
220+
},
221+
},
222+
{ status: 400 }
223+
);
224+
}
225+
153226
const currentGoingCount = event.rsvps.filter(
154227
(r) => r.rsvpType === RSVP_TYPE.GOING && r.attendeeId !== user.id // Exclude self if updating from Maybe->Going
155228
).length;

src/app/api/profile/update-image/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { NextRequest, NextResponse } from "next/server";
1+
import type { NextRequest } from "next/server";
2+
import { NextResponse } from "next/server";
23
import { auth } from "@clerk/nextjs/server";
34
import { prisma } from "src/utils/db";
45
import { getPublicS3Url } from "src/utils/s3";

src/app/api/query/getUserEvents/route.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
import { NextRequest, NextResponse } from "next/server";
1+
import type { NextRequest } from "next/server";
2+
import { NextResponse } from "next/server";
23
import { prisma } from "src/utils/db";
34
import formatEvent from "src/utils/formatEvent";
45
import { DateTime } from "luxon";

src/components/Card.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import type { ReactNode } from "react";
33
import { useEffect, useState } from "react";
44
import { Article } from "./Article";
55
import { Skeleton } from "./ui/skeleton";
6-
import { ClientEvent } from "src/types";
6+
import type { ClientEvent } from "src/types";
77
import { WhoElseIsGoing } from "./Hero";
88

99
export const CardTemplate = ({

src/components/Email/components/EventDetails.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ConvoEvent } from "../types";
1+
import type { ConvoEvent } from "../types";
22
import { DateTime } from "luxon";
33

44
interface EventDetailsProps {

src/components/Email/templates/Create.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EmailTemplateWithEventProps } from "../types";
1+
import type { EmailTemplateWithEventProps } from "../types";
22
import { EmailWrapper } from "../components/EmailWrapper";
33
import { EventDetails } from "../components/EventDetails";
44

src/components/Email/templates/DeletedProposer.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EmailTemplateWithEventProps } from "../types";
1+
import type { EmailTemplateWithEventProps } from "../types";
22
import { EmailWrapper } from "../components/EmailWrapper";
33
import { EventDetails } from "../components/EventDetails";
44

src/components/Email/templates/InviteGoing.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EmailTemplateWithEventProps } from "../types";
1+
import type { EmailTemplateWithEventProps } from "../types";
22
import { EmailWrapper } from "../components/EmailWrapper";
33
import { EventDetails } from "../components/EventDetails";
44
import { StarryEyesEmoji } from "../components/EmailEmojis";

src/components/Email/templates/InviteMaybe.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { EmailTemplateWithEventProps } from "../types";
1+
import type { EmailTemplateWithEventProps } from "../types";
22
import { EmailWrapper } from "../components/EmailWrapper";
33
import { EventDetails } from "../components/EventDetails";
44
import { TongueStickingOutEmoji } from "../components/EmailEmojis";

0 commit comments

Comments
 (0)