Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -33,114 +33,129 @@ export default defineHook(({ action, schedule }, hookContext) => {
}

/**
* Send invitation email when a speaker's portal token is generated.
* Send invitation email for a single speaker.
*/
action('speakers.items.update', async function (metadata, eventContext) {
const { payload, keys } = metadata

// Only proceed if portal_token was just set
if (!payload.portal_token) {
return
}

async function sendInvitationForSpeaker(speakerId: string, accountability: any) {
const context: EmailServiceContext = {
logger,
services,
getSchema,
accountability: eventContext.accountability,
accountability,
}

try {
const schema = await getSchema()
const schema = await getSchema()

const speakersService = new ItemsService('speakers', {
schema,
accountability,
})

const speaker = await speakersService.readOne(speakerId, {
fields: [
'id',
'first_name',
'last_name',
'email',
'portal_token',
'portal_submission_deadline',
'portal_submission_status',
],
})

// Only send if speaker has email, token, and status is pending
if (!speaker?.email || !speaker.portal_token || speaker.portal_submission_status !== 'pending') {
return
}

const speakersService = new ItemsService('speakers', {
// Find related podcast for context
let podcastTitle: string | undefined
let recordingDate: string | undefined

try {
const podcastSpeakersService = new ItemsService('podcasts_speakers', {
schema,
accountability: eventContext.accountability,
accountability,
})

for (const speakerId of keys) {
const speaker = await speakersService.readOne(speakerId, {
fields: [
'id',
'first_name',
'last_name',
'email',
'portal_token',
'portal_submission_deadline',
'portal_submission_status',
],
})
const podcastsService = new ItemsService('podcasts', {
schema,
accountability,
})

// Only send if speaker has email and status is pending
if (!speaker?.email || speaker.portal_submission_status !== 'pending') {
continue
}
const relations = await podcastSpeakersService.readByQuery({
filter: { speaker: { _eq: speakerId } },
fields: ['podcast'],
limit: 1,
sort: ['-id'],
})

// Verify this is the token being set
if (speaker.portal_token !== payload.portal_token) {
continue
}
if (relations && relations.length > 0 && relations[0].podcast) {
const podcast = await podcastsService.readOne(relations[0].podcast, {
fields: ['title', 'recording_date'],
})
podcastTitle = podcast?.title
recordingDate = podcast?.recording_date
}
} catch {
// Podcast lookup is optional
}

// Find related podcast for context
let podcastTitle: string | undefined
let recordingDate: string | undefined
const portalUrl = await buildPortalUrl(speaker.portal_token, context)
const deadline = speaker.portal_submission_deadline
? formatDateGerman(speaker.portal_submission_deadline)
: 'in den nächsten zwei Wochen'

try {
const podcastSpeakersService = new ItemsService('podcasts_speakers', {
schema,
accountability: eventContext.accountability,
})
logger.info(`${HOOK_NAME}: Sending invitation email to ${speaker.email}`)

const podcastsService = new ItemsService('podcasts', {
schema,
accountability: eventContext.accountability,
})
await sendTemplatedEmail(
{
templateKey: 'speaker_invitation',
to: speaker.email,
data: {
first_name: speaker.first_name,
last_name: speaker.last_name,
portal_url: portalUrl,
deadline,
podcast_title: podcastTitle,
recording_date: recordingDate ? formatDateGerman(recordingDate) : undefined,
},
},
context
)

const relations = await podcastSpeakersService.readByQuery({
filter: { speaker: { _eq: speakerId } },
fields: ['podcast'],
limit: 1,
sort: ['-id'],
})
logger.info(`${HOOK_NAME}: Invitation sent to ${speaker.first_name} ${speaker.last_name}`)
}

if (relations && relations.length > 0 && relations[0].podcast) {
const podcast = await podcastsService.readOne(relations[0].podcast, {
fields: ['title', 'recording_date'],
})
podcastTitle = podcast?.title
recordingDate = podcast?.recording_date
}
} catch {
// Podcast lookup is optional
}
/**
* Send invitation email when a new speaker is created (token is auto-generated).
*/
action('speakers.items.create', async function (metadata, eventContext) {
const { key } = metadata

const portalUrl = await buildPortalUrl(speaker.portal_token, context)
const deadline = speaker.portal_submission_deadline
? formatDateGerman(speaker.portal_submission_deadline)
: 'in den nächsten zwei Wochen'
try {
await sendInvitationForSpeaker(key, eventContext.accountability)
} catch (err: any) {
logger.error(`${HOOK_NAME}: Error sending invitation email on create: ${err?.message || err}`)
}
})

logger.info(`${HOOK_NAME}: Sending invitation email to ${speaker.email}`)
/**
* Send invitation email when a speaker's portal token is regenerated.
*/
action('speakers.items.update', async function (metadata, eventContext) {
const { payload, keys } = metadata

await sendTemplatedEmail(
{
templateKey: 'speaker_invitation',
to: speaker.email,
data: {
first_name: speaker.first_name,
last_name: speaker.last_name,
portal_url: portalUrl,
deadline,
podcast_title: podcastTitle,
recording_date: recordingDate ? formatDateGerman(recordingDate) : undefined,
},
},
context
)
// Only proceed if portal_token was just set
if (!payload.portal_token) {
return
}

logger.info(`${HOOK_NAME}: Invitation sent to ${speaker.first_name} ${speaker.last_name}`)
try {
for (const speakerId of keys) {
await sendInvitationForSpeaker(speakerId, eventContext.accountability)
}
} catch (err: any) {
logger.error(`${HOOK_NAME}: Error sending invitation email: ${err?.message || err}`)
logger.error(`${HOOK_NAME}: Error sending invitation email on update: ${err?.message || err}`)
}
})

Expand Down