From 1bcb94639edd083e3911c8a6c86d35e0fd3c531b Mon Sep 17 00:00:00 2001 From: Martin Kovachki <99181339+Martbul@users.noreply.github.com> Date: Fri, 27 Sep 2024 16:27:44 +0300 Subject: [PATCH] Email send task (#665) * add findAll and findOne for admin only usage * add delete file endpoint, findOne now accessible by organizer and fix fileUpload * re-write isAdminFlag * add send email functionality when new campaign application is added the organizer will receive an email * fix isAdmin import * edit email * fix person type in campaign application * add email template for admin for created campaign-application * fix sendEmailsOnCreatedCampaignApplication method and failing tests * add adminEmail to .env and add promise.all to sendEmailsOnCreatedCampaignApplication method * fix conflict * fix: TS errors * fix test --------- Co-authored-by: Aleksandar Petkov Co-authored-by: Aleksandar --- .env | 4 + .env.example | 4 + .../create-campaign-application-admin.json | 3 + .../create-campaign-application-admin.mjml | 46 ++++++++++ ...create-campaign-application-organizer.json | 3 + ...create-campaign-application-organizer.mjml | 57 +++++++++++++ .../__mocks__/campaign-application-mocks.ts | 6 +- .../campaign-application.module.ts | 4 +- .../campaign-application.service.spec.ts | 85 ++++++++++++++++++- .../campaign-application.service.ts | 74 +++++++++++++--- apps/api/src/email/template.interface.ts | 23 +++++ 11 files changed, 290 insertions(+), 19 deletions(-) create mode 100644 apps/api/src/assets/templates/create-campaign-application-admin.json create mode 100644 apps/api/src/assets/templates/create-campaign-application-admin.mjml create mode 100644 apps/api/src/assets/templates/create-campaign-application-organizer.json create mode 100644 apps/api/src/assets/templates/create-campaign-application-organizer.mjml diff --git a/.env b/.env index 5382ecde7..8568884c4 100644 --- a/.env +++ b/.env @@ -107,3 +107,7 @@ CAMPAIGN_ADMIN_MAIL=responsible for campaign management ## Cache ## ############## CACHE_TTL=30000 + +## AdminEmail ## +############## +CAMPAIGN_COORDINATOR_EMAIL=campaign_coordinators@podkrepi.bg diff --git a/.env.example b/.env.example index 5382ecde7..8568884c4 100644 --- a/.env.example +++ b/.env.example @@ -107,3 +107,7 @@ CAMPAIGN_ADMIN_MAIL=responsible for campaign management ## Cache ## ############## CACHE_TTL=30000 + +## AdminEmail ## +############## +CAMPAIGN_COORDINATOR_EMAIL=campaign_coordinators@podkrepi.bg diff --git a/apps/api/src/assets/templates/create-campaign-application-admin.json b/apps/api/src/assets/templates/create-campaign-application-admin.json new file mode 100644 index 000000000..bdccf1639 --- /dev/null +++ b/apps/api/src/assets/templates/create-campaign-application-admin.json @@ -0,0 +1,3 @@ +{ + "subject": "Създадена нова кампания" +} diff --git a/apps/api/src/assets/templates/create-campaign-application-admin.mjml b/apps/api/src/assets/templates/create-campaign-application-admin.mjml new file mode 100644 index 000000000..f03c84614 --- /dev/null +++ b/apps/api/src/assets/templates/create-campaign-application-admin.mjml @@ -0,0 +1,46 @@ + + + + + + Успешно създадена кампания от {{firstName}} + + + + + + + Организатор {{firstName}} с имейл {{email}} създаде нова кампания + {{campaignApplicationName}}!

+
+ + Поздрави,
+ Екипът на Подкрепи.бг +
+
+
+
+
diff --git a/apps/api/src/assets/templates/create-campaign-application-organizer.json b/apps/api/src/assets/templates/create-campaign-application-organizer.json new file mode 100644 index 000000000..bdccf1639 --- /dev/null +++ b/apps/api/src/assets/templates/create-campaign-application-organizer.json @@ -0,0 +1,3 @@ +{ + "subject": "Създадена нова кампания" +} diff --git a/apps/api/src/assets/templates/create-campaign-application-organizer.mjml b/apps/api/src/assets/templates/create-campaign-application-organizer.mjml new file mode 100644 index 000000000..0c07ffb2b --- /dev/null +++ b/apps/api/src/assets/templates/create-campaign-application-organizer.mjml @@ -0,0 +1,57 @@ + + + + + + Успешно създадохте кампания в Подкрепи.бг + + + + + + + Здравейте {{firstName}}, +

+
+ + Вашата кампания е създадена успешно! Вижте я + ТУК!

+ + Пожелаваме успешно набиране на средствата! +
+ + Поздрави,
+ Екипът на Подкрепи.бг +
+
+
+
+
diff --git a/apps/api/src/campaign-application/__mocks__/campaign-application-mocks.ts b/apps/api/src/campaign-application/__mocks__/campaign-application-mocks.ts index d5a6387e2..b9b2ed45f 100644 --- a/apps/api/src/campaign-application/__mocks__/campaign-application-mocks.ts +++ b/apps/api/src/campaign-application/__mocks__/campaign-application-mocks.ts @@ -1,4 +1,4 @@ -import { CampaignApplicationState } from '@prisma/client' +import { CampaignApplicationState, CampaignTypeCategory } from '@prisma/client' export const mockNewCampaignApplication = { campaignName: 'Test Campaign', @@ -38,12 +38,10 @@ export const mockSingleCampaignApplication = { otherFinanceSources: 'test otherFinanceSources1', otherNotes: 'test otherNotes1', state: CampaignApplicationState.review, - campaignTypeId: 'ffdbcc41-85ec-0000-9e59-0662f3b433af', + category: CampaignTypeCategory.medical, ticketURL: 'testsodifhso1', archived: false, documents: [{ id: 'fileId' }], - campaignEnd: 'funds', - campaignEndDate: undefined, } export const mockCampaigns = [ diff --git a/apps/api/src/campaign-application/campaign-application.module.ts b/apps/api/src/campaign-application/campaign-application.module.ts index 5b0047d23..06c89bf48 100644 --- a/apps/api/src/campaign-application/campaign-application.module.ts +++ b/apps/api/src/campaign-application/campaign-application.module.ts @@ -1,3 +1,4 @@ +import { EmailService } from './../email/email.service' import { Module } from '@nestjs/common' import { CampaignApplicationService } from './campaign-application.service' import { CampaignApplicationController } from './campaign-application.controller' @@ -5,9 +6,10 @@ import { PrismaModule } from '../prisma/prisma.module' import { PersonModule } from '../person/person.module' import { OrganizerModule } from '../organizer/organizer.module' import { S3Service } from '../s3/s3.service' +import { TemplateService } from '../email/template.service' @Module({ imports: [PrismaModule, PersonModule, OrganizerModule], controllers: [CampaignApplicationController], - providers: [CampaignApplicationService, S3Service], + providers: [CampaignApplicationService, S3Service, EmailService, TemplateService], }) export class CampaignApplicationModule {} diff --git a/apps/api/src/campaign-application/campaign-application.service.spec.ts b/apps/api/src/campaign-application/campaign-application.service.spec.ts index d9c35fd89..a157126ac 100644 --- a/apps/api/src/campaign-application/campaign-application.service.spec.ts +++ b/apps/api/src/campaign-application/campaign-application.service.spec.ts @@ -16,10 +16,18 @@ import { mockCampaignApplicationFilesFn, } from './__mocks__/campaing-application-file-mocks' import { CampaignApplicationService } from './campaign-application.service' +import { + CreateCampaignApplicationAdminEmailDto, + CreateCampaignApplicationOrganizerEmailDto, +} from '../email/template.interface' import { CreateCampaignApplicationDto } from './dto/create-campaign-application.dto' +import { EmailService } from '../email/email.service' +import { ConfigService } from 'aws-sdk' +import { ConfigModule } from '@nestjs/config' describe('CampaignApplicationService', () => { let service: CampaignApplicationService + let configService: ConfigService const mockPerson = { ...personMock, @@ -40,13 +48,23 @@ describe('CampaignApplicationService', () => { deleteObject: jest.fn(), } + const mockEmailService = { + sendFromTemplate: jest.fn(), + } + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ + imports: [ + ConfigModule.forFeature(async () => ({ + APP_URL: process.env.APP_URL, + })), + ], providers: [ CampaignApplicationService, MockPrismaService, { provide: OrganizerService, useValue: mockOrganizerService }, { provide: S3Service, useValue: mockS3Service }, + { provide: EmailService, useValue: mockEmailService }, ], }).compile() @@ -99,14 +117,14 @@ describe('CampaignApplicationService', () => { ) }) - it('should add a new campaign-application to db if all agreements are true', async () => { + it('should add a new campaign-application to db a if all agreements are true', async () => { const dto: CreateCampaignApplicationDto = { ...mockNewCampaignApplication, acceptTermsAndConditions: true, transparencyTermsAccepted: true, personalInformationProcessingAccepted: true, toEntity: new CreateCampaignApplicationDto().toEntity, - campaignEndDate: '2024-01-01' + campaignEndDate: '2024-01-01', } const mockOrganizerId = 'mockOrganizerId' @@ -119,6 +137,10 @@ describe('CampaignApplicationService', () => { .spyOn(prismaMock.campaignApplication, 'create') .mockResolvedValue(mockCreatedCampaignApplication) + const sendEmailsOnCreatedCampaignApplicationSpy = jest + .spyOn(service, 'sendEmailsOnCreatedCampaignApplication') + .mockResolvedValue(undefined) + const result = await service.create(dto, mockPerson) expect(result).toEqual(mockCreatedCampaignApplication) @@ -149,8 +171,63 @@ describe('CampaignApplicationService', () => { }, }) + expect(sendEmailsOnCreatedCampaignApplicationSpy).toHaveBeenCalledWith( + mockCreatedCampaignApplication.campaignName, + mockCreatedCampaignApplication.id, + mockPerson, + ) + expect(mockOrganizerService.create).toHaveBeenCalledTimes(1) expect(prismaMock.campaignApplication.create).toHaveBeenCalledTimes(1) + expect(sendEmailsOnCreatedCampaignApplicationSpy).toHaveBeenCalledTimes(1) + }) + }) + + describe('sendEmailsOnCreatedCampaignApplication', () => { + it('should send emails to both the organizer and the admin', async () => { + const mockAdminEmail = 'campaign_coordinators@podkrepi.bg' + const userEmail = { to: [mockPerson.email] } + const adminEmail = { to: [mockAdminEmail] } + + const emailAdminData = { + campaignApplicationName: mockSingleCampaignApplication.campaignName, + campaignApplicationLink: `${process.env.APP_URL}/admin/campaigns/${mockSingleCampaignApplication.id}`, + email: mockPerson.email as string, + firstName: mockPerson.firstName, + } + + const emailOrganizerData = { + campaignApplicationName: mockSingleCampaignApplication.campaignName, + campaignApplicationLink: `${process.env.APP_URL}/campaign/applications/${mockSingleCampaignApplication.id}`, + email: mockPerson.email as string, + firstName: mockPerson.firstName, + } + + const mailAdmin = new CreateCampaignApplicationAdminEmailDto(emailAdminData) + const mailOrganizer = new CreateCampaignApplicationOrganizerEmailDto(emailOrganizerData) + + mockEmailService.sendFromTemplate.mockResolvedValueOnce(undefined) + + await service.sendEmailsOnCreatedCampaignApplication( + mockSingleCampaignApplication.campaignName, + mockSingleCampaignApplication.id, + mockPerson, + ) + + expect(mockEmailService.sendFromTemplate).toHaveBeenNthCalledWith( + 1, + mailOrganizer, + userEmail, + { + bypassUnsubscribeManagement: { enable: true }, + }, + ) + + expect(mockEmailService.sendFromTemplate).toHaveBeenNthCalledWith(2, mailAdmin, adminEmail, { + bypassUnsubscribeManagement: { enable: true }, + }) + + expect(mockEmailService.sendFromTemplate).toHaveBeenCalledTimes(2) }) }) @@ -284,7 +361,7 @@ describe('CampaignApplicationService', () => { where: { id: '1' }, data: { ...mockUpdateCampaignApplication, - campaignEndDate: new Date('2024-09-09T00:00:00.000Z') + campaignEndDate: new Date('2024-09-09T00:00:00.000Z'), }, }) }) @@ -340,7 +417,7 @@ describe('CampaignApplicationService', () => { where: { id: '1' }, data: { ...mockUpdateCampaignApplication, - campaignEndDate: new Date('2024-09-09T00:00:00.000Z') + campaignEndDate: new Date('2024-09-09T00:00:00.000Z'), }, }) }) diff --git a/apps/api/src/campaign-application/campaign-application.service.ts b/apps/api/src/campaign-application/campaign-application.service.ts index d8b29d81c..b9fcda03d 100644 --- a/apps/api/src/campaign-application/campaign-application.service.ts +++ b/apps/api/src/campaign-application/campaign-application.service.ts @@ -12,8 +12,15 @@ import { OrganizerService } from '../organizer/organizer.service' import { CampaignApplicationFileRole, Person, Prisma } from '@prisma/client' import { S3Service } from './../s3/s3.service' import { CreateCampaignApplicationFileDto } from './dto/create-campaignApplication-file.dto' +import { EmailService } from '../email/email.service' +import { EmailData } from '../email/email.interface' +import { + CreateCampaignApplicationAdminEmailDto, + CreateCampaignApplicationOrganizerEmailDto, +} from '../email/template.interface' +import { ConfigService } from '@nestjs/config' -function dateMaybe (d?: string) { +function dateMaybe(d?: string) { return d != null && typeof d === 'string' && new Date(d).toString() != new Date('----invalid date ---').toString() @@ -28,6 +35,8 @@ export class CampaignApplicationService { private prisma: PrismaService, private organizerService: OrganizerService, private s3: S3Service, + private emailService: EmailService, + private readonly configService: ConfigService, ) {} async create(createCampaignApplicationDto: CreateCampaignApplicationDto, person: Person) { @@ -67,13 +76,19 @@ export class CampaignApplicationService { campaignTypeId: createCampaignApplicationDto.campaignTypeId, organizerId: organizer.id, campaignEnd: createCampaignApplicationDto.campaignEnd, - campaignEndDate: dateMaybe(createCampaignApplicationDto.campaignEndDate) + campaignEndDate: dateMaybe(createCampaignApplicationDto.campaignEndDate), } const newCampaignApplication = await this.prisma.campaignApplication.create({ data: campaingApplicationData, }) + await this.sendEmailsOnCreatedCampaignApplication( + newCampaignApplication.campaignName, + newCampaignApplication.id, + person, + ) + return newCampaignApplication } catch (error) { Logger.error('Error in create():', error) @@ -81,6 +96,53 @@ export class CampaignApplicationService { } } + async sendEmailsOnCreatedCampaignApplication( + campaignApplicationName: string, + campaignApplicationId: string, + person: Person, + ) { + const adminMail = this.configService.get('CAMPAIGN_COORDINATOR_EMAIL', '') + const userEmail = { to: [person.email] as EmailData[] } + const adminEmail = { to: [adminMail] as EmailData[] } + // const adminEmail = { to: ['martbul01@gmail.com'] as EmailData[] } + + const emailAdminData = { + campaignApplicationName, + campaignApplicationLink: `${this.configService.get( + 'APP_URL', + )}/admin/campaigns/${campaignApplicationId}`, + email: person.email as string, + firstName: person.firstName, + } + + const emailOrganizerData = { + campaignApplicationName, + campaignApplicationLink: `${this.configService.get( + 'APP_URL', + )}/campaign/applications/${campaignApplicationId}`, + email: person.email as string, + firstName: person.firstName, + } + + const mailAdmin = new CreateCampaignApplicationAdminEmailDto(emailAdminData) + const mailOrganizer = new CreateCampaignApplicationOrganizerEmailDto(emailOrganizerData) + + try { + const userEmailPromise = this.emailService.sendFromTemplate(mailOrganizer, userEmail, { + bypassUnsubscribeManagement: { enable: true }, + }) + + const adminEmailPromise = this.emailService.sendFromTemplate(mailAdmin, adminEmail, { + bypassUnsubscribeManagement: { enable: true }, + }) + + await Promise.allSettled([userEmailPromise, adminEmailPromise]) + } catch (error) { + Logger.error('Error in sendEmailsOnCreatedCampaignApplication():', error) + throw error + } + } + async uploadFiles(id: string, person: Person, files: Express.Multer.File[]) { try { const createdFiles = await Promise.all( @@ -111,14 +173,6 @@ export class CampaignApplicationService { try { const singleCampaignApplication = await this.prisma.campaignApplication.findUnique({ where: { id }, - include: { - documents: { - select: { - id: true, - filename: true, - }, - }, - }, }) if (!singleCampaignApplication) { throw new NotFoundException('Campaign application doesnt exist') diff --git a/apps/api/src/email/template.interface.ts b/apps/api/src/email/template.interface.ts index bcc0fd937..12059d83d 100644 --- a/apps/api/src/email/template.interface.ts +++ b/apps/api/src/email/template.interface.ts @@ -14,6 +14,8 @@ export enum TemplateType { confirmConsent = 'confirm-notifications-consent', campaignNewsDraft = 'campaign-news-draft', refundDonation = 'refund-donation', + createCampaignApplicationAdmin = 'create-campaign-application-admin', + createCampaignApplicationOrganizer = 'create-campaign-application-organizer', } export type TemplateTypeKeys = keyof typeof TemplateType export type TemplateTypeValues = typeof TemplateType[TemplateTypeKeys] @@ -100,3 +102,24 @@ export class RefundDonationEmailDto extends EmailTemplate<{ }> { name = TemplateType.refundDonation } + +export class CreateCampaignApplicationAdminEmailDto extends EmailTemplate<{ + campaignApplicationName: string + adminEditLink?: string + campaignApplicationLink: string + email: string + firstName: string +}> { + name = TemplateType.createCampaignApplicationAdmin +} + + +export class CreateCampaignApplicationOrganizerEmailDto extends EmailTemplate<{ + campaignApplicationName: string + editLink?: string + campaignApplicationLink: string + email: string + firstName: string +}> { + name = TemplateType.createCampaignApplicationOrganizer +} \ No newline at end of file