From 8369f7f666356ed8d6834d72b092ff7b4c1ae81e Mon Sep 17 00:00:00 2001 From: Guillaume Cauchois Date: Mon, 18 Nov 2024 13:35:12 +0100 Subject: [PATCH 1/3] feat(Refering): Add refer candidate api endpoint --- src/auth/auth.controller.ts | 10 ++ src/auth/auth.service.ts | 14 +++ src/mails/mails.service.ts | 16 +-- .../dto/create-user-refering.dto.ts | 106 ++++++++++++++++++ .../dto/create-user-refering.pipe.ts | 39 +++++++ .../users-creation.controller.ts | 90 ++++++++++++++- src/users-creation/users-creation.service.ts | 9 ++ src/users/users.types.ts | 2 +- 8 files changed, 273 insertions(+), 13 deletions(-) create mode 100644 src/users-creation/dto/create-user-refering.dto.ts create mode 100644 src/users-creation/dto/create-user-refering.pipe.ts diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts index b0fa6095..63ff283f 100644 --- a/src/auth/auth.controller.ts +++ b/src/auth/auth.controller.ts @@ -285,6 +285,16 @@ export class AuthController { throw new NotFoundException(); } + await this.authService.sendWelcomeMail({ + id: updatedUser.id, + firstName: updatedUser.firstName, + role: updatedUser.role, + zone: updatedUser.zone, + email: updatedUser.email, + }); + await this.authService.sendOnboardingJ1BAOMail(updatedUser); + await this.authService.sendOnboardingJ3ProfileCompletionMail(updatedUser); + return updatedUser.email; } } diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts index 4b9a0508..93a799d2 100644 --- a/src/auth/auth.service.ts +++ b/src/auth/auth.service.ts @@ -146,4 +146,18 @@ export class AuthService { } ); } + + async sendWelcomeMail( + user: Pick + ) { + return this.mailsService.sendWelcomeMail(user); + } + + async sendOnboardingJ1BAOMail(user: User) { + return this.mailsService.sendOnboardingJ1BAOMail(user); + } + + async sendOnboardingJ3ProfileCompletionMail(user: User) { + return this.mailsService.sendOnboardingJ3ProfileCompletionMail(user); + } } diff --git a/src/mails/mails.service.ts b/src/mails/mails.service.ts index 59d7cb57..904081c1 100644 --- a/src/mails/mails.service.ts +++ b/src/mails/mails.service.ts @@ -835,33 +835,27 @@ export class MailsService { refererFirstName: referer.firstName, candidateFirstName: candidate.firstName, candidateLastName: candidate.lastName, - loginUrl: `${process.env.FRONTEND_URL}/login`, + loginUrl: `${process.env.FRONT_URL}/login`, zone: referer.zone, }, }); } - // TODO: Call this method after completing refering funnel - // Generate a token with - // const token = await this.authService.generateVerificationToken(candidate); async sendReferedCandidateFinalizeAccountMail( - // TODO after merge - // referer: UserWithOrganization, referer: User, candidate: User, token: string ) { await this.queuesService.addToWorkQueue(Jobs.SEND_MAIL, { - toEmail: referer.email, + toEmail: candidate.email, templateId: MailjetTemplates.REFERED_CANDIDATE_FINALIZE_ACCOUNT, variables: { + id: candidate.id, candidateFirstName: candidate.firstName, refererFirstName: referer.firstName, refererLastName: referer.lastName, - // organizationName: referer.organization.name, - // TODO after merge - // structure: referer.structure, - finalizeAccountUrl: `${process.env.FRONTEND_URL}/finaliser-compte-oriente?token=${token}`, + organizationName: referer.organization.name, + finalizeAccountUrl: `${process.env.FRONT_URL}/finaliser-compte-oriente?token=${token}`, zone: candidate.zone, }, }); diff --git a/src/users-creation/dto/create-user-refering.dto.ts b/src/users-creation/dto/create-user-refering.dto.ts new file mode 100644 index 00000000..20e7e24c --- /dev/null +++ b/src/users-creation/dto/create-user-refering.dto.ts @@ -0,0 +1,106 @@ +import { ApiProperty, PickType } from '@nestjs/swagger'; +import { IsArray, IsNumber, IsOptional, IsString } from 'class-validator'; +import { Ambition } from 'src/common/ambitions/models'; +import { BusinessLine } from 'src/common/business-lines/models'; +import { Department } from 'src/common/locations/locations.types'; +import { + CandidateAccommodation, + CandidateResource, + CandidateYesNoNSPPValue, + JobSearchDuration, + Nationality, + StudiesLevel, + WorkingExperience, + YesNoJNSPRValue, +} from 'src/contacts/contacts.types'; +import { HelpNeed } from 'src/user-profiles/models'; +import { User } from 'src/users/models'; +import { Gender, Program } from 'src/users/users.types'; + +export class CreateUserReferingDto extends PickType(User, [ + 'firstName', + 'lastName', + 'email', + 'gender', + 'phone', + 'password', +] as const) { + @ApiProperty() + @IsNumber() + @IsOptional() + gender: Gender; + + @ApiProperty() + @IsString() + @IsOptional() + program: Program; + + @ApiProperty() + @IsString() + @IsOptional() + birthDate: Date; + + @ApiProperty() + @IsString() + department: Department; + + @ApiProperty() + @IsString() + @IsOptional() + campaign?: string; + + @ApiProperty() + @IsArray() + @IsOptional() + helpNeeds?: HelpNeed[]; + + @ApiProperty() + @IsString() + @IsOptional() + workingRight?: CandidateYesNoNSPPValue; + + @ApiProperty() + @IsArray() + @IsOptional() + searchBusinessLines?: BusinessLine[]; + + @ApiProperty() + @IsArray() + @IsOptional() + searchAmbitions?: Ambition[]; + + @ApiProperty() + @IsString() + @IsOptional() + nationality?: Nationality; + + @ApiProperty() + @IsString() + @IsOptional() + accommodation?: CandidateAccommodation; + + @ApiProperty() + @IsString() + @IsOptional() + hasSocialWorker?: YesNoJNSPRValue; + + @ApiProperty() + @IsString() + @IsOptional() + resources?: CandidateResource; + + @ApiProperty() + @IsString() + @IsOptional() + studiesLevel?: StudiesLevel; + + @ApiProperty() + @IsString() + @IsOptional() + workingExperience?: WorkingExperience; + + @ApiProperty() + @IsString() + @IsOptional() + jobSearchDuration?: JobSearchDuration; +} diff --git a/src/users-creation/dto/create-user-refering.pipe.ts b/src/users-creation/dto/create-user-refering.pipe.ts new file mode 100644 index 00000000..d5f8553a --- /dev/null +++ b/src/users-creation/dto/create-user-refering.pipe.ts @@ -0,0 +1,39 @@ +/* eslint-disable @typescript-eslint/ban-types */ +import { + ArgumentMetadata, + BadRequestException, + PipeTransform, +} from '@nestjs/common'; +import { plainToInstance } from 'class-transformer'; +import { validate } from 'class-validator'; +import { CreateUserReferingDto } from './create-user-refering.dto'; + +export class CreateUserReferingPipe + implements + PipeTransform> +{ + private static toValidate(metatype: Function): boolean { + const types: Function[] = [String, Boolean, Number, Array, Object]; + return !types.includes(metatype); + } + + async transform( + value: CreateUserReferingDto, + { metatype }: ArgumentMetadata + ): Promise { + if (!metatype || !CreateUserReferingPipe.toValidate(metatype)) { + return value; + } + const object = plainToInstance(metatype, value); + const errors = await validate(object, { + whitelist: true, + forbidNonWhitelisted: true, + forbidUnknownValues: true, + }); + + if (errors.length > 0) { + throw new BadRequestException(); + } + return value; + } +} diff --git a/src/users-creation/users-creation.controller.ts b/src/users-creation/users-creation.controller.ts index 72fd68fe..b638b381 100644 --- a/src/users-creation/users-creation.controller.ts +++ b/src/users-creation/users-creation.controller.ts @@ -11,7 +11,7 @@ import { import { ApiTags } from '@nestjs/swagger'; import { Throttle } from '@nestjs/throttler'; import { encryptPassword } from 'src/auth/auth.utils'; -import { Public } from 'src/auth/guards'; +import { Public, UserPayload } from 'src/auth/guards'; import { UserPermissions, UserPermissionsGuard } from 'src/users/guards'; import { User } from 'src/users/models'; import { @@ -33,6 +33,8 @@ import { CreateUserRegistrationDto, CreateUserRegistrationPipe, } from './dto'; +import { CreateUserReferingDto } from './dto/create-user-refering.dto'; +import { CreateUserReferingPipe } from './dto/create-user-refering.pipe'; import { UsersCreationService } from './users-creation.service'; function generateFakePassword() { @@ -259,4 +261,90 @@ export class UsersCreationController { } } } + + @UserPermissions(Permissions.REFERER) + @UseGuards(UserPermissionsGuard) + @Throttle(10, 60) + @Post('refering') + async createUserRefering( + @Body(new CreateUserReferingPipe()) + createUserReferingDto: CreateUserReferingDto, + @UserPayload() + referer: User + ) { + if (!isValidPhone(createUserReferingDto.phone)) { + throw new BadRequestException(); + } + + if (!createUserReferingDto.program) { + throw new BadRequestException(); + } + + const userRandomPassword = generateFakePassword(); + const { hash, salt } = encryptPassword(userRandomPassword); + + const zone = getZoneFromDepartment(createUserReferingDto.department); + + const userToCreate: Partial = { + refererId: referer.id, + OrganizationId: referer.OrganizationId, + firstName: createUserReferingDto.firstName, + lastName: createUserReferingDto.lastName, + email: createUserReferingDto.email, + role: UserRoles.CANDIDATE, + gender: createUserReferingDto.gender, + phone: createUserReferingDto.phone, + address: null, + adminRole: null, + zone, + password: hash, + salt, + }; + + try { + const { id: createdUserId } = await this.usersCreationService.createUser( + userToCreate + ); + + await this.usersCreationService.updateUserProfileByUserId(createdUserId, { + department: createUserReferingDto.department, + helpNeeds: createUserReferingDto.helpNeeds, + searchBusinessLines: createUserReferingDto.searchBusinessLines, + searchAmbitions: createUserReferingDto.searchAmbitions, + }); + + const createdUser = await this.usersCreationService.findOneUser( + createdUserId + ); + + await this.usersCreationService.createExternalDBUser(createdUserId, { + program: createUserReferingDto.program, + birthDate: createUserReferingDto.birthDate, + campaign: + createUserReferingDto.program === Programs.THREE_SIXTY + ? createUserReferingDto.campaign + : undefined, + workingRight: createUserReferingDto.workingRight, + nationality: createUserReferingDto.nationality, + accommodation: createUserReferingDto.accommodation, + hasSocialWorker: createUserReferingDto.hasSocialWorker, + resources: createUserReferingDto.resources, + studiesLevel: createUserReferingDto.studiesLevel, + workingExperience: createUserReferingDto.workingExperience, + jobSearchDuration: createUserReferingDto.jobSearchDuration, + gender: createUserReferingDto.gender, + }); + + await this.usersCreationService.sendFinalizeAccountReferedUser( + createdUser, + referer + ); + + return createdUser; + } catch (err) { + if (((err as Error).name = SequelizeUniqueConstraintError)) { + throw new ConflictException(); + } + } + } } diff --git a/src/users-creation/users-creation.service.ts b/src/users-creation/users-creation.service.ts index e8487fb0..6a9c9520 100644 --- a/src/users-creation/users-creation.service.ts +++ b/src/users-creation/users-creation.service.ts @@ -83,6 +83,15 @@ export class UsersCreationService { return this.mailsService.sendVerificationMail(user, token); } + async sendFinalizeAccountReferedUser(candidate: User, referer: User) { + const token = await this.authService.generateVerificationToken(candidate); + return this.mailsService.sendReferedCandidateFinalizeAccountMail( + referer, + candidate, + token + ); + } + async sendOnboardingJ3ProfileCompletionMail(user: User) { return this.mailsService.sendOnboardingJ3ProfileCompletionMail(user); } diff --git a/src/users/users.types.ts b/src/users/users.types.ts index d1ff1a28..8651d6b0 100644 --- a/src/users/users.types.ts +++ b/src/users/users.types.ts @@ -32,7 +32,7 @@ export type Permission = (typeof Permissions)[keyof typeof Permissions]; export const UserPermissions: { [K in UserRole]: Permission | Permission[] } = { [UserRoles.CANDIDATE]: Permissions.CANDIDATE, - [UserRoles.REFERER]: Permissions.COACH, + [UserRoles.REFERER]: Permissions.REFERER, [UserRoles.COACH]: [Permissions.COACH, Permissions.RESTRICTED_COACH], [UserRoles.ADMIN]: Permissions.ADMIN, }; From d1477db64c0c68b5c1969097d269a8a40b9e02e1 Mon Sep 17 00:00:00 2001 From: Guillaume Cauchois Date: Mon, 25 Nov 2024 14:20:24 +0100 Subject: [PATCH 2/3] fix: remove optionnal for mandatory fields --- src/users-creation/dto/create-user-refering.dto.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/users-creation/dto/create-user-refering.dto.ts b/src/users-creation/dto/create-user-refering.dto.ts index 20e7e24c..d834197e 100644 --- a/src/users-creation/dto/create-user-refering.dto.ts +++ b/src/users-creation/dto/create-user-refering.dto.ts @@ -27,17 +27,14 @@ export class CreateUserReferingDto extends PickType(User, [ ] as const) { @ApiProperty() @IsNumber() - @IsOptional() gender: Gender; @ApiProperty() @IsString() - @IsOptional() program: Program; @ApiProperty() @IsString() - @IsOptional() birthDate: Date; @ApiProperty() @@ -51,12 +48,10 @@ export class CreateUserReferingDto extends PickType(User, [ @ApiProperty() @IsArray() - @IsOptional() helpNeeds?: HelpNeed[]; @ApiProperty() @IsString() - @IsOptional() workingRight?: CandidateYesNoNSPPValue; @ApiProperty() From e5e6163f4d1e096daed4aa4699905afcc715a0c7 Mon Sep 17 00:00:00 2001 From: Guillaume Cauchois Date: Wed, 27 Nov 2024 16:09:57 +0100 Subject: [PATCH 3/3] fix: merge conflict --- src/mails/mails.service.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/mails/mails.service.ts b/src/mails/mails.service.ts index 7b321471..d91915a5 100644 --- a/src/mails/mails.service.ts +++ b/src/mails/mails.service.ts @@ -79,7 +79,9 @@ export class MailsService { }); } - async sendWelcomeMail(user: User) { + async sendWelcomeMail( + user: Pick + ) { const { candidatesAdminMail } = getAdminMailsFromZone(user.zone); if (user.role === UserRoles.COACH) {