From 8f1554a250b6a59921638de789ed41cb73b0dfdc Mon Sep 17 00:00:00 2001 From: Daniel Henkel Date: Tue, 18 Jun 2024 20:00:17 +0200 Subject: [PATCH] derive student achievements --- common/achievement/derive.ts | 148 +++++++++++++++++++++++++++++++++-- common/achievement/metric.ts | 3 + seed-db.ts | 77 +++++++++++++++++- 3 files changed, 220 insertions(+), 8 deletions(-) diff --git a/common/achievement/derive.ts b/common/achievement/derive.ts index 36ee05b10..e3130bbe4 100644 --- a/common/achievement/derive.ts +++ b/common/achievement/derive.ts @@ -1,5 +1,6 @@ import { pupil as Pupil, + student as Student, achievement_action_type_enum as AchievementActionType, achievement_template_for_enum as AchievementTemplateFor, achievement_type_enum as AchievementType, @@ -7,7 +8,7 @@ import { pupil_screening_status_enum, Prisma, } from '@prisma/client'; -import { User, getPupil } from '../user'; +import { User, getPupil, getStudent } from '../user'; import { prisma } from '../prisma'; import { achievement_with_template } from './types'; import { getAchievementTemplates, TemplateSelectEnum } from './template'; @@ -15,6 +16,8 @@ import { createRelation, EventRelationType } from './relation'; const PupilNewMatchGroup = 'pupil_new_match'; const PupilNewMatchGroupOrder = 3; +const StudentNewMatchGroup = 'student_new_match'; +const StudentNewMatchGroupOrder = 3; const GhostAchievements: { [key: string]: achievement_template } = { pupil_new_match_1: { @@ -65,6 +68,54 @@ const GhostAchievements: { [key: string]: achievement_template } = { achievedImage: null, sequentialStepName: 'Gespräch mit Lern-Fair absolvieren', }, + student_new_match_1: { + id: -1, + templateFor: AchievementTemplateFor.Match, + group: StudentNewMatchGroup, + groupOrder: 1, + type: AchievementType.SEQUENTIAL, + image: 'gamification/achievements/release/finish_onboarding/two_pieces/step_1.png', + tagline: 'Starte eine Lernpatenschaft', + title: 'Neue Lernunterstützung', + subtitle: null, + description: + 'Es war großartig, dich am {{date}} besser kennenzulernen und freuen uns, dass du gemeinsam mit uns die Bildungschancen von Schüler:innen verbessern möchtest. Um dir eine:n passende:n Lernpartner:in zuzuweisen, bitten wir dich zunächst, eine Anfrage auf unserer Plattform zu stellen. Hier kannst du die Fächer und Jahrgangsstufe angeben, die für dich passend sind. Wir freuen uns auf den Start!', + footer: null, + actionName: 'Anfrage stellen', + actionRedirectLink: '/matching', + actionType: AchievementActionType.Action, + condition: 'false', // This will ensure that an evaluation will always fail + conditionDataAggregations: {}, + isActive: true, + achievedDescription: null, + achievedFooter: null, + achievedImage: null, + sequentialStepName: 'Anfrage stellen', + }, + student_new_match_2: { + id: -1, + templateFor: AchievementTemplateFor.Match, + group: StudentNewMatchGroup, + groupOrder: 2, + type: AchievementType.SEQUENTIAL, + image: 'gamification/achievements/release/finish_onboarding/two_pieces/step_2.png', + tagline: 'Starte eine Lernpatenschaft', + title: 'Neue Lernunterstützung', + subtitle: null, + description: + 'Fantastisch, deine Anfrage ist eingegangen! Bevor wir dir deine:n ideale:n Lernpartner:in vermitteln können, möchten wir gerne kurz per Zoom mit dir sprechen. Unser Ziel ist es, die perfekte Person für dich zu finden und genau zu verstehen, was du dir wünschst. Buche doch gleich einen Termin für unser Gespräch – wir sind schon ganz gespannt auf dich!', + footer: null, + actionName: 'Termin buchen', + actionRedirectLink: 'https://calendly.com', + actionType: AchievementActionType.Action, + condition: 'false', + conditionDataAggregations: {}, + isActive: true, + achievedDescription: null, + achievedFooter: null, + achievedImage: null, + sequentialStepName: 'Gespräch mit Lern-Fair absolvieren', + }, }; // Large parts of our user communication are event based, i.e. users get a notification for an appointment, @@ -80,16 +131,12 @@ export async function deriveAchievements(user: User, realAchievements: achieveme if (user.pupilId) { const pupil = await getPupil(user); - - // await derivePupilOnboarding(pupil, result); await derivePupilMatching(user, pupil, result, realAchievements); } if (user.studentId) { - // const student = await getStudent(user); - // await deriveStudentOnboarding(student, result); - // await deriveStudentMatching(student, result); - // ... + const student = await getStudent(user); + await deriveStudentMatching(user, student, result, realAchievements); } return result; @@ -185,3 +232,90 @@ async function derivePupilMatching(user: User, pupil: Pupil, result: achievement result.push(...ghosts); } } + +interface StudentNewMatchGhostContext extends Prisma.JsonObject { + lastScreeningDate: string | null; +} + +async function deriveStudentMatching(user: User, student: Student, result: achievement_with_template[], userAchievements: achievement_with_template[]) { + const hasRequest = student.openMatchRequestCount > 0; + const successfulScreenings = await prisma.screening.findMany({ + where: { studentId: student.id, success: true }, + orderBy: { createdAt: 'desc' }, + }); + + const newMatchAchievements = userAchievements.filter( + (row) => row.template.group === StudentNewMatchGroup && row.template.groupOrder === StudentNewMatchGroupOrder + ); + + const ctx: StudentNewMatchGhostContext = { + lastScreeningDate: null, + }; + if (successfulScreenings.length > 0) { + ctx.lastScreeningDate = successfulScreenings[0].updatedAt.toISOString(); + } + for (let i = 0; i < student.openMatchRequestCount; i++) { + const ghosts = await generateStudentMatching(null, user, hasRequest, successfulScreenings.length > 0, ctx); + result.push(...ghosts); + } + + for (const userAchievement of newMatchAchievements) { + const ghosts = await generateStudentMatching(userAchievement, user, hasRequest, successfulScreenings.length > 0, ctx); + result.push(...ghosts); + } +} + +async function generateStudentMatching( + achievement: achievement_with_template | null, + user: User, + hasRequest: boolean, + hasSuccessfulScreening: boolean, + ctx: StudentNewMatchGhostContext +): Promise { + const result: achievement_with_template[] = []; + // Generating a ramdom relation to be able to show multiple sequences of this kind in parallel + const randomRelation = createRelation(EventRelationType.Match, Math.random()) + '-tmp'; + if (!achievement) { + const groups = await getAchievementTemplates(TemplateSelectEnum.BY_GROUP); + if (!groups.has(StudentNewMatchGroup) || groups.get(StudentNewMatchGroup).length === 0) { + throw new Error('group template not found!'); + } + // If there is no real achievement yet, we have to fake the first one in the row as well + result.push({ + id: -1, + templateId: -1, + userId: user.userID, + isSeen: true, + template: groups.get(StudentNewMatchGroup)[0], + context: ctx, + recordValue: null, + achievedAt: null, + relation: randomRelation, + }); + } + + result.push({ + id: -1, + templateId: -1, + userId: user.userID, + isSeen: true, + template: GhostAchievements.student_new_match_1, + context: ctx, + recordValue: null, + achievedAt: hasRequest || achievement ? new Date() : null, + relation: achievement?.relation ?? randomRelation, + }); + + result.push({ + id: -1, + templateId: -1, + userId: user.userID, + isSeen: true, + template: GhostAchievements.student_new_match_2, + context: ctx, + recordValue: null, + achievedAt: hasSuccessfulScreening || achievement ? new Date() : null, + relation: achievement?.relation ?? randomRelation, + }); + return result; +} diff --git a/common/achievement/metric.ts b/common/achievement/metric.ts index d0073320e..172ff0f65 100644 --- a/common/achievement/metric.ts +++ b/common/achievement/metric.ts @@ -109,6 +109,9 @@ const batchOfMetrics = [ createMetric('pupil_match_create', ['tutee_matching_success'], () => { return 1; }), + createMetric('student_match_create', ['tutor_matching_success'], () => { + return 1; + }), ]; export function registerAchievementMetrics() { diff --git a/seed-db.ts b/seed-db.ts index 11257d0ec..d70298d10 100644 --- a/seed-db.ts +++ b/seed-db.ts @@ -886,6 +886,81 @@ void (async function setupDevDB() { actionName: '{{var:student.firstname}} kontaktieren', actionRedirectLink: '/chat', actionType: achievement_action_type_enum.Action, + condition: 'pupil_verified_events > 0', + conditionDataAggregations: JSON.parse('{"pupil_verified_events":{"metric":"pupil_onboarding_verified","aggregator":"count"}}'), + isActive: true, + }, + }); + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'pupil_new_match', + groupOrder: 5, + sequentialStepName: 'Erstes Gespräch absolvieren', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: '{{name}}', + subtitle: null, + footer: null, + achievedFooter: 'Wow! Du hast alle Schritte abgeschlossen.', + description: + 'Wow, die Vorfreude steigt – bald startet eure gemeinsame Reise! 🚀 Wir wünschen dir viel Spaß bei deinem ersten Termin in der Lernunterstützung mit {{name}} und hoffen, dass ihr euch gut versteht und alles klappt. Für dein erstes Treffen haben wir einen Leitfaden zusammengestellt, der dir hilfreiche Tipps, Tricks und spannende Gesprächsthemen bietet. Nutze ihn, um dich optimal vorzubereiten und das Beste aus eurer Zusammenarbeit herauszuholen!', + achievedDescription: + 'Herzlichen Glückwunsch zu deinem erfolgreichen ersten Termin in der Lernunterstützung mit {{name}}! Möge diese Begegnung der Beginn einer spannenden und produktiven Lernreise sein. Wir sind sicher, dass eure Zusammenarbeit von Freude und Erfolg geprägt sein wird. Auf eine inspirierende Zeit des gemeinsamen Lernens!', + image: 'gamification/achievements/release/finish_onboarding/three_pieces/empty_state.png', + achievedImage: null, + actionName: 'Zum Termin', + actionRedirectLink: 'isso', + actionType: achievement_action_type_enum.Appointment, + condition: 'pupil_verified_events > 0', + conditionDataAggregations: JSON.parse('{"pupil_verified_events":{"metric":"pupil_onboarding_verified","aggregator":"count"}}'), + isActive: true, + }, + }); + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'student_new_match', + groupOrder: 3, + sequentialStepName: 'Lernpartner:in erhalten', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: 'Starte eine Lernpatenschaft', + subtitle: null, + footer: null, + achievedFooter: null, + description: '', + achievedDescription: null, + image: 'gamification/achievements/release/finish_onboarding/three_pieces/empty_state.png', + achievedImage: null, + actionName: 'Warten', + actionRedirectLink: null, + actionType: achievement_action_type_enum.Wait, + condition: 'student_match_create > 0', + conditionDataAggregations: JSON.parse('{"student_match_create":{"metric":"student_match_create","aggregator":"count"}}'), + isActive: true, + }, + }); + await prisma.achievement_template.create({ + data: { + templateFor: achievement_template_for_enum.Match, + group: 'student_new_match', + groupOrder: 4, + sequentialStepName: 'Lernpartner:in kontaktieren', + type: achievement_type_enum.SEQUENTIAL, + title: 'Neue Lernunterstützung', + tagline: '{{name}}', + subtitle: null, + footer: null, + achievedFooter: null, + description: + 'Hurra, wir haben eine:n Lernpartner:in für dich gefunden! 🎉{{var:student.firstname}} ist super motiviert, dir in {{var:matchSubjects}} unter die Arme zu greifen. Um möglichst schnell mit {{var:student.firstname}} loszulegen, kontaktieren {{var:student.firstname}} über den Chat und schlage einen Termin für ein erstes Gespräch vor. {{var:student.firstname}} kann es kaum erwarten, dich kennenzulernen und gemeinsam mit dir durchzustarten!', + achievedDescription: null, + image: 'gamification/achievements/release/finish_onboarding/three_pieces/empty_state.png', + achievedImage: null, + actionName: '{{var:pupil.firstname}} kontaktieren', + actionRedirectLink: '/chat', + actionType: achievement_action_type_enum.Action, condition: 'student_verified_events > 0', conditionDataAggregations: JSON.parse('{"student_verified_events":{"metric":"student_onboarding_verified","aggregator":"count"}}'), isActive: true, @@ -894,7 +969,7 @@ void (async function setupDevDB() { await prisma.achievement_template.create({ data: { templateFor: achievement_template_for_enum.Match, - group: 'pupil_new_match', + group: 'student_new_match', groupOrder: 5, sequentialStepName: 'Erstes Gespräch absolvieren', type: achievement_type_enum.SEQUENTIAL,