Skip to content

Commit f2bf4c9

Browse files
junlarsenCopilot
andauthored
Create service method for sending emails to event attendees (#2955)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
1 parent 5e518b6 commit f2bf4c9

File tree

4 files changed

+109
-14
lines changed

4 files changed

+109
-14
lines changed
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
<!DOCTYPE html>
2+
<html dir="ltr" lang="nb">
3+
4+
<head>
5+
<meta content="text/html; charset=UTF-8" http-equiv="Content-Type" />
6+
<meta name="x-apple-disable-message-reformatting" />
7+
</head>
8+
9+
<body style="background-color: rgb(255, 255, 255);">
10+
<table align="center" width="100%" border="0" cellpadding="0" cellspacing="0" role="presentation"
11+
style="max-width: 37.5em">
12+
<tbody>
13+
<tr style="width: 100%">
14+
<td>
15+
<h2>Viktig info om {{ eventName }}</h2>
16+
<p>Dette er en melding fra arrangørene av <a href="{{ eventLink }}">{{ eventName }}</a> som du er påmeldt hos
17+
Linjeforeningen Online.</p>
18+
<p>{{ message }}</p>
19+
</td>
20+
</tr>
21+
22+
<tr style="width: 100%">
23+
<td>
24+
<h3 style="font-size: 0.9em; margin-top: 3rem">Linjeforeningen Online</h3>
25+
<p style="font-size: 0.75em; color: gray">Du mottar denne e-posten fordi du er påmeldt arrangementet {{
26+
eventName }}</p>
27+
<p style="font-size: 0.75em; color: gray">Org. Nr. 992 548 045 &ndash; Høgskoleringen 5, 7034 Trondheim</p>
28+
<p style="font-size: 0.75em; color: gray">Alle datoer er i norsk tid.</p>
29+
</td>
30+
</tr>
31+
</tbody>
32+
</table>
33+
</body>
34+
35+
</html>

apps/rpc/src/modules/email/email-template.ts

Lines changed: 11 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ export type EmailType =
1111
| "COMPANY_INVOICE_NOTIFICATION"
1212
| "FEEDBACK_FORM_LINK"
1313
| "EVENT_ATTENDANCE"
14+
| "EVENT_MESSAGE"
1415
| "RECEIVED_MARK"
1516

1617
export interface EmailTemplate<TData, TType extends EmailType> {
@@ -23,12 +24,6 @@ export type InferEmailData<TDef> = TDef extends EmailTemplate<infer TData, infer
2324
export type InferEmailType<TDef> =
2425
TDef extends EmailTemplate<infer TData, infer TType extends EmailType> ? TType : never
2526

26-
export type CompanyCollaborationReceiptEmailTemplate = typeof emails.COMPANY_COLLABORATION_RECEIPT
27-
export type CompanyCollaborationNotificationEmailTemplate = typeof emails.COMPANY_COLLABORATION_NOTIFICATION
28-
export type CompanyInvoiceNotificationEmailTemplate = typeof emails.COMPANY_INVOICE_NOTIFICATION
29-
export type FeedbackFormLinkEmailTemplate = typeof emails.FEEDBACK_FORM_LINK
30-
export type AnyEmailTemplate = CompanyCollaborationReceiptEmailTemplate
31-
3227
export function createEmailTemplate<const TData, const TType extends EmailType>(
3328
definition: EmailTemplate<TData, TType>
3429
): EmailTemplate<TData, TType> {
@@ -124,6 +119,16 @@ export const emails = {
124119
}),
125120
getTemplate: async () => fsp.readFile(path.join(templates, "event_attendance.mustache"), "utf-8"),
126121
}),
122+
EVENT_MESSAGE: createEmailTemplate({
123+
type: "EVENT_MESSAGE",
124+
getSchema: () =>
125+
z.object({
126+
eventName: z.string(),
127+
eventLink: z.string().url(),
128+
message: z.string(),
129+
}),
130+
getTemplate: async () => fsp.readFile(path.join(templates, "event_message.mustache"), "utf-8"),
131+
}),
127132
RECEIVED_MARK: createEmailTemplate({
128133
type: "RECEIVED_MARK",
129134
getSchema: () =>

apps/rpc/src/modules/event/attendance-service.ts

Lines changed: 57 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@ import {
1818
AttendeeWriteSchema,
1919
DEFAULT_MARK_DURATION,
2020
type Event,
21-
type GroupType,
2221
type Membership,
2322
type TaskId,
2423
type User,
@@ -27,6 +26,7 @@ import {
2726
getMembershipGrade,
2827
hasAttendeePaid,
2928
isAttendable,
29+
findFirstHostingGroupEmail,
3030
} from "@dotkomonline/types"
3131
import { createAbsoluteEventPageUrl, createPoolName, getCurrentUTC, ogJoin, slugify } from "@dotkomonline/utils"
3232
import {
@@ -258,6 +258,8 @@ export interface AttendanceService {
258258
mergeTime: TZDate | null
259259
): Promise<TaskId | null>
260260
executeMergeEventPoolsTask(handle: DBHandle, task: InferTaskData<MergeAttendancePoolsTaskDefinition>): Promise<void>
261+
262+
notifyAttendees(handle: DBHandle, attendanceId: AttendanceId, message: string): Promise<void>
261263
}
262264

263265
export function getAttendanceService(
@@ -1438,12 +1440,7 @@ export function getAttendanceService(
14381440
return
14391441
}
14401442

1441-
const validGroupTypes: GroupType[] = ["COMMITTEE", "NODE_COMMITTEE"]
1442-
1443-
const hostingGroupEmail =
1444-
event.hostingGroups.filter((group) => group.email && validGroupTypes.includes(group.type)).at(0)?.email ??
1445-
"bedkom@online.ntnu.no"
1446-
1443+
const hostingGroupEmail = findFirstHostingGroupEmail(event) ?? "bedkom@online.ntnu.no"
14471444
logger.info(
14481445
"Sending feedback form email for Event(ID=%s) to %d attendees from email %s",
14491446
event.id,
@@ -1613,6 +1610,59 @@ export function getAttendanceService(
16131610
await attendanceRepository.updateAttendeeAttendancePoolIdByAttendancePoolIds(handle, mergeablePoolIds, pool.id)
16141611
await attendanceRepository.deleteAttendancePoolsByIds(handle, mergeablePoolIds)
16151612
},
1613+
async notifyAttendees(handle, attendanceId, message) {
1614+
const attendance = await this.getAttendanceById(handle, attendanceId)
1615+
const event = await eventService.getByAttendanceId(handle, attendanceId)
1616+
if (isBefore(getCurrentUTC(), attendance.registerStart)) {
1617+
throw new IllegalStateError(`Cannot send message to attendees for Attendance(ID=${attendance.id}) before start`)
1618+
}
1619+
1620+
const hostingGroupEmail = findFirstHostingGroupEmail(event)
1621+
if (hostingGroupEmail === null) {
1622+
logger.warn(
1623+
"Notification email sent for Event(ID=%s, Name=%s) did not have sufficient organizer email so %s was used.",
1624+
event.id,
1625+
event.title,
1626+
DEFAULT_EMAIL_SOURCE
1627+
)
1628+
}
1629+
const sourceEmail = hostingGroupEmail ?? DEFAULT_EMAIL_SOURCE
1630+
1631+
// In order to keep email sizes relatively small, and to prevent spam detection we batch the emails with 25 BCC recipients at a time
1632+
const batchSize = 25
1633+
for (let batchStartIndex = 0; batchStartIndex < attendance.attendees.length; batchStartIndex += batchSize) {
1634+
const attendees = attendance.attendees.slice(batchStartIndex, batchStartIndex + batchSize)
1635+
const attendeeEmails = attendees.map((a) => a.user.email).filter((e) => e !== null)
1636+
1637+
const currentBatchIndex = Math.floor(batchStartIndex / batchSize) + 1
1638+
const totalBatches = Math.ceil(attendance.attendees.length / batchSize)
1639+
1640+
logger.info(
1641+
"Scheduling emails %s..%s in batch %s/%s for notification for Event(ID=%s, Name=%s)",
1642+
batchStartIndex + 1,
1643+
batchStartIndex + attendees.length,
1644+
currentBatchIndex,
1645+
totalBatches,
1646+
event.id,
1647+
event.title
1648+
)
1649+
1650+
await emailService.send(
1651+
sourceEmail,
1652+
[sourceEmail],
1653+
[],
1654+
[],
1655+
attendeeEmails,
1656+
`Viktig melding om ${event.title}`,
1657+
emails.EVENT_MESSAGE,
1658+
{
1659+
eventName: event.title,
1660+
eventLink: createAbsoluteEventPageUrl(configuration.WEB_PUBLIC_ORIGIN, event.id, event.title),
1661+
message,
1662+
}
1663+
)
1664+
}
1665+
},
16161666
}
16171667
}
16181668

packages/types/src/event.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { AttendanceSchema, AttendanceSummarySchema } from "./attendance"
66
import { CompanySchema } from "./company"
77
import { FeedbackFormSchema } from "./feedback-form"
88
import { buildAnyOfFilter, buildDateRangeFilter, buildSearchFilter, createSortOrder } from "./filters"
9-
import { GroupSchema } from "./group"
9+
import { GroupSchema, type GroupType } from "./group"
1010

1111
/**
1212
* @packageDocumentation
@@ -183,3 +183,8 @@ export const getDefaultFeedbackAnswerDeadline = (eventEnd: Date, timezone: strin
183183
}
184184

185185
export const EVENT_IMAGE_MAX_SIZE_KIB = 5 * 1024
186+
187+
export function findFirstHostingGroupEmail(event: Event): string | null {
188+
const validGroupTypes: GroupType[] = ["COMMITTEE", "NODE_COMMITTEE"]
189+
return event.hostingGroups.filter((group) => group.email && validGroupTypes.includes(group.type)).at(0)?.email ?? null
190+
}

0 commit comments

Comments
 (0)