Skip to content

Commit 9323e67

Browse files
committed
feat: enhance hackathon and organization notification features
- Added notification functionality for hackathon publication, updates, and submission status changes, improving communication with participants and organization members. - Implemented email templates for various notification types, including organization creation and updates, ensuring consistent messaging. - Updated notification model to accommodate new notification types and data fields, enhancing the flexibility of the notification system. - Introduced a reusable email template for better maintainability and consistency across different notification types.
1 parent 70d5ed1 commit 9323e67

13 files changed

Lines changed: 2989 additions & 32 deletions

‎src/app.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@ const allowedOrigins = [
5757
"https://www.staging.boundlessfi.xyz",
5858
"http://localhost:3000", // For local frontend development
5959
"http://localhost:8000", // For local development
60+
"http://192.168.1.187:3000",
6061
];
6162

6263
app.use(

‎src/features/hackathons/hackathon-crud.controller.ts‎

Lines changed: 206 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import Hackathon, {
55
IHackathon,
66
} from "../../models/hackathon.model.js";
77
import Organization from "../../models/organization.model.js";
8+
import User from "../../models/user.model.js";
89
import {
910
sendSuccess,
1011
sendError,
@@ -22,6 +23,10 @@ import {
2223
validatePublishRequirements,
2324
} from "./hackathon.helpers.js";
2425
import { generateHackathonSlug } from "../../utils/hackathon.utils.js";
26+
import NotificationService from "../notifications/notification.service.js";
27+
import EmailTemplatesService from "../../services/email/email-templates.service.js";
28+
import { NotificationType } from "../../models/notification.model.js";
29+
import { config } from "../../config/main.config.js";
2530

2631
/**
2732
* @swagger
@@ -136,6 +141,99 @@ export const publishHackathon = async (
136141
});
137142
}
138143

144+
// Send notifications
145+
try {
146+
const frontendUrl =
147+
process.env.FRONTEND_URL ||
148+
config.cors.origin ||
149+
"https://boundlessfi.xyz";
150+
const baseUrl = Array.isArray(frontendUrl) ? frontendUrl[0] : frontendUrl;
151+
const creatorName =
152+
`${user.profile?.firstName || ""} ${user.profile?.lastName || ""}`.trim() ||
153+
user.email;
154+
155+
// Notify organization members about hackathon publication
156+
const allMembers = [organization.owner, ...organization.members].filter(
157+
(email) => email !== user.email,
158+
); // Don't notify the creator
159+
160+
if (allMembers.length > 0) {
161+
const memberUsers = await User.find({
162+
email: { $in: allMembers },
163+
}).select(
164+
"email profile.firstName profile.lastName settings.notifications",
165+
);
166+
167+
await NotificationService.notifyTeamMembers(
168+
memberUsers.map((member) => ({
169+
userId: member._id,
170+
email: member.email,
171+
name:
172+
`${member.profile?.firstName || ""} ${member.profile?.lastName || ""}`.trim() ||
173+
member.email,
174+
})),
175+
{
176+
type: NotificationType.HACKATHON_PUBLISHED,
177+
title: `New hackathon published: ${hackathon.title || "Hackathon"}`,
178+
message: `${creatorName} has published a new hackathon "${hackathon.title || "Hackathon"}"`,
179+
data: {
180+
hackathonId: hackathon._id,
181+
hackathonName: hackathon.title || "Hackathon",
182+
hackathonSlug: hackathon.slug,
183+
organizationId: new mongoose.Types.ObjectId(orgId),
184+
},
185+
emailTemplate: EmailTemplatesService.getTemplate(
186+
"hackathon-published",
187+
{
188+
hackathonId: hackathon._id.toString(),
189+
hackathonName: hackathon.title || "Hackathon",
190+
hackathonSlug: hackathon.slug,
191+
organizationId: orgId,
192+
unsubscribeUrl: undefined, // Will be set per recipient
193+
},
194+
),
195+
},
196+
);
197+
}
198+
199+
// Notify the creator
200+
await NotificationService.sendSingleNotification(
201+
{
202+
userId: user._id,
203+
email: user.email,
204+
name: creatorName,
205+
preferences: user.settings?.notifications,
206+
},
207+
{
208+
type: NotificationType.HACKATHON_PUBLISHED,
209+
title: `Hackathon "${hackathon.title || "Hackathon"}" published`,
210+
message: `Your hackathon "${hackathon.title || "Hackathon"}" has been successfully published.`,
211+
data: {
212+
hackathonId: hackathon._id,
213+
hackathonName: hackathon.title || "Hackathon",
214+
hackathonSlug: hackathon.slug,
215+
organizationId: new mongoose.Types.ObjectId(orgId),
216+
},
217+
emailTemplate: EmailTemplatesService.getTemplate(
218+
"hackathon-published",
219+
{
220+
hackathonId: hackathon._id.toString(),
221+
hackathonName: hackathon.title || "Hackathon",
222+
hackathonSlug: hackathon.slug,
223+
organizationId: orgId,
224+
unsubscribeUrl: `${baseUrl}/unsubscribe?email=${encodeURIComponent(user.email)}`,
225+
},
226+
),
227+
},
228+
);
229+
} catch (notificationError) {
230+
console.error(
231+
"Error sending hackathon published notifications:",
232+
notificationError,
233+
);
234+
// Don't fail the whole operation if notification fails
235+
}
236+
139237
sendCreated(res, hackathon, "Hackathon published successfully");
140238
} catch (error) {
141239
console.error("Publish hackathon error:", error);
@@ -203,10 +301,118 @@ export const updateHackathon = async (
203301
}
204302

205303
const updateData = transformRequestBody(req.body);
304+
const oldStatus = hackathon.status;
206305

207306
Object.assign(hackathon, updateData);
208307
await hackathon.save();
209308

309+
// Send notifications
310+
try {
311+
const frontendUrl =
312+
process.env.FRONTEND_URL ||
313+
config.cors.origin ||
314+
"https://boundlessfi.xyz";
315+
const baseUrl = Array.isArray(frontendUrl) ? frontendUrl[0] : frontendUrl;
316+
const updaterName =
317+
`${user.profile?.firstName || ""} ${user.profile?.lastName || ""}`.trim() ||
318+
user.email;
319+
const statusChanged = oldStatus !== hackathon.status;
320+
const changes = Object.keys(updateData).join(", ");
321+
322+
// Determine notification type based on status change
323+
let notificationType = NotificationType.HACKATHON_UPDATED;
324+
if (statusChanged) {
325+
notificationType = NotificationType.HACKATHON_STATUS_CHANGED;
326+
if (hackathon.status === HackathonStatus.ACTIVE) {
327+
notificationType = NotificationType.HACKATHON_ACTIVE;
328+
} else if (hackathon.status === HackathonStatus.COMPLETED) {
329+
notificationType = NotificationType.HACKATHON_COMPLETED;
330+
} else if (hackathon.status === HackathonStatus.CANCELLED) {
331+
notificationType = NotificationType.HACKATHON_CANCELLED;
332+
}
333+
}
334+
335+
// Notify organization members
336+
const allMembers = [organization.owner, ...organization.members].filter(
337+
(email) => email !== user.email,
338+
);
339+
340+
if (allMembers.length > 0) {
341+
const memberUsers = await User.find({
342+
email: { $in: allMembers },
343+
}).select(
344+
"email profile.firstName profile.lastName settings.notifications",
345+
);
346+
347+
await NotificationService.notifyTeamMembers(
348+
memberUsers.map((member) => ({
349+
userId: member._id,
350+
email: member.email,
351+
name:
352+
`${member.profile?.firstName || ""} ${member.profile?.lastName || ""}`.trim() ||
353+
member.email,
354+
})),
355+
{
356+
type: notificationType,
357+
title: statusChanged
358+
? `Hackathon status changed: ${hackathon.title || "Hackathon"}`
359+
: `Hackathon updated: ${hackathon.title || "Hackathon"}`,
360+
message: statusChanged
361+
? `${updaterName} has changed the status of "${hackathon.title || "Hackathon"}" from ${oldStatus} to ${hackathon.status}`
362+
: `${updaterName} has updated "${hackathon.title || "Hackathon"}"`,
363+
data: {
364+
hackathonId: hackathon._id,
365+
hackathonName: hackathon.title || "Hackathon",
366+
hackathonSlug: hackathon.slug,
367+
organizationId: new mongoose.Types.ObjectId(orgId),
368+
oldStatus: statusChanged ? oldStatus : undefined,
369+
newStatus: statusChanged ? hackathon.status : undefined,
370+
changes: statusChanged ? undefined : changes,
371+
},
372+
emailTemplate: statusChanged
373+
? EmailTemplatesService.getTemplate(
374+
hackathon.status === HackathonStatus.ACTIVE
375+
? "hackathon-active"
376+
: hackathon.status === HackathonStatus.COMPLETED
377+
? "hackathon-completed"
378+
: hackathon.status === HackathonStatus.CANCELLED
379+
? "hackathon-cancelled"
380+
: "hackathon-updated",
381+
{
382+
hackathonId: hackathon._id.toString(),
383+
hackathonName: hackathon.title || "Hackathon",
384+
hackathonSlug: hackathon.slug,
385+
organizationId: orgId,
386+
oldStatus,
387+
newStatus: hackathon.status,
388+
startDate: hackathon.startDate,
389+
reason: (hackathon as any).cancellationReason,
390+
unsubscribeUrl: undefined,
391+
},
392+
)
393+
: EmailTemplatesService.getTemplate("hackathon-updated", {
394+
hackathonId: hackathon._id.toString(),
395+
hackathonName: hackathon.title || "Hackathon",
396+
hackathonSlug: hackathon.slug,
397+
changes,
398+
unsubscribeUrl: undefined,
399+
}),
400+
sendEmail: statusChanged, // Only send email for status changes
401+
sendInApp: true,
402+
},
403+
);
404+
}
405+
406+
// TODO: Notify participants if hackathon is updated (especially for status changes)
407+
// This would require querying HackathonParticipant model
408+
} catch (notificationError) {
409+
console.error(
410+
"Error sending hackathon update notifications:",
411+
notificationError,
412+
);
413+
// Don't fail the whole operation if notification fails
414+
}
415+
210416
sendSuccess(res, hackathon, "Hackathon updated successfully");
211417
} catch (error) {
212418
console.error("Update hackathon error:", error);

‎src/features/hackathons/hackathon-review.controller.ts‎

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { Request, Response } from "express";
22
import mongoose from "mongoose";
33
import Hackathon from "../../models/hackathon.model.js";
44
import HackathonParticipant from "../../models/hackathon-participant.model.js";
5+
import User from "../../models/user.model.js";
6+
import Organization from "../../models/organization.model.js";
57
import {
68
sendSuccess,
79
sendError,
@@ -14,6 +16,10 @@ import {
1416
AuthenticatedRequest,
1517
canManageHackathons,
1618
} from "./hackathon.helpers.js";
19+
import NotificationService from "../notifications/notification.service.js";
20+
import EmailTemplatesService from "../../services/email/email-templates.service.js";
21+
import { NotificationType } from "../../models/notification.model.js";
22+
import { config } from "../../config/main.config.js";
1723

1824
/**
1925
* @swagger
@@ -110,6 +116,54 @@ export const shortlistSubmission = async (
110116

111117
await participant.save();
112118

119+
// Send notification to participant about status change
120+
try {
121+
const frontendUrl =
122+
process.env.FRONTEND_URL ||
123+
config.cors.origin ||
124+
"https://boundlessfi.xyz";
125+
const baseUrl = Array.isArray(frontendUrl) ? frontendUrl[0] : frontendUrl;
126+
const participantUser = await User.findById(participant.userId).select(
127+
"email profile.firstName profile.lastName settings.notifications",
128+
);
129+
130+
if (participantUser && participant.submission.status === "shortlisted") {
131+
await NotificationService.sendSingleNotification(
132+
{
133+
userId: participantUser._id,
134+
email: participantUser.email,
135+
name:
136+
`${participantUser.profile?.firstName || ""} ${participantUser.profile?.lastName || ""}`.trim() ||
137+
participantUser.email,
138+
preferences: participantUser.settings?.notifications,
139+
},
140+
{
141+
type: NotificationType.HACKATHON_SUBMISSION_SHORTLISTED,
142+
title: `Submission shortlisted for ${hackathon.title || "Hackathon"}`,
143+
message: `Your submission for "${hackathon.title || "Hackathon"}" has been shortlisted!`,
144+
data: {
145+
hackathonId: hackathon._id,
146+
hackathonName: hackathon.title || "Hackathon",
147+
hackathonSlug: hackathon.slug,
148+
submissionStatus: "shortlisted",
149+
},
150+
emailTemplate: EmailTemplatesService.getTemplate(
151+
"hackathon-submission-shortlisted",
152+
{
153+
hackathonId: hackathon._id.toString(),
154+
hackathonName: hackathon.title || "Hackathon",
155+
hackathonSlug: hackathon.slug,
156+
unsubscribeUrl: `${baseUrl}/unsubscribe?email=${encodeURIComponent(participantUser.email)}`,
157+
},
158+
),
159+
},
160+
);
161+
}
162+
} catch (notificationError) {
163+
console.error("Error sending shortlist notification:", notificationError);
164+
// Don't fail the whole operation if notification fails
165+
}
166+
113167
// Populate user data for response
114168
await participant.populate({
115169
path: "userId",
@@ -306,6 +360,58 @@ export const disqualifySubmission = async (
306360

307361
await participant.save();
308362

363+
// Send notification to participant about disqualification
364+
try {
365+
const frontendUrl =
366+
process.env.FRONTEND_URL ||
367+
config.cors.origin ||
368+
"https://boundlessfi.xyz";
369+
const baseUrl = Array.isArray(frontendUrl) ? frontendUrl[0] : frontendUrl;
370+
const participantUser = await User.findById(participant.userId).select(
371+
"email profile.firstName profile.lastName settings.notifications",
372+
);
373+
374+
if (participantUser && participant.submission.status === "disqualified") {
375+
await NotificationService.sendSingleNotification(
376+
{
377+
userId: participantUser._id,
378+
email: participantUser.email,
379+
name:
380+
`${participantUser.profile?.firstName || ""} ${participantUser.profile?.lastName || ""}`.trim() ||
381+
participantUser.email,
382+
preferences: participantUser.settings?.notifications,
383+
},
384+
{
385+
type: NotificationType.HACKATHON_SUBMISSION_DISQUALIFIED,
386+
title: `Submission disqualified for ${hackathon.title || "Hackathon"}`,
387+
message: `Your submission for "${hackathon.title || "Hackathon"}" has been disqualified.`,
388+
data: {
389+
hackathonId: hackathon._id,
390+
hackathonName: hackathon.title || "Hackathon",
391+
hackathonSlug: hackathon.slug,
392+
submissionStatus: "disqualified",
393+
},
394+
emailTemplate: EmailTemplatesService.getTemplate(
395+
"hackathon-submission-disqualified",
396+
{
397+
hackathonId: hackathon._id.toString(),
398+
hackathonName: hackathon.title || "Hackathon",
399+
hackathonSlug: hackathon.slug,
400+
reason: participant.submission.disqualificationReason,
401+
unsubscribeUrl: `${baseUrl}/unsubscribe?email=${encodeURIComponent(participantUser.email)}`,
402+
},
403+
),
404+
},
405+
);
406+
}
407+
} catch (notificationError) {
408+
console.error(
409+
"Error sending disqualification notification:",
410+
notificationError,
411+
);
412+
// Don't fail the whole operation if notification fails
413+
}
414+
309415
// Populate user data for response
310416
await participant.populate({
311417
path: "userId",

0 commit comments

Comments
 (0)