From 6bb688dee142caed0085ac15a9fc420bc159c594 Mon Sep 17 00:00:00 2001 From: Martbul Date: Wed, 14 Aug 2024 14:46:39 +0300 Subject: [PATCH 01/13] add findAll and findOne for admin only usage --- .../__mocks__/campaign-application-mocks.ts | 24 +++++++++++++++ .../campaign-application.controller.spec.ts | 25 +++++++++++++--- .../campaign-application.controller.ts | 5 +++- .../campaign-application.service.spec.ts | 29 +++++++++++++++++++ .../campaign-application.service.ts | 25 +++++++++++++--- 5 files changed, 99 insertions(+), 9 deletions(-) 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 85a419ef..686f4942 100644 --- a/apps/api/src/campaign-application/__mocks__/campaign-application-mocks.ts +++ b/apps/api/src/campaign-application/__mocks__/campaign-application-mocks.ts @@ -18,6 +18,30 @@ export const mockNewCampaignApplication = { category: CampaignTypeCategory.medical, } +export const mockSingleCampaignApplication = { + id: '1', + createdAt: new Date('2022-04-08T06:36:33.661Z'), + updatedAt: new Date('2022-04-08T06:36:33.662Z'), + description: 'Test description1', + organizerId: 'testOrganizerId1', + organizerName: 'Test Organizer1', + organizerEmail: 'organizer1@example.com', + beneficiary: 'test beneficary1', + organizerPhone: '123456789', + organizerBeneficiaryRel: 'Test Relation1', + campaignName: 'Test Campaign1', + goal: 'Test Goal1', + history: 'test history1', + amount: '1000', + campaignGuarantee: 'test campaignGuarantee1', + otherFinanceSources: 'test otherFinanceSources1', + otherNotes: 'test otherNotes1', + state: CampaignApplicationState.review, + category: CampaignTypeCategory.medical, + ticketURL: 'testsodifhso1', + archived: false, +} + export const mockCampaigns = [ { id: '1', diff --git a/apps/api/src/campaign-application/campaign-application.controller.spec.ts b/apps/api/src/campaign-application/campaign-application.controller.spec.ts index a0179f0e..12c78524 100644 --- a/apps/api/src/campaign-application/campaign-application.controller.spec.ts +++ b/apps/api/src/campaign-application/campaign-application.controller.spec.ts @@ -123,11 +123,28 @@ describe('CampaignApplicationController', () => { expect(service.findAll).toHaveBeenCalled() }) - it('when findOne called it should delegate to the service findOne', () => { - // Act - controller.findOne('id') + it('when findOne called by a non-admin user it should throw a ForbiddenException', () => { + jest.mock('../auth/keycloak', () => ({ + isAdmin: jest.fn().mockReturnValue(false), + })) - // Assert + // Arrange + const user = { sub: 'non-admin', 'allowed-origins': ['test'] } as KeycloakTokenParsed + + // Act & Assert + expect(() => controller.findOne('id', user)).toThrow(ForbiddenException) + }) + + it('when findOne called by an admin user it should delegate to the service findOne', () => { + jest.mock('../auth/keycloak', () => ({ + isAdmin: jest.fn().mockImplementation((user: KeycloakTokenParsed) => { + return user.resource_access?.account?.roles.includes('account-view-supporters') + }), + })) + + // Act & Assert + expect(() => controller.findOne('id', mockUserAdmin)).not.toThrow(ForbiddenException) + controller.findOne('id', mockUserAdmin) expect(service.findOne).toHaveBeenCalledWith('id') }) diff --git a/apps/api/src/campaign-application/campaign-application.controller.ts b/apps/api/src/campaign-application/campaign-application.controller.ts index d11333e8..d9905c2f 100644 --- a/apps/api/src/campaign-application/campaign-application.controller.ts +++ b/apps/api/src/campaign-application/campaign-application.controller.ts @@ -76,7 +76,10 @@ export class CampaignApplicationController { } @Get('byId/:id') - findOne(@Param('id') id: string) { + findOne(@Param('id') id: string, @AuthenticatedUser() user: KeycloakTokenParsed) { + if (!isAdmin(user)) { + throw new ForbiddenException('Must be admin to get a single campaign-application') + } return this.campaignApplicationService.findOne(id) } 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 5bbca2f7..f4e5048a 100644 --- a/apps/api/src/campaign-application/campaign-application.service.spec.ts +++ b/apps/api/src/campaign-application/campaign-application.service.spec.ts @@ -10,6 +10,7 @@ import { mockCampaigns, mockCreatedCampaignApplication, mockNewCampaignApplication, + mockSingleCampaignApplication, mockUpdateCampaignApplication, } from './__mocks__/campaign-application-mocks' import { S3Service } from '../s3/s3.service' @@ -202,6 +203,34 @@ describe('CampaignApplicationService', () => { }) }) + describe('findOne', () => { + it('should return a single campaign-application', async () => { + prismaMock.campaignApplication.findUnique.mockResolvedValue(mockSingleCampaignApplication) + + const result = await service.findOne('id') + + expect(result).toEqual(mockSingleCampaignApplication) + expect(prismaMock.campaignApplication.findUnique).toHaveBeenCalledTimes(1) + }) + + it('should throw a NotFoundException if no campaign-application is found', async () => { + prismaMock.campaignApplication.findUnique.mockResolvedValue(null) + + await expect(service.findOne('id')).rejects.toThrow( + new NotFoundException('Campaign application doesnt exist'), + ) + expect(prismaMock.campaignApplication.findUnique).toHaveBeenCalledTimes(1) + }) + + it('should handle errors and throw an exception', async () => { + const errorMessage = 'error' + prismaMock.campaignApplication.findUnique.mockRejectedValue(new Error(errorMessage)) + + await expect(service.findOne('id')).rejects.toThrow(errorMessage) + expect(prismaMock.campaignApplication.findUnique).toHaveBeenCalledTimes(1) + }) + }) + describe('updateCampaignApplication', () => { it('should update a campaign application if the user is the organizer', async () => { const mockCampaignApplication = { diff --git a/apps/api/src/campaign-application/campaign-application.service.ts b/apps/api/src/campaign-application/campaign-application.service.ts index 0024aac0..0d475d4e 100644 --- a/apps/api/src/campaign-application/campaign-application.service.ts +++ b/apps/api/src/campaign-application/campaign-application.service.ts @@ -86,12 +86,29 @@ export class CampaignApplicationService { } } - findAll() { - return this.prisma.campaignApplication.findMany() + async findAll() { + try { + const campaignApplications = await this.prisma.campaignApplication.findMany() + return campaignApplications + } catch (error) { + Logger.error('Error in findAll():', error) + throw error + } } - findOne(id: string) { - return `This action returns a #${id} campaignApplication` + async findOne(id: string) { + try { + const singleCampaignApplication = await this.prisma.campaignApplication.findUnique({ + where: { id }, + }) + if (!singleCampaignApplication) { + throw new NotFoundException('Campaign application doesnt exist') + } + return singleCampaignApplication + } catch (error) { + Logger.error('Error in findOne():', error) + throw error + } } async updateCampaignApplication( From 91a822209b81b1fad25e2197dd737d3b79e8f494 Mon Sep 17 00:00:00 2001 From: Martbul Date: Wed, 14 Aug 2024 18:54:28 +0300 Subject: [PATCH 02/13] add delete file endpoint, findOne now accessible by organizer and fix fileUpload --- .../__mocks__/campaign-application-mocks.ts | 3 +- .../campaign-application.controller.spec.ts | 54 +++++++------- .../campaign-application.controller.ts | 32 ++++++++- .../campaign-application.service.spec.ts | 36 +++++++++- .../campaign-application.service.ts | 72 +++++++++++++++---- 5 files changed, 145 insertions(+), 52 deletions(-) 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 686f4942..c788a891 100644 --- a/apps/api/src/campaign-application/__mocks__/campaign-application-mocks.ts +++ b/apps/api/src/campaign-application/__mocks__/campaign-application-mocks.ts @@ -23,7 +23,7 @@ export const mockSingleCampaignApplication = { createdAt: new Date('2022-04-08T06:36:33.661Z'), updatedAt: new Date('2022-04-08T06:36:33.662Z'), description: 'Test description1', - organizerId: 'testOrganizerId1', + organizerId: 'ffdbcc41-85ec-476c-9e59-0662f3b433af', organizerName: 'Test Organizer1', organizerEmail: 'organizer1@example.com', beneficiary: 'test beneficary1', @@ -40,6 +40,7 @@ export const mockSingleCampaignApplication = { category: CampaignTypeCategory.medical, ticketURL: 'testsodifhso1', archived: false, + documents: [{ id: 'fileId' }], } export const mockCampaigns = [ diff --git a/apps/api/src/campaign-application/campaign-application.controller.spec.ts b/apps/api/src/campaign-application/campaign-application.controller.spec.ts index 12c78524..23381046 100644 --- a/apps/api/src/campaign-application/campaign-application.controller.spec.ts +++ b/apps/api/src/campaign-application/campaign-application.controller.spec.ts @@ -18,6 +18,7 @@ describe('CampaignApplicationController', () => { let controller: CampaignApplicationController let service: CampaignApplicationService let personService: PersonService + const { isAdmin } = require('../auth/keycloak') const mockPerson = { ...personMock, @@ -26,6 +27,10 @@ describe('CampaignApplicationController', () => { organizer: { id: 'personOrganaizerId' }, } + jest.mock('../auth/keycloak', () => ({ + isAdmin: jest.fn(), + })) + const mockCreateNewCampaignApplication = { ...mockNewCampaignApplication, acceptTermsAndConditions: true, @@ -110,42 +115,31 @@ describe('CampaignApplicationController', () => { // Act & Assert expect(() => controller.findAll(user)).toThrow(ForbiddenException) }) - it('when findAll called by an admin user it should delegate to the service findAll', () => { - jest.mock('../auth/keycloak', () => ({ - isAdmin: jest.fn().mockImplementation((user: KeycloakTokenParsed) => { - return user.resource_access?.account?.roles.includes('account-view-supporters') - }), - })) - - // Act & Assert - expect(() => controller.findAll(mockUserAdmin)).not.toThrow(ForbiddenException) - controller.findAll(mockUserAdmin) - expect(service.findAll).toHaveBeenCalled() - }) - - it('when findOne called by a non-admin user it should throw a ForbiddenException', () => { - jest.mock('../auth/keycloak', () => ({ - isAdmin: jest.fn().mockReturnValue(false), - })) + it('when findOne is called by an organizer, it should delegate to the service findOne', async () => { // Arrange - const user = { sub: 'non-admin', 'allowed-origins': ['test'] } as KeycloakTokenParsed + jest.spyOn(personService, 'findOneByKeycloakId').mockResolvedValue(mockUser) + isAdmin.mockReturnValue(false) // Non-admin user - // Act & Assert - expect(() => controller.findOne('id', user)).toThrow(ForbiddenException) + // Act + await controller.findOne('id', mockUser) + + // Assert + expect(personService.findOneByKeycloakId).toHaveBeenCalledWith(mockUser.sub) + expect(service.findOne).toHaveBeenCalledWith('id', false, mockUser) }) - it('when findOne called by an admin user it should delegate to the service findOne', () => { - jest.mock('../auth/keycloak', () => ({ - isAdmin: jest.fn().mockImplementation((user: KeycloakTokenParsed) => { - return user.resource_access?.account?.roles.includes('account-view-supporters') - }), - })) + it('when findOne is called by an admin user, it should delegate to the service with isAdmin true', async () => { + // Arrange + jest.spyOn(personService, 'findOneByKeycloakId').mockResolvedValue(mockUserAdmin) + isAdmin.mockReturnValue(true) // Admin user - // Act & Assert - expect(() => controller.findOne('id', mockUserAdmin)).not.toThrow(ForbiddenException) - controller.findOne('id', mockUserAdmin) - expect(service.findOne).toHaveBeenCalledWith('id') + // Act + await controller.findOne('id', mockUserAdmin) + + // Assert + expect(personService.findOneByKeycloakId).toHaveBeenCalledWith(mockUserAdmin.sub) + expect(service.findOne).toHaveBeenCalledWith('id', true, mockUserAdmin) }) it('when update called by an user it should delegate to the service update', async () => { diff --git a/apps/api/src/campaign-application/campaign-application.controller.ts b/apps/api/src/campaign-application/campaign-application.controller.ts index d9905c2f..7d4b8bb7 100644 --- a/apps/api/src/campaign-application/campaign-application.controller.ts +++ b/apps/api/src/campaign-application/campaign-application.controller.ts @@ -10,6 +10,7 @@ import { Logger, UploadedFiles, UseInterceptors, + Delete, } from '@nestjs/common' import { CampaignApplicationService } from './campaign-application.service' import { CreateCampaignApplicationDto } from './dto/create-campaign-application.dto' @@ -76,11 +77,36 @@ export class CampaignApplicationController { } @Get('byId/:id') - findOne(@Param('id') id: string, @AuthenticatedUser() user: KeycloakTokenParsed) { + async findOne(@Param('id') id: string, @AuthenticatedUser() user: KeycloakTokenParsed) { + const person = await this.personService.findOneByKeycloakId(user.sub) + if (!person) { + Logger.error('No person found in database') + throw new NotFoundException('No person found in database') + } + let isAdminFlag if (!isAdmin(user)) { - throw new ForbiddenException('Must be admin to get a single campaign-application') + isAdminFlag = false + } else { + isAdminFlag = true } - return this.campaignApplicationService.findOne(id) + return this.campaignApplicationService.findOne(id, isAdminFlag, person) + } + + @Delete('fileById/:id') + async deleteFile(@Param('id') id: string, @AuthenticatedUser() user: KeycloakTokenParsed) { + const person = await this.personService.findOneByKeycloakId(user.sub) + if (!person) { + Logger.error('No person found in database') + throw new NotFoundException('No person found in database') + } + let isAdminFlag + if (!isAdmin(user)) { + isAdminFlag = false + } else { + isAdminFlag = true + } + + return this.campaignApplicationService.deleteFile(id, isAdminFlag, person) } @Patch(':id') 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 f4e5048a..eadf7e94 100644 --- a/apps/api/src/campaign-application/campaign-application.service.spec.ts +++ b/apps/api/src/campaign-application/campaign-application.service.spec.ts @@ -39,6 +39,7 @@ describe('CampaignApplicationService', () => { const mockS3Service = { uploadObject: jest.fn(), + deleteObject: jest.fn(), } beforeEach(async () => { @@ -207,7 +208,7 @@ describe('CampaignApplicationService', () => { it('should return a single campaign-application', async () => { prismaMock.campaignApplication.findUnique.mockResolvedValue(mockSingleCampaignApplication) - const result = await service.findOne('id') + const result = await service.findOne('id', false, mockPerson) expect(result).toEqual(mockSingleCampaignApplication) expect(prismaMock.campaignApplication.findUnique).toHaveBeenCalledTimes(1) @@ -216,7 +217,7 @@ describe('CampaignApplicationService', () => { it('should throw a NotFoundException if no campaign-application is found', async () => { prismaMock.campaignApplication.findUnique.mockResolvedValue(null) - await expect(service.findOne('id')).rejects.toThrow( + await expect(service.findOne('id', false, mockPerson)).rejects.toThrow( new NotFoundException('Campaign application doesnt exist'), ) expect(prismaMock.campaignApplication.findUnique).toHaveBeenCalledTimes(1) @@ -226,11 +227,40 @@ describe('CampaignApplicationService', () => { const errorMessage = 'error' prismaMock.campaignApplication.findUnique.mockRejectedValue(new Error(errorMessage)) - await expect(service.findOne('id')).rejects.toThrow(errorMessage) + await expect(service.findOne('id', false, mockPerson)).rejects.toThrow(errorMessage) expect(prismaMock.campaignApplication.findUnique).toHaveBeenCalledTimes(1) }) }) + describe('deleteFile', () => { + it('should return a message on successful deletion', async () => { + prismaMock.campaignApplication.findFirst.mockResolvedValue(mockSingleCampaignApplication) + + const result = await service.deleteFile('fileId', false, mockPerson) + + expect(result).toEqual('Successfully deleted file') + expect(prismaMock.campaignApplication.findFirst).toHaveBeenCalledTimes(1) + }) + + it('should throw a NotFoundException if no campaign-application is found', async () => { + prismaMock.campaignApplication.findUnique.mockResolvedValue(null) + + await expect(service.deleteFile('fileId', false, mockPerson)).rejects.toThrow( + new NotFoundException('File does not exist'), + ) + expect(prismaMock.campaignApplication.findFirst).toHaveBeenCalledTimes(1) + }) + + it('should handle errors and throw an exception', async () => { + const errorMessage = 'error' + prismaMock.campaignApplication.findFirst.mockRejectedValue(new Error(errorMessage)) + + await expect(service.deleteFile('fileId', false, mockPerson)).rejects.toThrow(errorMessage) + expect(prismaMock.campaignApplication.findFirst).toHaveBeenCalledTimes(1) + expect(prismaMock.campaignApplicationFile.delete).not.toHaveBeenCalled() + }) + }) + describe('updateCampaignApplication', () => { it('should update a campaign application if the user is the organizer', async () => { const mockCampaignApplication = { diff --git a/apps/api/src/campaign-application/campaign-application.service.ts b/apps/api/src/campaign-application/campaign-application.service.ts index 0d475d4e..813d2eb1 100644 --- a/apps/api/src/campaign-application/campaign-application.service.ts +++ b/apps/api/src/campaign-application/campaign-application.service.ts @@ -96,7 +96,7 @@ export class CampaignApplicationService { } } - async findOne(id: string) { + async findOne(id: string, isAdminFlag: boolean, person: Person) { try { const singleCampaignApplication = await this.prisma.campaignApplication.findUnique({ where: { id }, @@ -104,6 +104,11 @@ export class CampaignApplicationService { if (!singleCampaignApplication) { throw new NotFoundException('Campaign application doesnt exist') } + + if (isAdminFlag === false && singleCampaignApplication.organizerId !== person.organizer.id) { + throw new ForbiddenException('User is not admin or organizer of the campaignApplication') + } + return singleCampaignApplication } catch (error) { Logger.error('Error in findOne():', error) @@ -111,6 +116,38 @@ export class CampaignApplicationService { } } + async deleteFile(id: string, isAdminFlag: boolean, person: Person) { + try { + const campaignApplication = await this.prisma.campaignApplication.findFirst({ + where: { + documents: { + some: { + id: id, + }, + }, + }, + }) + + if (!campaignApplication) { + throw new NotFoundException('File does not exist') + } + + if (isAdminFlag === false && campaignApplication.organizerId !== person.organizer.id) { + throw new ForbiddenException('User is not admin or organizer of the campaignApplication') + } + + await this.prisma.campaignApplicationFile.delete({ + where: { id }, + }) + + await this.s3.deleteObject(this.bucketName, id) + } catch (error) { + Logger.error('Error in deleteFile():', error) + throw error + } + return 'Successfully deleted file' + } + async updateCampaignApplication( id: string, updateCampaignApplicationDto: UpdateCampaignApplicationDto, @@ -199,20 +236,25 @@ export class CampaignApplicationService { role: CampaignApplicationFileRole.document, } - const createFileInDb = await this.prisma.campaignApplicationFile.create({ - data: fileDto, - }) + try { + const createFileInDb = await this.prisma.campaignApplicationFile.create({ + data: fileDto, + }) - await this.s3.uploadObject( - this.bucketName, - createFileInDb.id, - file.originalname, - file.mimetype, - file.buffer, - 'CampaignApplicationFile', - campaignApplicationId, - personId, - ) - return createFileInDb + await this.s3.uploadObject( + this.bucketName, + createFileInDb.id, + file.originalname, + file.mimetype, + file.buffer, + 'CampaignApplicationFile', + campaignApplicationId, + personId, + ) + return createFileInDb + } catch (error) { + Logger.error('Error in campaignApplicationFilesCreate():', error) + throw error + } } } From a7f135a8bb75a47c12b69d678295dc1cc3a0e09d Mon Sep 17 00:00:00 2001 From: Martbul Date: Sun, 18 Aug 2024 11:45:21 +0300 Subject: [PATCH 03/13] re-write isAdminFlag --- .../campaign-application.controller.ts | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/apps/api/src/campaign-application/campaign-application.controller.ts b/apps/api/src/campaign-application/campaign-application.controller.ts index 7d4b8bb7..b3beb2ac 100644 --- a/apps/api/src/campaign-application/campaign-application.controller.ts +++ b/apps/api/src/campaign-application/campaign-application.controller.ts @@ -83,12 +83,9 @@ export class CampaignApplicationController { Logger.error('No person found in database') throw new NotFoundException('No person found in database') } - let isAdminFlag - if (!isAdmin(user)) { - isAdminFlag = false - } else { - isAdminFlag = true - } + + const isAdminFlag = isAdmin(user) + return this.campaignApplicationService.findOne(id, isAdminFlag, person) } @@ -99,13 +96,9 @@ export class CampaignApplicationController { Logger.error('No person found in database') throw new NotFoundException('No person found in database') } - let isAdminFlag - if (!isAdmin(user)) { - isAdminFlag = false - } else { - isAdminFlag = true - } + const isAdminFlag = isAdmin(user) + return this.campaignApplicationService.deleteFile(id, isAdminFlag, person) } From 64debd6d61023b332f1964f8d3a450d0f8b6994c Mon Sep 17 00:00:00 2001 From: Martbul Date: Mon, 26 Aug 2024 17:55:09 +0300 Subject: [PATCH 04/13] add send email functionality when new campaign application is added the organizer will receive an email --- .../create-campaign-application.json | 3 + .../create-campaign-application.mjml | 63 +++++++++++++++++++ .../campaign-application.module.ts | 4 +- .../campaign-application.service.ts | 17 +++++ apps/api/src/email/template.interface.ts | 8 +++ 5 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 apps/api/src/assets/templates/create-campaign-application.json create mode 100644 apps/api/src/assets/templates/create-campaign-application.mjml diff --git a/apps/api/src/assets/templates/create-campaign-application.json b/apps/api/src/assets/templates/create-campaign-application.json new file mode 100644 index 00000000..45cb0fef --- /dev/null +++ b/apps/api/src/assets/templates/create-campaign-application.json @@ -0,0 +1,3 @@ +{ + "subject": "Създадена нова апликация за кампания" +} \ No newline at end of file diff --git a/apps/api/src/assets/templates/create-campaign-application.mjml b/apps/api/src/assets/templates/create-campaign-application.mjml new file mode 100644 index 00000000..a95526d8 --- /dev/null +++ b/apps/api/src/assets/templates/create-campaign-application.mjml @@ -0,0 +1,63 @@ + + + + + + Успешно създадохте кампания в Подкрепи.бг + + + + + + + Здравейте {{person.firstName}}, +

+
+ + Благодарим, че проявихте интерес и желание да се включите с помощ в проекта!

+ Предлагаме да продължим комуникацията в + Discord сървъра, където се помещава нашият виртуален офис. Съветът ни е да прочетете внимателно какво + пише на началната страница в канал #започни-от-тук.

+ При желания за промяна на детайли по кампанията посетете ЛИНК.

+ Още веднъж благодарим за готовността да помогнете за нашата кауза! +
+ + Поздрави,
+ Екипът на Подкрепи.бг +
+
+
+
+
\ No newline at end of file diff --git a/apps/api/src/campaign-application/campaign-application.module.ts b/apps/api/src/campaign-application/campaign-application.module.ts index 5b0047d2..37c3a96e 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.ts b/apps/api/src/campaign-application/campaign-application.service.ts index 813d2eb1..11a23db9 100644 --- a/apps/api/src/campaign-application/campaign-application.service.ts +++ b/apps/api/src/campaign-application/campaign-application.service.ts @@ -12,6 +12,9 @@ import { OrganizerService } from '../organizer/organizer.service' import { CampaignApplicationFileRole, Person } 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 { CreateCampaignApplicationEmailDto } from '../email/template.interface' @Injectable() export class CampaignApplicationService { private readonly bucketName: string = 'campaignapplication-files' @@ -19,6 +22,7 @@ export class CampaignApplicationService { private prisma: PrismaService, private organizerService: OrganizerService, private s3: S3Service, + private sendEmail: EmailService, ) {} async getCampaignByIdWithPersonIds(id: string): Promise { @@ -67,6 +71,19 @@ export class CampaignApplicationService { data: campaingApplicationData, }) + const userEmail = { to: [person.email] as EmailData[] } + + const emailData = { + campaignApplicationName: newCampaignApplication.campaignName, + editLink: 'https://www.formula1.com/', + } + + const mail = new CreateCampaignApplicationEmailDto(emailData) + + await this.sendEmail.sendFromTemplate(mail, userEmail, { + bypassUnsubscribeManagement: { enable: true }, + }) + return newCampaignApplication } catch (error) { Logger.error('Error in create():', error) diff --git a/apps/api/src/email/template.interface.ts b/apps/api/src/email/template.interface.ts index bcc0fd93..f1c37942 100644 --- a/apps/api/src/email/template.interface.ts +++ b/apps/api/src/email/template.interface.ts @@ -14,6 +14,7 @@ export enum TemplateType { confirmConsent = 'confirm-notifications-consent', campaignNewsDraft = 'campaign-news-draft', refundDonation = 'refund-donation', + createCampaignApplication = 'create-campaign-application', } export type TemplateTypeKeys = keyof typeof TemplateType export type TemplateTypeValues = typeof TemplateType[TemplateTypeKeys] @@ -100,3 +101,10 @@ export class RefundDonationEmailDto extends EmailTemplate<{ }> { name = TemplateType.refundDonation } + +export class CreateCampaignApplicationEmailDto extends EmailTemplate<{ + campaignApplicationName: string + editLink: string +}> { + name = TemplateType.createCampaignApplication +} \ No newline at end of file From a45c5874750ee2b37d7332c7653112d678042249 Mon Sep 17 00:00:00 2001 From: Martbul Date: Mon, 26 Aug 2024 18:13:25 +0300 Subject: [PATCH 05/13] fix isAdmin import --- .../campaign-application.controller.spec.ts | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/apps/api/src/campaign-application/campaign-application.controller.spec.ts b/apps/api/src/campaign-application/campaign-application.controller.spec.ts index 23381046..9effa150 100644 --- a/apps/api/src/campaign-application/campaign-application.controller.spec.ts +++ b/apps/api/src/campaign-application/campaign-application.controller.spec.ts @@ -18,7 +18,6 @@ describe('CampaignApplicationController', () => { let controller: CampaignApplicationController let service: CampaignApplicationService let personService: PersonService - const { isAdmin } = require('../auth/keycloak') const mockPerson = { ...personMock, @@ -27,10 +26,6 @@ describe('CampaignApplicationController', () => { organizer: { id: 'personOrganaizerId' }, } - jest.mock('../auth/keycloak', () => ({ - isAdmin: jest.fn(), - })) - const mockCreateNewCampaignApplication = { ...mockNewCampaignApplication, acceptTermsAndConditions: true, @@ -119,7 +114,10 @@ describe('CampaignApplicationController', () => { it('when findOne is called by an organizer, it should delegate to the service findOne', async () => { // Arrange jest.spyOn(personService, 'findOneByKeycloakId').mockResolvedValue(mockUser) - isAdmin.mockReturnValue(false) // Non-admin user + + jest.mock('../auth/keycloak', () => ({ + isAdmin: jest.fn().mockReturnValue(false), + })) // Act await controller.findOne('id', mockUser) @@ -132,7 +130,9 @@ describe('CampaignApplicationController', () => { it('when findOne is called by an admin user, it should delegate to the service with isAdmin true', async () => { // Arrange jest.spyOn(personService, 'findOneByKeycloakId').mockResolvedValue(mockUserAdmin) - isAdmin.mockReturnValue(true) // Admin user + jest.mock('../auth/keycloak', () => ({ + isAdmin: jest.fn().mockReturnValue(true), + })) // Act await controller.findOne('id', mockUserAdmin) From 93447ed7d90e9ec7c4fe83156ca600f6d54c15c2 Mon Sep 17 00:00:00 2001 From: Martbul Date: Wed, 28 Aug 2024 10:36:18 +0300 Subject: [PATCH 06/13] edit email --- .../templates/create-campaign-application.mjml | 2 +- .../campaign-application.controller.spec.ts | 14 +++++++------- .../campaign-application.service.ts | 4 ++++ 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/apps/api/src/assets/templates/create-campaign-application.mjml b/apps/api/src/assets/templates/create-campaign-application.mjml index a95526d8..1036c71c 100644 --- a/apps/api/src/assets/templates/create-campaign-application.mjml +++ b/apps/api/src/assets/templates/create-campaign-application.mjml @@ -28,7 +28,7 @@ font-family="open Sans Helvetica, Arial, sans-serif" padding-left="25px" padding-right="25px"> - Здравейте {{person.firstName}}, + Здравейте {{firstName}},

{ let controller: CampaignApplicationController let service: CampaignApplicationService let personService: PersonService - const { isAdmin } = require('../auth/keycloak') const mockPerson = { ...personMock, @@ -27,10 +26,6 @@ describe('CampaignApplicationController', () => { organizer: { id: 'personOrganaizerId' }, } - jest.mock('../auth/keycloak', () => ({ - isAdmin: jest.fn(), - })) - const mockCreateNewCampaignApplication = { ...mockNewCampaignApplication, acceptTermsAndConditions: true, @@ -119,7 +114,10 @@ describe('CampaignApplicationController', () => { it('when findOne is called by an organizer, it should delegate to the service findOne', async () => { // Arrange jest.spyOn(personService, 'findOneByKeycloakId').mockResolvedValue(mockUser) - isAdmin.mockReturnValue(false) // Non-admin user + + jest.mock('../auth/keycloak', () => ({ + isAdmin: jest.fn().mockReturnValue(false), + })) // Act await controller.findOne('id', mockUser) @@ -132,7 +130,9 @@ describe('CampaignApplicationController', () => { it('when findOne is called by an admin user, it should delegate to the service with isAdmin true', async () => { // Arrange jest.spyOn(personService, 'findOneByKeycloakId').mockResolvedValue(mockUserAdmin) - isAdmin.mockReturnValue(true) // Admin user + jest.mock('../auth/keycloak', () => ({ + isAdmin: jest.fn().mockReturnValue(true), + })) // Act await controller.findOne('id', mockUserAdmin) diff --git a/apps/api/src/campaign-application/campaign-application.service.ts b/apps/api/src/campaign-application/campaign-application.service.ts index 11a23db9..b07b1090 100644 --- a/apps/api/src/campaign-application/campaign-application.service.ts +++ b/apps/api/src/campaign-application/campaign-application.service.ts @@ -15,6 +15,7 @@ import { CreateCampaignApplicationFileDto } from './dto/create-campaignApplicati import { EmailService } from '../email/email.service' import { EmailData } from '../email/email.interface' import { CreateCampaignApplicationEmailDto } from '../email/template.interface' + @Injectable() export class CampaignApplicationService { private readonly bucketName: string = 'campaignapplication-files' @@ -76,6 +77,9 @@ export class CampaignApplicationService { const emailData = { campaignApplicationName: newCampaignApplication.campaignName, editLink: 'https://www.formula1.com/', + email: person.email, + firstName: person.firstName, + } const mail = new CreateCampaignApplicationEmailDto(emailData) From 87f5e41c9f391ec226d836af0962cbeec552fb87 Mon Sep 17 00:00:00 2001 From: Martbul Date: Wed, 28 Aug 2024 10:39:03 +0300 Subject: [PATCH 07/13] fix person type in campaign application --- .../campaign-application.service.ts | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/apps/api/src/campaign-application/campaign-application.service.ts b/apps/api/src/campaign-application/campaign-application.service.ts index 813d2eb1..ae0fff51 100644 --- a/apps/api/src/campaign-application/campaign-application.service.ts +++ b/apps/api/src/campaign-application/campaign-application.service.ts @@ -9,7 +9,7 @@ import { CreateCampaignApplicationDto } from './dto/create-campaign-application. import { UpdateCampaignApplicationDto } from './dto/update-campaign-application.dto' import { PrismaService } from '../prisma/prisma.service' import { OrganizerService } from '../organizer/organizer.service' -import { CampaignApplicationFileRole, Person } from '@prisma/client' +import { CampaignApplicationFileRole, Person, Prisma } from '@prisma/client' import { S3Service } from './../s3/s3.service' import { CreateCampaignApplicationFileDto } from './dto/create-campaignApplication-file.dto' @Injectable() @@ -96,7 +96,7 @@ export class CampaignApplicationService { } } - async findOne(id: string, isAdminFlag: boolean, person: Person) { + async findOne(id: string, isAdminFlag: boolean, person: Prisma.PersonGetPayload<{ include: { organizer: {select:{id:true}}}}>) { try { const singleCampaignApplication = await this.prisma.campaignApplication.findUnique({ where: { id }, @@ -105,7 +105,7 @@ export class CampaignApplicationService { throw new NotFoundException('Campaign application doesnt exist') } - if (isAdminFlag === false && singleCampaignApplication.organizerId !== person.organizer.id) { + if (isAdminFlag === false && singleCampaignApplication.organizerId !== person.organizer?.id) { throw new ForbiddenException('User is not admin or organizer of the campaignApplication') } @@ -116,7 +116,7 @@ export class CampaignApplicationService { } } - async deleteFile(id: string, isAdminFlag: boolean, person: Person) { + async deleteFile(id: string, isAdminFlag: boolean, person: Prisma.PersonGetPayload<{ include: { organizer: {select:{id:true}}}}>) { try { const campaignApplication = await this.prisma.campaignApplication.findFirst({ where: { @@ -132,7 +132,7 @@ export class CampaignApplicationService { throw new NotFoundException('File does not exist') } - if (isAdminFlag === false && campaignApplication.organizerId !== person.organizer.id) { + if (isAdminFlag === false && campaignApplication.organizerId !== person.organizer?.id) { throw new ForbiddenException('User is not admin or organizer of the campaignApplication') } From bd2355f872c00320c777773e321a464a29f31005 Mon Sep 17 00:00:00 2001 From: Martbul Date: Thu, 29 Aug 2024 12:05:49 +0300 Subject: [PATCH 08/13] add email template for admin for created campaign-application --- ...=> create-campaign-application-admin.json} | 2 +- .../create-campaign-application-admin.mjml | 62 +++++++++++++++++++ ...create-campaign-application-organizer.json | 3 + ...reate-campaign-application-organizer.mjml} | 17 ++--- .../campaign-application.service.ts | 34 +++++++--- apps/api/src/email/template.interface.ts | 21 ++++++- 6 files changed, 118 insertions(+), 21 deletions(-) rename apps/api/src/assets/templates/{create-campaign-application.json => create-campaign-application-admin.json} (67%) 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 rename apps/api/src/assets/templates/{create-campaign-application.mjml => create-campaign-application-organizer.mjml} (65%) diff --git a/apps/api/src/assets/templates/create-campaign-application.json b/apps/api/src/assets/templates/create-campaign-application-admin.json similarity index 67% rename from apps/api/src/assets/templates/create-campaign-application.json rename to apps/api/src/assets/templates/create-campaign-application-admin.json index 45cb0fef..8ff9b2e7 100644 --- a/apps/api/src/assets/templates/create-campaign-application.json +++ b/apps/api/src/assets/templates/create-campaign-application-admin.json @@ -1,3 +1,3 @@ { - "subject": "Създадена нова апликация за кампания" + "subject": "Създадена нова апликация за кампания(админ)" } \ No newline at end of file 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 00000000..89cc131a --- /dev/null +++ b/apps/api/src/assets/templates/create-campaign-application-admin.mjml @@ -0,0 +1,62 @@ + + + + + + Успешно създадена кампания от {{firstName}} + + + + + + + Здравейте {{firstName}}, +

+
+ + Организатор: {{firstName}} с имейл: {{email}} създаде нова кампания ТУК!

+ За промяна на детайли по кампанията посетете + Редактиране на кампания

+
+ + Поздрави,
+ Екипът на Подкрепи.бг +
+
+
+
+
\ No newline at end of file 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 00000000..3e514294 --- /dev/null +++ b/apps/api/src/assets/templates/create-campaign-application-organizer.json @@ -0,0 +1,3 @@ +{ + "subject": "Създадена нова апликация за кампания(организатор)" +} \ No newline at end of file diff --git a/apps/api/src/assets/templates/create-campaign-application.mjml b/apps/api/src/assets/templates/create-campaign-application-organizer.mjml similarity index 65% rename from apps/api/src/assets/templates/create-campaign-application.mjml rename to apps/api/src/assets/templates/create-campaign-application-organizer.mjml index 1036c71c..cd6c634f 100644 --- a/apps/api/src/assets/templates/create-campaign-application.mjml +++ b/apps/api/src/assets/templates/create-campaign-application-organizer.mjml @@ -38,14 +38,15 @@ font-family="open Sans Helvetica, Arial, sans-serif" padding-left="25px" padding-right="25px"> - Благодарим, че проявихте интерес и желание да се включите с помощ в проекта!

- Предлагаме да продължим комуникацията в - Discord сървъра, където се помещава нашият виртуален офис. Съветът ни е да прочетете внимателно какво - пише на началната страница в канал #започни-от-тук.

- При желания за промяна на детайли по кампанията посетете ЛИНК.

- Още веднъж благодарим за готовността да помогнете за нашата кауза! + Вашата кампания бе създадена успешно, може да я намерите ТУК!

+ За промяна на детайли по кампанията посетете + Редактиране на кампания

+ + Пожелаваме успешно набиране на средства!
{ + name = TemplateType.createCampaignApplicationAdmin +} + + +export class CreateCampaignApplicationOrganizerEmailDto extends EmailTemplate<{ campaignApplicationName: string editLink: string + campaignApplicationLink:string + email: string + firstName: string }> { - name = TemplateType.createCampaignApplication + name = TemplateType.createCampaignApplicationOrganizer } \ No newline at end of file From 482f556cd1e27307a5b59f8a30c787973285a2f5 Mon Sep 17 00:00:00 2001 From: Martbul Date: Mon, 2 Sep 2024 23:47:51 +0300 Subject: [PATCH 09/13] fix sendEmailsOnCreatedCampaignApplication method and failing tests --- .../create-campaign-application-admin.json | 4 +- .../create-campaign-application-admin.mjml | 30 ++----- ...create-campaign-application-organizer.json | 4 +- ...create-campaign-application-organizer.mjml | 21 ++--- .../campaign-application.module.ts | 4 +- .../campaign-application.service.spec.ts | 66 +++++++++++++- .../campaign-application.service.ts | 90 ++++++++++++------- 7 files changed, 142 insertions(+), 77 deletions(-) diff --git a/apps/api/src/assets/templates/create-campaign-application-admin.json b/apps/api/src/assets/templates/create-campaign-application-admin.json index 8ff9b2e7..bdccf163 100644 --- a/apps/api/src/assets/templates/create-campaign-application-admin.json +++ b/apps/api/src/assets/templates/create-campaign-application-admin.json @@ -1,3 +1,3 @@ { - "subject": "Създадена нова апликация за кампания(админ)" -} \ No newline at end of file + "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 index 89cc131a..0f5c39f9 100644 --- a/apps/api/src/assets/templates/create-campaign-application-admin.mjml +++ b/apps/api/src/assets/templates/create-campaign-application-admin.mjml @@ -1,9 +1,6 @@ - - + + - - Здравейте {{firstName}}, -

-
- Организатор: {{firstName}} с имейл: {{email}} създаде нова кампания ТУК{{campaignApplicationName}}!

- За промяна на детайли по кампанията посетете - Редактиране на кампания

-
+
-
\ No newline at end of file + diff --git a/apps/api/src/assets/templates/create-campaign-application-organizer.json b/apps/api/src/assets/templates/create-campaign-application-organizer.json index 3e514294..bdccf163 100644 --- a/apps/api/src/assets/templates/create-campaign-application-organizer.json +++ b/apps/api/src/assets/templates/create-campaign-application-organizer.json @@ -1,3 +1,3 @@ { - "subject": "Създадена нова апликация за кампания(организатор)" -} \ No newline at end of file + "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 index cd6c634f..0c07ffb2 100644 --- a/apps/api/src/assets/templates/create-campaign-application-organizer.mjml +++ b/apps/api/src/assets/templates/create-campaign-application-organizer.mjml @@ -1,9 +1,6 @@ - - + + - Вашата кампания бе създадена успешно, може да я намерите ТУКТУК!

- За промяна на детайли по кампанията посетете - Редактиране на кампания

- - Пожелаваме успешно набиране на средства! + + Пожелаваме успешно набиране на средствата!
-
\ No newline at end of file + diff --git a/apps/api/src/campaign-application/campaign-application.module.ts b/apps/api/src/campaign-application/campaign-application.module.ts index 37c3a96e..06c89bf4 100644 --- a/apps/api/src/campaign-application/campaign-application.module.ts +++ b/apps/api/src/campaign-application/campaign-application.module.ts @@ -1,4 +1,4 @@ -import { EmailService } from './../email/email.service'; +import { EmailService } from './../email/email.service' import { Module } from '@nestjs/common' import { CampaignApplicationService } from './campaign-application.service' import { CampaignApplicationController } from './campaign-application.controller' @@ -6,7 +6,7 @@ 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'; +import { TemplateService } from '../email/template.service' @Module({ imports: [PrismaModule, PersonModule, OrganizerModule], controllers: [CampaignApplicationController], 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 eadf7e94..34e785b9 100644 --- a/apps/api/src/campaign-application/campaign-application.service.spec.ts +++ b/apps/api/src/campaign-application/campaign-application.service.spec.ts @@ -19,6 +19,11 @@ import { mockCampaignApplicationFilesFn, mockCampaignApplicationUploadFileFn, } from './__mocks__/campaing-application-file-mocks' +import { EmailService } from '../email/email.service' +import { + CreateCampaignApplicationAdminEmailDto, + CreateCampaignApplicationOrganizerEmailDto, +} from '../email/template.interface' describe('CampaignApplicationService', () => { let service: CampaignApplicationService @@ -42,6 +47,10 @@ describe('CampaignApplicationService', () => { deleteObject: jest.fn(), } + const mockEmailService = { + sendFromTemplate: jest.fn(), + } + beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ @@ -49,6 +58,7 @@ describe('CampaignApplicationService', () => { MockPrismaService, { provide: OrganizerService, useValue: mockOrganizerService }, { provide: S3Service, useValue: mockS3Service }, + { provide: EmailService, useValue: mockEmailService }, ], }).compile() @@ -101,7 +111,7 @@ 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, @@ -120,6 +130,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) @@ -148,8 +162,58 @@ 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: `https://podkrepi.bg/admin/campaigns/${mockSingleCampaignApplication.id}`, + email: mockPerson.email as string, + firstName: mockPerson.firstName, + } + + const emailOrganizerData = { + campaignApplicationName: mockSingleCampaignApplication.campaignName, + campaignApplicationLink: `https://podkrepi.bg/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).toHaveBeenCalledWith(mailOrganizer, userEmail, { + bypassUnsubscribeManagement: { enable: true }, + }) + + expect(mockEmailService.sendFromTemplate).toHaveBeenCalledWith(mailAdmin, adminEmail, { + bypassUnsubscribeManagement: { enable: true }, + }) + + expect(mockEmailService.sendFromTemplate).toHaveBeenCalledTimes(2) }) }) diff --git a/apps/api/src/campaign-application/campaign-application.service.ts b/apps/api/src/campaign-application/campaign-application.service.ts index 78b7deb7..c021409c 100644 --- a/apps/api/src/campaign-application/campaign-application.service.ts +++ b/apps/api/src/campaign-application/campaign-application.service.ts @@ -14,7 +14,10 @@ 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 { + CreateCampaignApplicationAdminEmailDto, + CreateCampaignApplicationOrganizerEmailDto, +} from '../email/template.interface' @Injectable() export class CampaignApplicationService { @@ -72,41 +75,54 @@ export class CampaignApplicationService { data: campaingApplicationData, }) - const userEmail = { to: [person.email] as EmailData[] } - const adminEmail = { to: [person.email] as EmailData[] } //! reciever admin email - - - const emailAdminData = { - campaignApplicationName: newCampaignApplication.campaignName, - adminEditLink:`https://podkrepi.bg/admin/campaign-applications/edit/${newCampaignApplication.id}`, - campaignApplicationLink: `https://podkrepi.bg/campaigns/nadezhda-za-kaloyan`, - email: person.email as string, - firstName: person.firstName, - } - - const emailOrganizerData = { - campaignApplicationName: newCampaignApplication.campaignName, - editLink: `https://podkrepi.bg/admin/campaign-applications/edit/${newCampaignApplication.id}`, - campaignApplicationLink: `https://podkrepi.bg/campaigns/nadezhda-za-kaloyan`, - - email: person.email as string, - firstName: person.firstName, - } + await this.sendEmailsOnCreatedCampaignApplication( + newCampaignApplication.campaignName, + newCampaignApplication.id, + person, + ) + + return newCampaignApplication + } catch (error) { + Logger.error('Error in create():', error) + throw error + } + } + + async sendEmailsOnCreatedCampaignApplication( + campaignApplicationName: string, + campaignApplicationId: string, + person: Person, + ) { + const userEmail = { to: [person.email] as EmailData[] } + const adminEmail = { to: ['campaign_coordinators@podkrepi.bg'] as EmailData[] } + + const emailAdminData = { + campaignApplicationName, + campaignApplicationLink: `https://podkrepi.bg/admin/campaigns/${campaignApplicationId}`, + email: person.email as string, + firstName: person.firstName, + } - const mailAdmin = new CreateCampaignApplicationAdminEmailDto(emailAdminData) - const mailOrganizer = new CreateCampaignApplicationOrganizerEmailDto(emailOrganizerData) + const emailOrganizerData = { + campaignApplicationName, + campaignApplicationLink: `https://podkrepi.bg/campaign/applications/${campaignApplicationId}`, + email: person.email as string, + firstName: person.firstName, + } - await this.sendEmail.sendFromTemplate(mailAdmin, userEmail, { - bypassUnsubscribeManagement: { enable: true }, - }) + const mailAdmin = new CreateCampaignApplicationAdminEmailDto(emailAdminData) + const mailOrganizer = new CreateCampaignApplicationOrganizerEmailDto(emailOrganizerData) - await this.sendEmail.sendFromTemplate(mailOrganizer, adminEmail, { - bypassUnsubscribeManagement: { enable: true }, - }) + try { + await this.sendEmail.sendFromTemplate(mailOrganizer, userEmail, { + bypassUnsubscribeManagement: { enable: true }, + }) - return newCampaignApplication + await this.sendEmail.sendFromTemplate(mailAdmin, adminEmail, { + bypassUnsubscribeManagement: { enable: true }, + }) } catch (error) { - Logger.error('Error in create():', error) + Logger.error('Error in sendEmailsOnCreatedCampaignApplication():', error) throw error } } @@ -133,7 +149,11 @@ export class CampaignApplicationService { } } - async findOne(id: string, isAdminFlag: boolean, person: Prisma.PersonGetPayload<{ include: { organizer: {select:{id:true}}}}>) { + async findOne( + id: string, + isAdminFlag: boolean, + person: Prisma.PersonGetPayload<{ include: { organizer: { select: { id: true } } } }>, + ) { try { const singleCampaignApplication = await this.prisma.campaignApplication.findUnique({ where: { id }, @@ -153,7 +173,11 @@ export class CampaignApplicationService { } } - async deleteFile(id: string, isAdminFlag: boolean, person: Prisma.PersonGetPayload<{ include: { organizer: {select:{id:true}}}}>) { + async deleteFile( + id: string, + isAdminFlag: boolean, + person: Prisma.PersonGetPayload<{ include: { organizer: { select: { id: true } } } }>, + ) { try { const campaignApplication = await this.prisma.campaignApplication.findFirst({ where: { From 9456a97b4bbffbe8af731d1a3deb4893027bc4d1 Mon Sep 17 00:00:00 2001 From: Martbul Date: Tue, 3 Sep 2024 11:19:31 +0300 Subject: [PATCH 10/13] add adminEmail to .env and add promise.all to sendEmailsOnCreatedCampaignApplication method --- .env | 4 ++++ .env.example | 4 ++++ .../templates/create-campaign-application-admin.mjml | 2 +- .../campaign-application.service.ts | 11 +++++++---- 4 files changed, 16 insertions(+), 5 deletions(-) diff --git a/.env b/.env index 5382ecde..8568884c 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 5382ecde..8568884c 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.mjml b/apps/api/src/assets/templates/create-campaign-application-admin.mjml index 0f5c39f9..f03c8461 100644 --- a/apps/api/src/assets/templates/create-campaign-application-admin.mjml +++ b/apps/api/src/assets/templates/create-campaign-application-admin.mjml @@ -25,7 +25,7 @@ font-family="open Sans Helvetica, Arial, sans-serif" padding-left="25px" padding-right="25px"> - Организатор: {{firstName}} с имейл: {{email}} създаде нова кампания + Организатор {{firstName}} с имейл {{email}} създаде нова кампания {{campaignApplicationName}}!

diff --git a/apps/api/src/campaign-application/campaign-application.service.ts b/apps/api/src/campaign-application/campaign-application.service.ts index c021409c..ee0bf95f 100644 --- a/apps/api/src/campaign-application/campaign-application.service.ts +++ b/apps/api/src/campaign-application/campaign-application.service.ts @@ -26,7 +26,7 @@ export class CampaignApplicationService { private prisma: PrismaService, private organizerService: OrganizerService, private s3: S3Service, - private sendEmail: EmailService, + private emailService: EmailService, ) {} async getCampaignByIdWithPersonIds(id: string): Promise { @@ -94,7 +94,8 @@ export class CampaignApplicationService { person: Person, ) { const userEmail = { to: [person.email] as EmailData[] } - const adminEmail = { to: ['campaign_coordinators@podkrepi.bg'] as EmailData[] } + // const adminEmail = { to: [process.env.CAMPAIGN_COORDINATOR_EMAIL] as EmailData[] } + const adminEmail = { to: ["martbul01@gmail.com"] as EmailData[] } const emailAdminData = { campaignApplicationName, @@ -114,13 +115,15 @@ export class CampaignApplicationService { const mailOrganizer = new CreateCampaignApplicationOrganizerEmailDto(emailOrganizerData) try { - await this.sendEmail.sendFromTemplate(mailOrganizer, userEmail, { + const userEmailPromise = this.emailService.sendFromTemplate(mailOrganizer, userEmail, { bypassUnsubscribeManagement: { enable: true }, }) - await this.sendEmail.sendFromTemplate(mailAdmin, adminEmail, { + 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 From b04e94a30c3d01bf257f67bd3aafc6b2f7a51fd9 Mon Sep 17 00:00:00 2001 From: Martbul Date: Tue, 3 Sep 2024 16:05:58 +0300 Subject: [PATCH 11/13] fix conflict --- .../campaign-application/campaign-application.service.ts | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/apps/api/src/campaign-application/campaign-application.service.ts b/apps/api/src/campaign-application/campaign-application.service.ts index ee0bf95f..2098d38b 100644 --- a/apps/api/src/campaign-application/campaign-application.service.ts +++ b/apps/api/src/campaign-application/campaign-application.service.ts @@ -176,11 +176,7 @@ export class CampaignApplicationService { } } - async deleteFile( - id: string, - isAdminFlag: boolean, - person: Prisma.PersonGetPayload<{ include: { organizer: { select: { id: true } } } }>, - ) { + async deleteFile(id: string, isAdminFlag: boolean, person: Prisma.PersonGetPayload<{ include: { organizer: {select:{id:true}}}}>) { try { const campaignApplication = await this.prisma.campaignApplication.findFirst({ where: { From e681783a2940a608ef783a6c1f8303ad7b28e7b6 Mon Sep 17 00:00:00 2001 From: Aleksandar Date: Thu, 26 Sep 2024 10:14:19 +0300 Subject: [PATCH 12/13] fix: TS errors --- .../__mocks__/campaign-application-mocks.ts | 29 +------------------ apps/api/src/email/template.interface.ts | 6 ++-- 2 files changed, 4 insertions(+), 31 deletions(-) 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 d1c23364..b9b2ed45 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', @@ -19,33 +19,6 @@ export const mockNewCampaignApplication = { campaignEndDate: '2024-02-02', } -export const mockSingleCampaignApplication = { - id: '1', - createdAt: new Date('2022-04-08T06:36:33.661Z'), - updatedAt: new Date('2022-04-08T06:36:33.662Z'), - description: 'Test description1', - organizerId: 'ffdbcc41-85ec-476c-9e59-0662f3b433af', - organizerName: 'Test Organizer1', - organizerEmail: 'organizer1@example.com', - beneficiary: 'test beneficary1', - organizerPhone: '123456789', - organizerBeneficiaryRel: 'Test Relation1', - campaignName: 'Test Campaign1', - goal: 'Test Goal1', - history: 'test history1', - amount: '1000', - campaignGuarantee: 'test campaignGuarantee1', - otherFinanceSources: 'test otherFinanceSources1', - otherNotes: 'test otherNotes1', - state: CampaignApplicationState.review, - campaignTypeId: 'ffdbcc41-85ec-0000-9e59-0662f3b433af', - ticketURL: 'testsodifhso1', - archived: false, - documents: [{ id: 'fileId' }], - campaignEnd: 'funds', - campaignEndDate: undefined, -} - export const mockSingleCampaignApplication = { id: '1', createdAt: new Date('2022-04-08T06:36:33.661Z'), diff --git a/apps/api/src/email/template.interface.ts b/apps/api/src/email/template.interface.ts index 8769504b..12059d83 100644 --- a/apps/api/src/email/template.interface.ts +++ b/apps/api/src/email/template.interface.ts @@ -105,7 +105,7 @@ export class RefundDonationEmailDto extends EmailTemplate<{ export class CreateCampaignApplicationAdminEmailDto extends EmailTemplate<{ campaignApplicationName: string - adminEditLink: string + adminEditLink?: string campaignApplicationLink: string email: string firstName: string @@ -116,8 +116,8 @@ export class CreateCampaignApplicationAdminEmailDto extends EmailTemplate<{ export class CreateCampaignApplicationOrganizerEmailDto extends EmailTemplate<{ campaignApplicationName: string - editLink: string - campaignApplicationLink:string + editLink?: string + campaignApplicationLink: string email: string firstName: string }> { From 7fdee4e8008e80d47cc98e48c9a85b186c9650c8 Mon Sep 17 00:00:00 2001 From: Aleksandar Date: Thu, 26 Sep 2024 11:45:30 +0300 Subject: [PATCH 13/13] fix test --- .../campaign-application.service.spec.ts | 35 +++++++++++++++---- .../campaign-application.service.ts | 15 +++++--- 2 files changed, 40 insertions(+), 10 deletions(-) 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 1dd37cba..a157126a 100644 --- a/apps/api/src/campaign-application/campaign-application.service.spec.ts +++ b/apps/api/src/campaign-application/campaign-application.service.spec.ts @@ -16,9 +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, @@ -39,8 +48,17 @@ 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, @@ -173,14 +191,14 @@ describe('CampaignApplicationService', () => { const emailAdminData = { campaignApplicationName: mockSingleCampaignApplication.campaignName, - campaignApplicationLink: `https://podkrepi.bg/admin/campaigns/${mockSingleCampaignApplication.id}`, + campaignApplicationLink: `${process.env.APP_URL}/admin/campaigns/${mockSingleCampaignApplication.id}`, email: mockPerson.email as string, firstName: mockPerson.firstName, } const emailOrganizerData = { campaignApplicationName: mockSingleCampaignApplication.campaignName, - campaignApplicationLink: `https://podkrepi.bg/campaign/applications/${mockSingleCampaignApplication.id}`, + campaignApplicationLink: `${process.env.APP_URL}/campaign/applications/${mockSingleCampaignApplication.id}`, email: mockPerson.email as string, firstName: mockPerson.firstName, } @@ -196,11 +214,16 @@ describe('CampaignApplicationService', () => { mockPerson, ) - expect(mockEmailService.sendFromTemplate).toHaveBeenCalledWith(mailOrganizer, userEmail, { - bypassUnsubscribeManagement: { enable: true }, - }) + expect(mockEmailService.sendFromTemplate).toHaveBeenNthCalledWith( + 1, + mailOrganizer, + userEmail, + { + bypassUnsubscribeManagement: { enable: true }, + }, + ) - expect(mockEmailService.sendFromTemplate).toHaveBeenCalledWith(mailAdmin, adminEmail, { + expect(mockEmailService.sendFromTemplate).toHaveBeenNthCalledWith(2, mailAdmin, adminEmail, { bypassUnsubscribeManagement: { enable: true }, }) diff --git a/apps/api/src/campaign-application/campaign-application.service.ts b/apps/api/src/campaign-application/campaign-application.service.ts index 4f2311fd..b9fcda03 100644 --- a/apps/api/src/campaign-application/campaign-application.service.ts +++ b/apps/api/src/campaign-application/campaign-application.service.ts @@ -18,6 +18,7 @@ import { CreateCampaignApplicationAdminEmailDto, CreateCampaignApplicationOrganizerEmailDto, } from '../email/template.interface' +import { ConfigService } from '@nestjs/config' function dateMaybe(d?: string) { return d != null && @@ -35,6 +36,7 @@ export class CampaignApplicationService { private organizerService: OrganizerService, private s3: S3Service, private emailService: EmailService, + private readonly configService: ConfigService, ) {} async create(createCampaignApplicationDto: CreateCampaignApplicationDto, person: Person) { @@ -99,20 +101,25 @@ export class CampaignApplicationService { campaignApplicationId: string, person: Person, ) { + const adminMail = this.configService.get('CAMPAIGN_COORDINATOR_EMAIL', '') const userEmail = { to: [person.email] as EmailData[] } - // const adminEmail = { to: [process.env.CAMPAIGN_COORDINATOR_EMAIL] as EmailData[] } - const adminEmail = { to: ['martbul01@gmail.com'] as EmailData[] } + const adminEmail = { to: [adminMail] as EmailData[] } + // const adminEmail = { to: ['martbul01@gmail.com'] as EmailData[] } const emailAdminData = { campaignApplicationName, - campaignApplicationLink: `https://podkrepi.bg/admin/campaigns/${campaignApplicationId}`, + campaignApplicationLink: `${this.configService.get( + 'APP_URL', + )}/admin/campaigns/${campaignApplicationId}`, email: person.email as string, firstName: person.firstName, } const emailOrganizerData = { campaignApplicationName, - campaignApplicationLink: `https://podkrepi.bg/campaign/applications/${campaignApplicationId}`, + campaignApplicationLink: `${this.configService.get( + 'APP_URL', + )}/campaign/applications/${campaignApplicationId}`, email: person.email as string, firstName: person.firstName, }