From 32d9933dedf825b5fb5b22621098e4121b4b7328 Mon Sep 17 00:00:00 2001 From: ninom5 Date: Fri, 10 Oct 2025 00:23:05 +0200 Subject: [PATCH 1/4] add cron job for sending email interview invitations --- apps/api/package.json | 1 + apps/api/src/app.module.ts | 2 + apps/api/src/email/email.service.ts | 60 +++++++++++++++++++ apps/api/src/intern/intern.module.ts | 4 +- .../interview-slot/interview-slot.module.ts | 1 - apps/api/src/test-slot/test-slot.module.ts | 4 +- yarn.lock | 25 ++++++++ 7 files changed, 94 insertions(+), 3 deletions(-) diff --git a/apps/api/package.json b/apps/api/package.json index 9243dba6..bb6b15c7 100644 --- a/apps/api/package.json +++ b/apps/api/package.json @@ -25,6 +25,7 @@ "@nestjs/jwt": "^10.1.1", "@nestjs/passport": "^11.0.5", "@nestjs/platform-express": "^9.0.0", + "@nestjs/schedule": "^6.0.1", "@nestjs/serve-static": "^4.0.0", "@nestjs/swagger": "^7.1.10", "@prisma/client": "^5.2.0", diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index 0694c856..a02435dd 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -15,6 +15,7 @@ import { LoggerModule } from './logger/logger.module'; import { OldInternResultModule } from './old-intern-result/old-intern-result.module'; import { PrismaService } from './prisma.service'; import { TestSlotModule } from './test-slot/test-slot.module'; +import { ScheduleModule } from '@nestjs/schedule'; @Module({ imports: [ @@ -60,6 +61,7 @@ import { TestSlotModule } from './test-slot/test-slot.module'; QuestionModule, InternshipApplicationStatusModule, OldInternResultModule, + ScheduleModule.forRoot(), ], controllers: [AppController], providers: [AppService, PrismaService], diff --git a/apps/api/src/email/email.service.ts b/apps/api/src/email/email.service.ts index 259469a5..84c4b084 100644 --- a/apps/api/src/email/email.service.ts +++ b/apps/api/src/email/email.service.ts @@ -2,6 +2,8 @@ import { Injectable } from '@nestjs/common'; import * as nunjucks from 'nunjucks'; import * as postmark from 'postmark'; import { PrismaService } from 'src/prisma.service'; +import { Cron } from '@nestjs/schedule'; +import { Intern } from '@internship-app/types'; @Injectable() export class EmailService { @@ -47,6 +49,7 @@ export class EmailService { (email) => email.internId === intern.id, ).id; const trackImage = ``; + return this.postmark.sendEmail({ From: 'info@dump.hr', To: intern.email, @@ -123,4 +126,61 @@ export class EmailService { }); } } + + @Cron('0 0 8 * * *', { timeZone: 'UTC+2' }) + async sendInterviewInvitations() { + const newInterns: Intern[] = await this.prisma.$queryRaw` + SELECT i.email + FROM "Intern" i + WHERE i."createdAt" >= NOW() - INTERVAL '1 day' + AND ( + ( + NOT EXISTS ( + SELECT 1 + FROM "InternDiscipline" id + WHERE id."internId" = i.id + AND id."discipline" = 'Development' + ) + AND NOT EXISTS ( + SELECT 1 + FROM "InternDiscipline" id + WHERE id."internId" = i.id + AND id."discipline" <> 'Development' + AND id.status <> 'Pending' + ) + ) + OR + ( + EXISTS ( + SELECT 1 + FROM "InternDiscipline" id + WHERE id."internId" = i.id + AND id."discipline" = 'Development' + AND id.status = 'Rejected' + ) + AND ( + NOT EXISTS ( + SELECT 1 + FROM "InternDiscipline" id + WHERE id."internId" = i.id + AND id."discipline" <> 'Development' + AND id.status <> 'Approved' + ) + OR EXISTS ( + SELECT 1 + FROM "InternDiscipline" id + WHERE id."internId" = i.id + AND id."discipline" = 'Development' + AND id.status = 'Pending' + ) + ) + ) + ); +`; + + console.log('NEW INTERNS', newInterns); + const emails = newInterns.map((intern) => intern.email); + + this.sendEmail(emails, `pozivamo te na intervju...`, 'poziv na intervju'); + } } diff --git a/apps/api/src/intern/intern.module.ts b/apps/api/src/intern/intern.module.ts index 2da25953..0f888fdf 100644 --- a/apps/api/src/intern/intern.module.ts +++ b/apps/api/src/intern/intern.module.ts @@ -5,9 +5,11 @@ import { PrismaService } from 'src/prisma.service'; import { InternController } from './intern.controller'; import { InternService } from './intern.service'; +import { EmailModule } from 'src/email/email.module'; @Module({ controllers: [InternController], - providers: [InternService, LoggerService, EmailService, PrismaService], + providers: [InternService, LoggerService, PrismaService], + imports: [EmailModule], }) export class InternModule {} diff --git a/apps/api/src/interview-slot/interview-slot.module.ts b/apps/api/src/interview-slot/interview-slot.module.ts index 7412cc46..d04ffb62 100644 --- a/apps/api/src/interview-slot/interview-slot.module.ts +++ b/apps/api/src/interview-slot/interview-slot.module.ts @@ -14,7 +14,6 @@ import { InterviewSlotService } from './interview-slot.service'; providers: [ InterviewSlotService, LoggerService, - EmailService, MicrosoftGraphService, PrismaService, ], diff --git a/apps/api/src/test-slot/test-slot.module.ts b/apps/api/src/test-slot/test-slot.module.ts index a466feba..91df93ba 100644 --- a/apps/api/src/test-slot/test-slot.module.ts +++ b/apps/api/src/test-slot/test-slot.module.ts @@ -5,9 +5,11 @@ import { PrismaService } from 'src/prisma.service'; import { TestSlotController } from './test-slot.controller'; import { TestSlotService } from './test-slot.service'; +import { EmailModule } from 'src/email/email.module'; @Module({ controllers: [TestSlotController], - providers: [TestSlotService, LoggerService, PrismaService, EmailService], + providers: [TestSlotService, LoggerService, PrismaService], + imports: [EmailModule], }) export class TestSlotModule {} diff --git a/yarn.lock b/yarn.lock index b0e4ca83..d1a4003e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -1397,6 +1397,13 @@ multer "1.4.4-lts.1" tslib "2.5.3" +"@nestjs/schedule@^6.0.1": + version "6.0.1" + resolved "https://registry.yarnpkg.com/@nestjs/schedule/-/schedule-6.0.1.tgz#048b34e5c16d2d9fa14a9df2bfa2d3b7fa8ef168" + integrity sha512-v3yO6cSPAoBSSyH67HWnXHzuhPhSNZhRmLY38JvCt2sqY8sPMOODpcU1D79iUMFf7k16DaMEbL4Mgx61ZhiC8Q== + dependencies: + cron "4.3.3" + "@nestjs/schematics@^9.0.0", "@nestjs/schematics@^9.0.4": version "9.2.0" resolved "https://registry.yarnpkg.com/@nestjs/schematics/-/schematics-9.2.0.tgz#f840054b5ae4b0b4e70aa9f72c09c3cf388f2512" @@ -2194,6 +2201,11 @@ resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.199.tgz#c3edb5650149d847a277a8961a7ad360c474e9bf" integrity sha512-Vrjz5N5Ia4SEzWWgIVwnHNEnb1UE1XMkvY5DGXrAeOGE9imk0hgTHh5GyDjLDJi9OTCn9oo9dXH1uToK1VRfrg== +"@types/luxon@~3.7.0": + version "3.7.1" + resolved "https://registry.yarnpkg.com/@types/luxon/-/luxon-3.7.1.tgz#ef51b960ff86801e4e2de80c68813a96e529d531" + integrity sha512-H3iskjFIAn5SlJU7OuxUmTEpebK6TKB8rxZShDslBMZJ5u9S//KM1sbdAisiSrqwLQncVjnpi2OK2J51h+4lsg== + "@types/mime@*": version "3.0.1" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-3.0.1.tgz#5f8f2bca0a5863cb69bc0b0acd88c96cb1d4ae10" @@ -3368,6 +3380,14 @@ create-require@^1.1.0: resolved "https://registry.yarnpkg.com/create-require/-/create-require-1.1.1.tgz#c1d7e8f1e5f6cfc9ff65f9cd352d37348756c333" integrity sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ== +cron@4.3.3: + version "4.3.3" + resolved "https://registry.yarnpkg.com/cron/-/cron-4.3.3.tgz#d37cfcbc73ba34a50d9d9ce9b653ae60837377d7" + integrity sha512-B/CJj5yL3sjtlun6RtYHvoSB26EmQ2NUmhq9ZiJSyKIM4K/fqfh9aelDFlIayD2YMeFZqWLi9hHV+c+pq2Djkw== + dependencies: + "@types/luxon" "~3.7.0" + luxon "~3.7.0" + cross-spawn@^5.0.1: version "5.1.0" resolved "https://registry.yarnpkg.com/cross-spawn/-/cross-spawn-5.1.0.tgz#e8bd0efee58fcff6f8f94510a0a554bbfa235449" @@ -4910,6 +4930,11 @@ luxon@^3.2.1: resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.4.3.tgz#8ddf0358a9492267ffec6a13675fbaab5551315d" integrity sha512-tFWBiv3h7z+T/tDaoxA8rqTxy1CHV6gHS//QdaH4pulbq/JuBSGgQspQQqcgnwdAx6pNI7cmvz5Sv/addzHmUg== +luxon@~3.7.0: + version "3.7.2" + resolved "https://registry.yarnpkg.com/luxon/-/luxon-3.7.2.tgz#d697e48f478553cca187a0f8436aff468e3ba0ba" + integrity sha512-vtEhXh/gNjI9Yg1u4jX/0YVPMvxzHuGgCm6tC5kZyb08yjGWGnqAjGJvcXbqQR2P3MyMEFnRbpcdFS6PBcLqew== + macos-release@^2.5.0: version "2.5.1" resolved "https://registry.yarnpkg.com/macos-release/-/macos-release-2.5.1.tgz#bccac4a8f7b93163a8d163b8ebf385b3c5f55bf9" From 5a313033f3f637fa19deea29e1246f01c7536aba Mon Sep 17 00:00:00 2001 From: ninom5 Date: Fri, 10 Oct 2025 00:27:57 +0200 Subject: [PATCH 2/4] fix subject, ad comment --- apps/api/src/email/email.service.ts | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/apps/api/src/email/email.service.ts b/apps/api/src/email/email.service.ts index 84c4b084..fcbce2ec 100644 --- a/apps/api/src/email/email.service.ts +++ b/apps/api/src/email/email.service.ts @@ -181,6 +181,26 @@ export class EmailService { console.log('NEW INTERNS', newInterns); const emails = newInterns.map((intern) => intern.email); - this.sendEmail(emails, `pozivamo te na intervju...`, 'poziv na intervju'); + this.sendEmail( + emails, + `Vezano za tvoju prijavu na ovogodišnji DUMP Internship, pozivamo te da sudjeluješ u prvom krugu intervjua koji će se održati kroz ovaj tjedan. Ako ti ne odgovara ni jedan od ponuđenih termina, ne brini - vremenom ćemo ih dodati još. +Pripazi; kako vrijeme bude prolazilo, bit će sve manja ponuda termina. +Ovo su tvoja prijavljena područja: +Ukoliko želiš promijeniti prijavljena područja, javi nam se na info@dump.hr, i to prije nego što odabereš intervju termin. +Molimo te da u nastavku izabereš termin koji ti najviše odgovara prateći sljedeće upute: +- Otvori svoju status stranivu iz prethodnog maila +- Odaberi jedan od ponuđenih dostupnih datuma, nakon čega ti se otvore dostupni termini +- Odaberi jedan od ponuđenih termina, nakon čega ti se otvori potvrdna forma +- Klik na confirm i tvoj termin je uspješno rezerviran! +Tvoj intervju će se održati u odabranom terminu u našem uredu (prostorija A223) na FESB-u (Ruđera Boškovića 32). +Naš ured ćeš pronaći tako da kad uđeš kroz glavna vrata FESB-a skreneš desno do kraja hodnika (put referade) dok ne dođeš do stepenica koje su s lijeve strane. Popneš se stepenicama na prvi kat i skreneš lijevo. Nastaviš hodnikom do kraja i s desne strane vidjet ćeš vrata našeg ureda (A223). +Važno: +Molimo te da ne ulaziš u ured dok te ne pozovemo, nego da čekaš ispred. +Ako kojim slučajem ne možeš doći, moguće je intervju organizirati videopozivom uz prethodnu najavu nemogućnosti dolaska. +Međutim, dojam je uvijek bolji uživo! +Vidimo se! +DUMP Udruga mladih programera`, + 'Poziv na dump internrship intervju', + ); } } From 2bdecb1b44b57ab249dc4551a74e19568b48cc98 Mon Sep 17 00:00:00 2001 From: ninom5 Date: Fri, 10 Oct 2025 18:34:02 +0200 Subject: [PATCH 3/4] fix --- apps/api/src/email/email.service.ts | 113 +++++++++++----------------- 1 file changed, 46 insertions(+), 67 deletions(-) diff --git a/apps/api/src/email/email.service.ts b/apps/api/src/email/email.service.ts index fcbce2ec..9ee6ee30 100644 --- a/apps/api/src/email/email.service.ts +++ b/apps/api/src/email/email.service.ts @@ -3,11 +3,20 @@ import * as nunjucks from 'nunjucks'; import * as postmark from 'postmark'; import { PrismaService } from 'src/prisma.service'; import { Cron } from '@nestjs/schedule'; -import { Intern } from '@internship-app/types'; +import { + BoardActionType, + Intern, + InterviewStatus, +} from '@internship-app/types'; +import { InternService } from 'src/intern/intern.service'; +import { BoardAction } from '@internship-app/types'; @Injectable() export class EmailService { - constructor(private readonly prisma: PrismaService) {} + constructor( + private readonly prisma: PrismaService, + private readonly internService: InternService, + ) {} private postmark = new postmark.ServerClient(process.env.POSTMARK_API_TOKEN); @@ -130,76 +139,46 @@ export class EmailService { @Cron('0 0 8 * * *', { timeZone: 'UTC+2' }) async sendInterviewInvitations() { const newInterns: Intern[] = await this.prisma.$queryRaw` - SELECT i.email - FROM "Intern" i - WHERE i."createdAt" >= NOW() - INTERVAL '1 day' - AND ( - ( - NOT EXISTS ( - SELECT 1 - FROM "InternDiscipline" id - WHERE id."internId" = i.id - AND id."discipline" = 'Development' - ) - AND NOT EXISTS ( - SELECT 1 - FROM "InternDiscipline" id - WHERE id."internId" = i.id - AND id."discipline" <> 'Development' - AND id.status <> 'Pending' - ) - ) - OR - ( - EXISTS ( - SELECT 1 - FROM "InternDiscipline" id - WHERE id."internId" = i.id - AND id."discipline" = 'Development' - AND id.status = 'Rejected' - ) - AND ( - NOT EXISTS ( - SELECT 1 - FROM "InternDiscipline" id - WHERE id."internId" = i.id - AND id."discipline" <> 'Development' - AND id.status <> 'Approved' - ) - OR EXISTS ( - SELECT 1 - FROM "InternDiscipline" id - WHERE id."internId" = i.id - AND id."discipline" = 'Development' - AND id.status = 'Pending' - ) - ) - ) - ); -`; - - console.log('NEW INTERNS', newInterns); + select i.email + from "Intern" i + left join "InternDiscipline" id on i.id = id."internId" + where i."createdAt" >= NOW() - INTERVAL '1 day' + and id."discipline" <> 'Development' + and id.status = 'Pending' + and i."interviewStatus" = 'Pending' + `; + + const ids = newInterns.map((newIntern) => newIntern.id); + + this.internService.applyBoardAction( + { + actionType: BoardActionType.SetInterviewStatus, + interviewStatus: InterviewStatus.PickTerm, + }, + ids, + ); + const emails = newInterns.map((intern) => intern.email); this.sendEmail( emails, `Vezano za tvoju prijavu na ovogodišnji DUMP Internship, pozivamo te da sudjeluješ u prvom krugu intervjua koji će se održati kroz ovaj tjedan. Ako ti ne odgovara ni jedan od ponuđenih termina, ne brini - vremenom ćemo ih dodati još. -Pripazi; kako vrijeme bude prolazilo, bit će sve manja ponuda termina. -Ovo su tvoja prijavljena područja: -Ukoliko želiš promijeniti prijavljena područja, javi nam se na info@dump.hr, i to prije nego što odabereš intervju termin. -Molimo te da u nastavku izabereš termin koji ti najviše odgovara prateći sljedeće upute: -- Otvori svoju status stranivu iz prethodnog maila -- Odaberi jedan od ponuđenih dostupnih datuma, nakon čega ti se otvore dostupni termini -- Odaberi jedan od ponuđenih termina, nakon čega ti se otvori potvrdna forma -- Klik na confirm i tvoj termin je uspješno rezerviran! -Tvoj intervju će se održati u odabranom terminu u našem uredu (prostorija A223) na FESB-u (Ruđera Boškovića 32). -Naš ured ćeš pronaći tako da kad uđeš kroz glavna vrata FESB-a skreneš desno do kraja hodnika (put referade) dok ne dođeš do stepenica koje su s lijeve strane. Popneš se stepenicama na prvi kat i skreneš lijevo. Nastaviš hodnikom do kraja i s desne strane vidjet ćeš vrata našeg ureda (A223). -Važno: -Molimo te da ne ulaziš u ured dok te ne pozovemo, nego da čekaš ispred. -Ako kojim slučajem ne možeš doći, moguće je intervju organizirati videopozivom uz prethodnu najavu nemogućnosti dolaska. -Međutim, dojam je uvijek bolji uživo! -Vidimo se! -DUMP Udruga mladih programera`, + Pripazi; kako vrijeme bude prolazilo, bit će sve manja ponuda termina. + Ovo su tvoja prijavljena područja: + Ukoliko želiš promijeniti prijavljena područja, javi nam se na info@dump.hr, i to prije nego što odabereš intervju termin. + Molimo te da u nastavku izabereš termin koji ti najviše odgovara prateći sljedeće upute: + - Otvori svoju status stranivu iz prethodnog maila + - Odaberi jedan od ponuđenih dostupnih datuma, nakon čega ti se otvore dostupni termini + - Odaberi jedan od ponuđenih termina, nakon čega ti se otvori potvrdna forma + - Klik na confirm i tvoj termin je uspješno rezerviran! + Tvoj intervju će se održati u odabranom terminu u našem uredu (prostorija A223) na FESB-u (Ruđera Boškovića 32). + Naš ured ćeš pronaći tako da kad uđeš kroz glavna vrata FESB-a skreneš desno do kraja hodnika (put referade) dok ne dođeš do stepenica koje su s lijeve strane. Popneš se stepenicama na prvi kat i skreneš lijevo. Nastaviš hodnikom do kraja i s desne strane vidjet ćeš vrata našeg ureda (A223). + Važno: + Molimo te da ne ulaziš u ured dok te ne pozovemo, nego da čekaš ispred. + Ako kojim slučajem ne možeš doći, moguće je intervju organizirati videopozivom uz prethodnu najavu nemogućnosti dolaska. + Međutim, dojam je uvijek bolji uživo! + Vidimo se! + DUMP Udruga mladih programera`, 'Poziv na dump internrship intervju', ); } From 30f62274816459e2f44d3ab1a54888f7ca55ffd3 Mon Sep 17 00:00:00 2001 From: ninom5 Date: Sun, 12 Oct 2025 10:17:31 +0200 Subject: [PATCH 4/4] fix lint warnings --- apps/api/src/app.module.ts | 2 +- apps/api/src/email/email.service.ts | 11 +++++------ apps/api/src/intern/intern.module.ts | 3 +-- apps/api/src/interview-slot/interview-slot.module.ts | 1 - apps/api/src/test-slot/test-slot.module.ts | 3 +-- 5 files changed, 8 insertions(+), 12 deletions(-) diff --git a/apps/api/src/app.module.ts b/apps/api/src/app.module.ts index a02435dd..6a2117c4 100644 --- a/apps/api/src/app.module.ts +++ b/apps/api/src/app.module.ts @@ -1,4 +1,5 @@ import { Module } from '@nestjs/common'; +import { ScheduleModule } from '@nestjs/schedule'; import { ServeStaticModule } from '@nestjs/serve-static'; import { extname, join } from 'path'; @@ -15,7 +16,6 @@ import { LoggerModule } from './logger/logger.module'; import { OldInternResultModule } from './old-intern-result/old-intern-result.module'; import { PrismaService } from './prisma.service'; import { TestSlotModule } from './test-slot/test-slot.module'; -import { ScheduleModule } from '@nestjs/schedule'; @Module({ imports: [ diff --git a/apps/api/src/email/email.service.ts b/apps/api/src/email/email.service.ts index 9ee6ee30..13add31e 100644 --- a/apps/api/src/email/email.service.ts +++ b/apps/api/src/email/email.service.ts @@ -1,15 +1,14 @@ -import { Injectable } from '@nestjs/common'; -import * as nunjucks from 'nunjucks'; -import * as postmark from 'postmark'; -import { PrismaService } from 'src/prisma.service'; -import { Cron } from '@nestjs/schedule'; import { BoardActionType, Intern, InterviewStatus, } from '@internship-app/types'; +import { Injectable } from '@nestjs/common'; +import { Cron } from '@nestjs/schedule'; +import * as nunjucks from 'nunjucks'; +import * as postmark from 'postmark'; import { InternService } from 'src/intern/intern.service'; -import { BoardAction } from '@internship-app/types'; +import { PrismaService } from 'src/prisma.service'; @Injectable() export class EmailService { diff --git a/apps/api/src/intern/intern.module.ts b/apps/api/src/intern/intern.module.ts index 0f888fdf..1b6e8bfe 100644 --- a/apps/api/src/intern/intern.module.ts +++ b/apps/api/src/intern/intern.module.ts @@ -1,11 +1,10 @@ import { Module } from '@nestjs/common'; -import { EmailService } from 'src/email/email.service'; +import { EmailModule } from 'src/email/email.module'; import { LoggerService } from 'src/logger/logger.service'; import { PrismaService } from 'src/prisma.service'; import { InternController } from './intern.controller'; import { InternService } from './intern.service'; -import { EmailModule } from 'src/email/email.module'; @Module({ controllers: [InternController], diff --git a/apps/api/src/interview-slot/interview-slot.module.ts b/apps/api/src/interview-slot/interview-slot.module.ts index d04ffb62..90d43a0d 100644 --- a/apps/api/src/interview-slot/interview-slot.module.ts +++ b/apps/api/src/interview-slot/interview-slot.module.ts @@ -1,6 +1,5 @@ import { Module } from '@nestjs/common'; import { EmailModule } from 'src/email/email.module'; -import { EmailService } from 'src/email/email.service'; import { LoggerService } from 'src/logger/logger.service'; import { PrismaService } from 'src/prisma.service'; diff --git a/apps/api/src/test-slot/test-slot.module.ts b/apps/api/src/test-slot/test-slot.module.ts index 91df93ba..ce099435 100644 --- a/apps/api/src/test-slot/test-slot.module.ts +++ b/apps/api/src/test-slot/test-slot.module.ts @@ -1,11 +1,10 @@ import { Module } from '@nestjs/common'; -import { EmailService } from 'src/email/email.service'; +import { EmailModule } from 'src/email/email.module'; import { LoggerService } from 'src/logger/logger.service'; import { PrismaService } from 'src/prisma.service'; import { TestSlotController } from './test-slot.controller'; import { TestSlotService } from './test-slot.service'; -import { EmailModule } from 'src/email/email.module'; @Module({ controllers: [TestSlotController],