From 1d13527f63f814bf76f2595acf68c3e267ba1d18 Mon Sep 17 00:00:00 2001 From: John Angel <161815068+JeangelLF@users.noreply.github.com> Date: Fri, 15 Nov 2024 12:40:38 +0100 Subject: [PATCH] Feat: Add new actions for DaZ Matches and Instructors Only Users (#1159) * feat: Add new actions * feat: Execute actions for new DaZ matches * feat: Add new action for instructors completing their first group appointment * feat: Pass notification context to hooks * feat: Add hook to invite instructors to reflection meetings * fix: Adjust condition to invite users to reflection meeting --- common/match/create.ts | 14 +++++++++++--- common/notification/actions.ts | 28 ++++++++++++++++++++++++++++ common/notification/hook.ts | 17 +++++++++-------- common/notification/hooks.ts | 26 ++++++++++++++++++++++++++ common/notification/index.ts | 3 ++- common/util/subjectsutils.ts | 2 ++ 6 files changed, 78 insertions(+), 12 deletions(-) diff --git a/common/match/create.ts b/common/match/create.ts index 8b6b06773..7a1feb902 100644 --- a/common/match/create.ts +++ b/common/match/create.ts @@ -10,6 +10,7 @@ import { PrerequisiteError } from '../util/error'; import type { ConcreteMatchPool } from './pool'; import { invalidateAllScreeningsOfPupil } from '../pupil/screening'; import { userForPupil, userForStudent } from '../user'; +import { DAZ } from '../util/subjectsutils'; const logger = getLogger('Match'); @@ -58,9 +59,9 @@ export async function createMatch(pupil: Pupil, student: Student, pool: Concrete await removeInterest(pupil); const callURL = getJitsiTutoringLink(match); - const matchSubjects = getOverlappingSubjects(pupil, student) - .map((it) => it.name) - .join('/'); + const subjects = getOverlappingSubjects(pupil, student).map((it) => it.name); + + const matchSubjects = subjects.join('/'); const tutorFirstMatch = (await prisma.match.count({ where: { studentId: student.id } })) === 1; const tuteeFirstMatch = (await prisma.match.count({ where: { pupilId: pupil.id } })) === 1; @@ -81,6 +82,13 @@ export async function createMatch(pupil: Pupil, student: Student, pool: Concrete }; await Notification.actionTaken(userForStudent(student), `tutor_matching_success`, tutorContext); + + await Notification.actionTaken( + userForStudent(student), + subjects.includes(DAZ) ? 'tutor_daz_matching_success' : 'tutor_standard_matching_success', + tutorContext + ); + await Notification.actionTaken(userForStudent(student), `tutor_matching_${pool.name}`, tutorContext); const tuteeContext = { diff --git a/common/notification/actions.ts b/common/notification/actions.ts index f8cb6049c..129c74cbb 100644 --- a/common/notification/actions.ts +++ b/common/notification/actions.ts @@ -279,6 +279,14 @@ const _notificationActions = { }, }, }, + instructor_first_appointment_completed: { + description: 'Instructor / Completed first appointment', + sampleContext: { + uniqueId: 'REQUIRED', + appointment: sampleAppointment, + ...sampleCourse, + }, + }, instructor_course_ended: { description: 'Instructor / Course ended', sampleContext: sampleCourse, @@ -431,6 +439,26 @@ const _notificationActions = { firstMatch: true, }, }, + tutor_daz_matching_success: { + description: 'Tutor / DaZ Match success', + sampleContext: { + pupil: sampleUser, + pupilGrade: '3. Klasse', + matchHash: '...', + matchDate: '...', + firstMatch: true, + }, + }, + tutor_standard_matching_success: { + description: 'Tutor / Standard Match success', + sampleContext: { + pupil: sampleUser, + pupilGrade: '3. Klasse', + matchHash: '...', + matchDate: '...', + firstMatch: true, + }, + }, 'tutor_matching_lern-fair-now': { description: 'Tutor / Lern-Fair Now Match success', sampleContext: { diff --git a/common/notification/hook.ts b/common/notification/hook.ts index 4b7a7b360..9bf40d0ab 100644 --- a/common/notification/hook.ts +++ b/common/notification/hook.ts @@ -5,25 +5,26 @@ import { student as Student, pupil as Pupil } from '@prisma/client'; import { getPupil, getStudent, User } from '../user'; +import { NotificationContext } from './types'; -type NotificationHook = { fn: (user: User) => Promise; description: string }; +type NotificationHook = { fn: (user: User, context: NotificationContext) => Promise; description: string }; const hooks: { [hookID: string]: NotificationHook } = {}; export const hookExists = (hookID: string) => hookID in hooks; export const getHookDescription = (hookID: string) => hooks[hookID]?.description; -export async function triggerHook(hookID: string, user: User) { +export async function triggerHook(hookID: string, user: User, context: NotificationContext) { if (!hookExists(hookID)) { throw new Error(`Unknown hook ${hookID}`); } const hook = hooks[hookID]; - await hook.fn(user); + await hook.fn(user, context); } -export function registerHook(hookID: string, description: string, fn: (user: User) => Promise) { +export function registerHook(hookID: string, description: string, fn: (user: User, context: NotificationContext) => Promise) { if (hookExists(hookID)) { throw new Error(`Hook may only be registered once`); } @@ -31,8 +32,8 @@ export function registerHook(hookID: string, description: string, fn: (user: Use hooks[hookID] = { description, fn }; } -export const registerStudentHook = (hookID: string, description: string, hook: (student: Student) => Promise) => - registerHook(hookID, description, (user) => getStudent(user).then(hook)); +export const registerStudentHook = (hookID: string, description: string, hook: (student: Student, context: NotificationContext) => Promise) => + registerHook(hookID, description, (user, context) => getStudent(user).then((student) => hook(student, context))); -export const registerPupilHook = (hookID: string, description: string, hook: (pupil: Pupil) => Promise) => - registerHook(hookID, description, (user) => getPupil(user).then(hook)); +export const registerPupilHook = (hookID: string, description: string, hook: (pupil: Pupil, context: NotificationContext) => Promise) => + registerHook(hookID, description, (user, context) => getPupil(user).then((pupil) => hook(pupil, context))); diff --git a/common/notification/hooks.ts b/common/notification/hooks.ts index 8f122bdcb..e79d695d5 100644 --- a/common/notification/hooks.ts +++ b/common/notification/hooks.ts @@ -4,6 +4,10 @@ import { registerPupilHook, registerStudentHook } from './hook'; // this ensures that the hooks are always registered when the Notification is loaded (i.e. in the jobs Deno, which only loads parts of the backend) import { deactivateStudent } from '../student/activation'; import { cancelRemissionRequest } from '../remission-request'; +import { prisma } from '../prisma'; +import { userForStudent } from '../user'; +import * as Notification from '../../common/notification'; +import { SpecificNotificationContext } from './actions'; registerStudentHook( 'deactivate-student', @@ -17,6 +21,28 @@ registerStudentHook('cancel-remission-request', 'Cancels the remission request(s await cancelRemissionRequest(student); }); +registerStudentHook( + 'attempt-invite-instructor-to-reflection-meeting', + 'Instructors (without matches) are invited to participate in a reflection meeting after their very first group appointment', + async (student, context) => { + const activeMatches = await prisma.match.count({ where: { studentId: student.id, dissolved: false } }); + if (activeMatches > 0) { + return; + } + const user = userForStudent(student); + const appointmentContext = context as SpecificNotificationContext<'student_group_appointment_starts'>; + const triggeredAppointmentId = Number(context.uniqueId); + const firstInstructorAppointment = await prisma.lecture.findFirst({ + where: { organizerIds: { has: user.userID }, isCanceled: false, appointmentType: 'group' }, + orderBy: { start: 'asc' }, + }); + const isFirstInstructorAppointment = triggeredAppointmentId === firstInstructorAppointment.id; + if (isFirstInstructorAppointment) { + await Notification.actionTaken(user, 'instructor_first_appointment_completed', appointmentContext); + } + } +); + import { deletePupilMatchRequest } from '../match/request'; import { deactivatePupil } from '../pupil/activation'; registerPupilHook('revoke-pupil-match-request', 'Match Request is taken back, pending Pupil Screenings are invalidated', async (pupil) => { diff --git a/common/notification/index.ts b/common/notification/index.ts index a276d2c4e..eb7a2663c 100644 --- a/common/notification/index.ts +++ b/common/notification/index.ts @@ -106,7 +106,8 @@ async function deliverNotification( try { // Always trigger the hook, no matter whether we actually send something to the user if (notification.hookID) { - await triggerHook(notification.hookID, user); + logger.debug(`Running Hook(${notification.hookID}) for ConcreteNotification(${concreteNotification.id})`); + await triggerHook(notification.hookID, user, context); } const channelPreferencesForMessageType = await getNotificationChannelPreferences(user, concreteNotification); diff --git a/common/util/subjectsutils.ts b/common/util/subjectsutils.ts index c18a7cc31..cf68e5aa7 100644 --- a/common/util/subjectsutils.ts +++ b/common/util/subjectsutils.ts @@ -115,3 +115,5 @@ export function parseSubjectString(subjects: string): Subject[] { }; }); } + +export const DAZ = 'Deutsch als Zweitsprache';