From ba6f7dbd7faf31ebb00177cc6ed78f02fa1d387f Mon Sep 17 00:00:00 2001 From: Yoan VALLET <ext.sopra.yvallet@grandlyon.com> Date: Wed, 23 Mar 2022 17:04:54 +0000 Subject: [PATCH] feat(personalOffers): create personal offers endpoints --- src/app.module.ts | 2 + src/auth/auth.service.spec.ts | 1 + .../dto/create-personal-offer.dto.ts | 21 +++ src/personal-offers/dto/personal-offer.dto.ts | 36 ++++ .../personal-offers.controller.spec.ts | 142 +++++++++++++++ .../personal-offers.controller.ts | 81 +++++++++ src/personal-offers/personal-offers.module.ts | 18 ++ .../personal-offers.service.spec.ts | 111 ++++++++++++ .../personal-offers.service.ts | 41 +++++ .../schemas/personal-offer.schema.ts | 22 +++ .../schemas/week.schema.ts | 2 +- src/structures/dto/structure.dto.ts | 8 +- src/structures/schemas/structure.schema.ts | 9 +- .../services/structure.service.spec.ts | 46 ++++- src/structures/services/structures.service.ts | 60 ++++++- src/structures/structures.controller.spec.ts | 2 + src/users/dto/create-user.dto.ts | 5 + .../guards/isPersonalOfferOwner.guard.spec.ts | 104 +++++++++++ .../guards/isPersonalOfferOwner.guard.ts | 21 +++ .../guards/isStructureOwner.guard.spec.ts | 3 +- src/users/schemas/user.schema.ts | 5 + src/users/services/users.service.spec.ts | 44 ++++- src/users/services/users.service.ts | 34 +++- test/mock/data/personalOffers.mock.data.ts | 81 +++++++++ test/mock/data/structures.mock.data.ts | 168 ++++++++++++++++++ test/mock/data/users.mock.data.ts | 58 ++++++ .../guards/isPersonalOfferOwner.mock.guard.ts | 2 + .../services/personalOffers.mock.service.ts | 47 +++++ test/mock/services/structures.mock.service.ts | 100 +++++++++++ test/mock/services/user.mock.service.ts | 40 +++++ 30 files changed, 1294 insertions(+), 20 deletions(-) create mode 100644 src/personal-offers/dto/create-personal-offer.dto.ts create mode 100644 src/personal-offers/dto/personal-offer.dto.ts create mode 100644 src/personal-offers/personal-offers.controller.spec.ts create mode 100644 src/personal-offers/personal-offers.controller.ts create mode 100644 src/personal-offers/personal-offers.module.ts create mode 100644 src/personal-offers/personal-offers.service.spec.ts create mode 100644 src/personal-offers/personal-offers.service.ts create mode 100644 src/personal-offers/schemas/personal-offer.schema.ts rename src/{structures => shared}/schemas/week.schema.ts (85%) create mode 100644 src/users/guards/isPersonalOfferOwner.guard.spec.ts create mode 100644 src/users/guards/isPersonalOfferOwner.guard.ts create mode 100644 test/mock/data/personalOffers.mock.data.ts create mode 100644 test/mock/data/structures.mock.data.ts create mode 100644 test/mock/data/users.mock.data.ts create mode 100644 test/mock/guards/isPersonalOfferOwner.mock.guard.ts create mode 100644 test/mock/services/personalOffers.mock.service.ts diff --git a/src/app.module.ts b/src/app.module.ts index 148e1d602..f8b0a733d 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -15,6 +15,7 @@ import { PagesModule } from './pages/pages.module'; import { TempUserModule } from './temp-user/temp-user.module'; import { NewsletterModule } from './newsletter/newsletter.module'; import { ContactModule } from './contact/contact.module'; +import { PersonalOffersModule } from './personal-offers/personal-offers.module'; @Module({ imports: [ ConfigurationModule, @@ -34,6 +35,7 @@ import { ContactModule } from './contact/contact.module'; TempUserModule, NewsletterModule, ContactModule, + PersonalOffersModule, ], controllers: [AppController], }) diff --git a/src/auth/auth.service.spec.ts b/src/auth/auth.service.spec.ts index e8f8f9205..f67d73a15 100644 --- a/src/auth/auth.service.spec.ts +++ b/src/auth/auth.service.spec.ts @@ -44,6 +44,7 @@ describe('AuthService', () => { emailVerified: false, email: 'jacques.dupont@mii.com', role: 0, + personalOffers: [], }; const loginDto: LoginDto = { email: 'jacques.dupont@mii.com', password: 'test1A!!' }; //NOSONAR expect(await service.validateUser(loginDto)).toStrictEqual(result); diff --git a/src/personal-offers/dto/create-personal-offer.dto.ts b/src/personal-offers/dto/create-personal-offer.dto.ts new file mode 100644 index 000000000..5cfc1d0e6 --- /dev/null +++ b/src/personal-offers/dto/create-personal-offer.dto.ts @@ -0,0 +1,21 @@ +import { ApiProperty } from '@nestjs/swagger'; +import { Type } from 'class-transformer'; +import { IsNotEmpty, ValidateNested } from 'class-validator'; +import { PersonalOfferDto } from './personal-offer.dto'; + +export class CreatePersonalOfferDto { + @ApiProperty({ + description: 'Id of the structure', + type: [String], + }) + @IsNotEmpty() + structureId: string; + + @ApiProperty({ + description: 'Personal offer to create', + type: [String], + }) + @ValidateNested({ each: true }) + @Type(() => PersonalOfferDto) + personalOffer: PersonalOfferDto; +} diff --git a/src/personal-offers/dto/personal-offer.dto.ts b/src/personal-offers/dto/personal-offer.dto.ts new file mode 100644 index 000000000..2db73fa51 --- /dev/null +++ b/src/personal-offers/dto/personal-offer.dto.ts @@ -0,0 +1,36 @@ +import { ApiProperty } from '@nestjs/swagger'; + +export class PersonalOfferDto { + /** accompaniments **/ + @ApiProperty({ + description: 'List of procedures accompaniments', + type: [String], + }) + proceduresAccompaniment: string[]; + /** trainings **/ + @ApiProperty({ + description: 'List of base skills trainings', + type: [String], + }) + baseSkills: string[]; + @ApiProperty({ + description: 'List of access right trainings', + type: [String], + }) + accessRight: string[]; + @ApiProperty({ + description: 'List of digital, culture and security trainings', + type: [String], + }) + digitalCultureSecurity: string[]; + @ApiProperty({ + description: 'List of social and profressional trainings', + type: [String], + }) + socialAndProfessional: string[]; + @ApiProperty({ + description: 'List of parenting help trainings', + type: [String], + }) + parentingHelp: string[]; +} diff --git a/src/personal-offers/personal-offers.controller.spec.ts b/src/personal-offers/personal-offers.controller.spec.ts new file mode 100644 index 000000000..9c7511d5a --- /dev/null +++ b/src/personal-offers/personal-offers.controller.spec.ts @@ -0,0 +1,142 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { PersonalOffersController } from './personal-offers.controller'; +import { PersonalOffersService } from './personal-offers.service'; +import { PersonalOffersServiceMock } from '../../test/mock/services/personalOffers.mock.service'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { mockJwtAuthGuard } from '../../test/mock/guards/jwt-auth.mock.guard'; +import { IsPersonalOfferOwnerGuard } from '../users/guards/isPersonalOfferOwner.guard'; +import { mockIsPersonalOfferOwnerGuard } from '../../test/mock/guards/isPersonalOfferOwner.mock.guard'; +import { + createPersonalOffersDtoDataMock, + updatePersonalOffersDtoDataMock, + personalOffersDataMock, +} from '../../test/mock/data/personalOffers.mock.data'; +import { UsersService } from '../users/services/users.service'; +import { UsersServiceMock } from '../../test/mock/services/user.mock.service'; +import { StructuresService } from '../structures/services/structures.service'; +import { StructuresServiceMock } from '../../test/mock/services/structures.mock.service'; + +describe('PersonalOffersController', () => { + let controller: PersonalOffersController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + controllers: [PersonalOffersController], + providers: [ + { + provide: PersonalOffersService, + useClass: PersonalOffersServiceMock, + }, + { + provide: UsersService, + useClass: UsersServiceMock, + }, + { + provide: StructuresService, + useClass: StructuresServiceMock, + }, + ], + }) + .overrideGuard(JwtAuthGuard) + .useValue(mockJwtAuthGuard) + .overrideGuard(IsPersonalOfferOwnerGuard) + .useValue(mockIsPersonalOfferOwnerGuard) + .compile(); + + controller = module.get<PersonalOffersController>(PersonalOffersController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + describe('find personal offer', () => { + it('should get personal offer', async () => { + expect(await controller.find('1234ba0e2ab5775cfc01ed3e')).toBe(personalOffersDataMock[0]); + }); + it('should get personal offer does not exist', async () => { + try { + await controller.find('abcd'); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('Personal offer does not exist'); + expect(e.status).toBe(404); + } + }); + }); + + describe('create personal offer', () => { + it('should create personal offer for existing user and structure', async () => { + const req = { user: { _id: '6036721022462b001334c4bb' } }; + expect(await controller.create(req, createPersonalOffersDtoDataMock[0])).toEqual(personalOffersDataMock[0]); + }); + it('should return personal offer already exist in the structure', async () => { + const req = { user: { _id: '6036721022462b001334c4bb' } }; + try { + await controller.create(req, createPersonalOffersDtoDataMock[1]); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('Personal offer already exist in the structure'); + expect(e.status).toBe(400); + } + }); + it('should return structure not found for the personal offer attachment', async () => { + const req = { user: { _id: '6036721022462b001334c4bb' } }; + try { + await controller.create(req, createPersonalOffersDtoDataMock[2]); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('Structure not found for the personal offer attachment'); + expect(e.status).toBe(400); + } + }); + it('should return user not found for the personal offer attachment', async () => { + const req = { user: { _id: 'unIdQuiExistePasTropTrop' } }; + try { + await controller.create(req, createPersonalOffersDtoDataMock[0]); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('User not found for the personal offer attachment'); + expect(e.status).toBe(400); + } + }); + }); + + describe('update personal offer', () => { + it('should get updated personal offer', async () => { + expect(await controller.update('2345ba0e2ab5775cfc01ed4d', updatePersonalOffersDtoDataMock[1])).toEqual( + personalOffersDataMock[1] + ); + }); + it('should get invalid personal offer id', async () => { + try { + await controller.update('abcd', updatePersonalOffersDtoDataMock[1]); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('Invalid personal offer id for update'); + expect(e.status).toBe(400); + } + }); + }); + + describe('update personal offer', () => { + it('should get deleted personal offer', async () => { + expect(await controller.delete('2345ba0e2ab5775cfc01ed4d')).toEqual(personalOffersDataMock[1]); + }); + it('should get invalid personal offer id', async () => { + try { + await controller.delete('abcd'); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('Invalid personal offer id for deletion'); + expect(e.status).toBe(400); + } + }); + }); +}); diff --git a/src/personal-offers/personal-offers.controller.ts b/src/personal-offers/personal-offers.controller.ts new file mode 100644 index 000000000..f2c72d4f0 --- /dev/null +++ b/src/personal-offers/personal-offers.controller.ts @@ -0,0 +1,81 @@ +import { Body, Controller, Delete, Get, Param, Post, Put, Request, UseGuards } from '@nestjs/common'; +import { ApiBody, ApiParam } from '@nestjs/swagger'; +import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { StructuresService } from '../structures/services/structures.service'; +import { IsPersonalOfferOwnerGuard } from '../users/guards/isPersonalOfferOwner.guard'; +import { IUser } from '../users/interfaces/user.interface'; +import { UsersService } from '../users/services/users.service'; +import { CreatePersonalOfferDto } from './dto/create-personal-offer.dto'; +import { PersonalOfferDto } from './dto/personal-offer.dto'; +import { PersonalOffersService } from './personal-offers.service'; +import { PersonalOfferDocument } from './schemas/personal-offer.schema'; + +@Controller('personal-offers') +export class PersonalOffersController { + constructor( + private personalOffersService: PersonalOffersService, + private structuresService: StructuresService, + private usersService: UsersService + ) {} + + /** + * Return personal offer of given id. + * @param id + * @returns {PersonalOffer} Personal offer + */ + @Get(':id') + @ApiParam({ name: 'id', type: String, required: true }) + public async find(@Param('id') id: string) { + return this.personalOffersService.findOne(id); + } + + /** + * Create and return a personal offer. + * @param createPersonalOfferDto + * @returns {PersonalOffer} created personal offer + */ + @Post() + @ApiBody({ type: CreatePersonalOfferDto, required: true }) + @UseGuards(JwtAuthGuard) + public async create( + @Request() req, + @Body() createPersonalOfferDto: CreatePersonalOfferDto + ): Promise<PersonalOfferDocument> { + const user: IUser = req.user; + const personalOfferDocument: PersonalOfferDocument = await this.personalOffersService.create( + createPersonalOfferDto + ); + await this.structuresService.addPersonalOffer(createPersonalOfferDto.structureId, personalOfferDocument); + await this.usersService.addPersonalOffer(user._id, personalOfferDocument); + return personalOfferDocument; + } + + /** + * Update and return a personal offer. + * @param id - id of the personal offer to update + * @param updatePersonalOfferDto - personal offer data to update + * @returns {PersonalOffer} Updated personal offer + */ + @Put(':id') + @ApiParam({ name: 'id', type: String, required: true }) + @ApiBody({ type: PersonalOfferDto, required: true }) + @UseGuards(JwtAuthGuard, IsPersonalOfferOwnerGuard) + public async update( + @Param('id') id: string, + @Body() updatePersonalOfferDto: PersonalOfferDto + ): Promise<PersonalOfferDocument> { + return this.personalOffersService.update(id, updatePersonalOfferDto); + } + + /** + * Delete and return a personal offer. + * @param id - id of the personal offer to delete + * @returns {PersonalOffer} Deleted personal offer + */ + @Delete(':id') + @ApiParam({ name: 'id', type: String, required: true }) + @UseGuards(JwtAuthGuard, IsPersonalOfferOwnerGuard) + public async delete(@Param('id') id: string): Promise<PersonalOfferDocument> { + return this.personalOffersService.delete(id); + } +} diff --git a/src/personal-offers/personal-offers.module.ts b/src/personal-offers/personal-offers.module.ts new file mode 100644 index 000000000..d4b634fd1 --- /dev/null +++ b/src/personal-offers/personal-offers.module.ts @@ -0,0 +1,18 @@ +import { Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { StructuresModule } from '../structures/structures.module'; +import { UsersModule } from '../users/users.module'; +import { PersonalOffersController } from './personal-offers.controller'; +import { PersonalOffersService } from './personal-offers.service'; +import { PersonalOffer, PersonalOfferSchema } from './schemas/personal-offer.schema'; + +@Module({ + imports: [ + MongooseModule.forFeature([{ name: PersonalOffer.name, schema: PersonalOfferSchema }]), + StructuresModule, + UsersModule, + ], + controllers: [PersonalOffersController], + providers: [PersonalOffersService], +}) +export class PersonalOffersModule {} diff --git a/src/personal-offers/personal-offers.service.spec.ts b/src/personal-offers/personal-offers.service.spec.ts new file mode 100644 index 000000000..835635d59 --- /dev/null +++ b/src/personal-offers/personal-offers.service.spec.ts @@ -0,0 +1,111 @@ +import { getModelToken } from '@nestjs/mongoose'; +import { Test, TestingModule } from '@nestjs/testing'; +import { + createPersonalOffersDtoDataMock, + updatePersonalOffersDtoDataMock, + personalOffersDataMock, +} from '../../test/mock/data/personalOffers.mock.data'; +import { PersonalOffersService } from './personal-offers.service'; + +describe('PersonalOffersService', () => { + let service: PersonalOffersService; + + const personalOfferModelMock = { + findById: jest.fn(), + create: jest.fn(), + findByIdAndUpdate: jest.fn(), + findByIdAndDelete: jest.fn(), + exec: jest.fn(), + }; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + PersonalOffersService, + { + provide: getModelToken('PersonalOffer'), + useValue: personalOfferModelMock, + }, + ], + }).compile(); + + service = module.get<PersonalOffersService>(PersonalOffersService); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(service).toBeDefined(); + }); + + describe('findOne', () => { + it('should return personal offer', async () => { + personalOfferModelMock.findById.mockReturnThis(); + personalOfferModelMock.exec.mockResolvedValueOnce(personalOffersDataMock[0]); + expect(await service.findOne('1234ba0e2ab5775cfc01ed3e')).toEqual(personalOffersDataMock[0]); + }); + it('should return exception if personal offer is not found for given id', async () => { + personalOfferModelMock.findById.mockReturnThis(); + personalOfferModelMock.exec.mockResolvedValueOnce(null); + let error: any; + try { + await service.findOne('abcd'); + } catch (e) { + error = e; + } + expect(error.message).toBe('Personal offer does not exist'); + expect(error.status).toBe(404); + }); + }); + + describe('create', () => { + it('should create personal offer', async () => { + personalOfferModelMock.create.mockResolvedValueOnce(personalOffersDataMock[0]); + expect(await service.create(createPersonalOffersDtoDataMock[0])).toEqual(personalOffersDataMock[0]); + }); + }); + + describe('update', () => { + it('should update personal offer', async () => { + personalOfferModelMock.findByIdAndUpdate.mockReturnThis(); + personalOfferModelMock.exec.mockResolvedValueOnce(personalOffersDataMock[1]); + expect(await service.update('2345ba0e2ab5775cfc01ed4d', updatePersonalOffersDtoDataMock[1])).toEqual( + personalOffersDataMock[1] + ); + }); + it('should return exception if personal offer is not found for given id', async () => { + personalOfferModelMock.findById.mockReturnThis(); + personalOfferModelMock.exec.mockResolvedValueOnce(null); + let error: any; + try { + await service.update('abcd', updatePersonalOffersDtoDataMock[1]); + } catch (e) { + error = e; + } + expect(error.message).toBe('Invalid personal offer id for update'); + expect(error.status).toBe(400); + }); + }); + + describe('delete', () => { + it('should update personal offer', async () => { + personalOfferModelMock.findByIdAndDelete.mockReturnThis(); + personalOfferModelMock.exec.mockResolvedValueOnce(personalOffersDataMock[0]); + expect(await service.delete('2345ba0e2ab5775cfc01ed4d')).toEqual(personalOffersDataMock[0]); + }); + it('should return exception if personal offer is not found for given id', async () => { + personalOfferModelMock.findById.mockReturnThis(); + personalOfferModelMock.exec.mockResolvedValueOnce(null); + let error: any; + try { + await service.delete('abcd'); + } catch (e) { + error = e; + } + expect(error.message).toBe('Invalid personal offer id for deletion'); + expect(error.status).toBe(400); + }); + }); +}); diff --git a/src/personal-offers/personal-offers.service.ts b/src/personal-offers/personal-offers.service.ts new file mode 100644 index 000000000..13d00a67e --- /dev/null +++ b/src/personal-offers/personal-offers.service.ts @@ -0,0 +1,41 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model } from 'mongoose'; +import { CreatePersonalOfferDto } from './dto/create-personal-offer.dto'; +import { PersonalOfferDto } from './dto/personal-offer.dto'; +import { PersonalOffer, PersonalOfferDocument } from './schemas/personal-offer.schema'; + +@Injectable() +export class PersonalOffersService { + constructor(@InjectModel(PersonalOffer.name) private personalOfferModel: Model<PersonalOfferDocument>) {} + + public async findOne(id: string): Promise<PersonalOffer> { + const result: PersonalOfferDocument = await this.personalOfferModel.findById(id).exec(); + if (!result) { + throw new HttpException('Personal offer does not exist', HttpStatus.NOT_FOUND); + } + return result; + } + + public async create(createDto: CreatePersonalOfferDto): Promise<PersonalOfferDocument> { + return this.personalOfferModel.create(createDto.personalOffer); + } + + public async update(id: string, updatePersonalOfferDto: PersonalOfferDto): Promise<PersonalOfferDocument> { + const result: PersonalOfferDocument = await this.personalOfferModel + .findByIdAndUpdate(id, updatePersonalOfferDto) + .exec(); + if (!result) { + throw new HttpException('Invalid personal offer id for update', HttpStatus.BAD_REQUEST); + } + return result; + } + + public async delete(id: string): Promise<PersonalOfferDocument> { + const result: PersonalOfferDocument = await this.personalOfferModel.findByIdAndDelete(id).exec(); + if (!result) { + throw new HttpException('Invalid personal offer id for deletion', HttpStatus.BAD_REQUEST); + } + return result; + } +} diff --git a/src/personal-offers/schemas/personal-offer.schema.ts b/src/personal-offers/schemas/personal-offer.schema.ts new file mode 100644 index 000000000..00d985e3f --- /dev/null +++ b/src/personal-offers/schemas/personal-offer.schema.ts @@ -0,0 +1,22 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; + +export type PersonalOfferDocument = PersonalOffer & Document; + +@Schema({ timestamps: true }) +export class PersonalOffer { + @Prop() + proceduresAccompaniment: string[]; + @Prop() + baseSkills: string[]; + @Prop() + accessRight: string[]; + @Prop() + digitalCultureSecurity: string[]; + @Prop() + socialAndProfessional: string[]; + @Prop() + parentingHelp: string[]; +} + +export const PersonalOfferSchema = SchemaFactory.createForClass(PersonalOffer); diff --git a/src/structures/schemas/week.schema.ts b/src/shared/schemas/week.schema.ts similarity index 85% rename from src/structures/schemas/week.schema.ts rename to src/shared/schemas/week.schema.ts index 53c9fed58..44570306b 100644 --- a/src/structures/schemas/week.schema.ts +++ b/src/shared/schemas/week.schema.ts @@ -1,6 +1,6 @@ import { SchemaFactory } from '@nestjs/mongoose'; import { Document } from 'mongoose'; -import { Day } from './day.schema'; +import { Day } from '../../structures/schemas/day.schema'; export type WeekDocument = Week & Document; diff --git a/src/structures/dto/structure.dto.ts b/src/structures/dto/structure.dto.ts index 4299a0cbf..438ca5d52 100644 --- a/src/structures/dto/structure.dto.ts +++ b/src/structures/dto/structure.dto.ts @@ -1,7 +1,8 @@ import { Type } from 'class-transformer'; -import { ArrayNotEmpty, IsNotEmpty, ValidateNested } from 'class-validator'; +import { ArrayNotEmpty, IsArray, IsNotEmpty, ValidateNested } from 'class-validator'; import { Address } from '../schemas/address.schema'; -import { Week } from '../schemas/week.schema'; +import { Week } from '../../shared/schemas/week.schema'; +import { PersonalOfferDocument } from '../../personal-offers/schemas/personal-offer.schema'; export class structureDto { numero: string; @@ -70,4 +71,7 @@ export class structureDto { coord: number[]; accountVerified: boolean; dataShareConsentDate: Date; + + @IsArray() + personalOffers: PersonalOfferDocument[]; } diff --git a/src/structures/schemas/structure.schema.ts b/src/structures/schemas/structure.schema.ts index f3bf40679..1118541d3 100644 --- a/src/structures/schemas/structure.schema.ts +++ b/src/structures/schemas/structure.schema.ts @@ -1,9 +1,10 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Type } from 'class-transformer'; import { ArrayNotEmpty, IsNotEmpty, ValidateNested } from 'class-validator'; -import { Document } from 'mongoose'; +import { Document, Types } from 'mongoose'; import { Address } from './address.schema'; -import { Week } from './week.schema'; +import { Week } from '../../shared/schemas/week.schema'; +import { PersonalOfferDocument } from '../../personal-offers/schemas/personal-offer.schema'; export type StructureDocument = Structure & Document; @@ -52,6 +53,7 @@ export class Structure { this.deletedAt = data.deletedAt; this.accountVerified = data.accountVerified; this.dataShareConsentDate = data.dataShareConsentDate; + this.personalOffers = data.personalOffers; } @Prop() @@ -191,6 +193,9 @@ export class Structure { @Prop() dataShareConsentDate: Date; + + @Prop({ type: [{ type: Types.ObjectId, ref: 'PersonalOffer' }] }) + personalOffers: PersonalOfferDocument[]; } export const StructureSchema = SchemaFactory.createForClass(Structure); diff --git a/src/structures/services/structure.service.spec.ts b/src/structures/services/structure.service.spec.ts index ca6be1066..f20271c7f 100644 --- a/src/structures/services/structure.service.spec.ts +++ b/src/structures/services/structure.service.spec.ts @@ -15,6 +15,9 @@ import { StructuresSearchService } from './structures-search.service'; import { StructuresService } from './structures.service'; import { IUser } from '../../users/interfaces/user.interface'; import * as bcrypt from 'bcrypt'; +import { personalOffersDataMock } from '../../../test/mock/data/personalOffers.mock.data'; +import { PersonalOfferDocument } from '../../personal-offers/schemas/personal-offer.schema'; +import { structuresDocumentDataMock } from '../../../test/mock/data/structures.mock.data'; function hashPassword() { return bcrypt.hashSync(process.env.USER_PWD, process.env.SALT); @@ -29,6 +32,7 @@ describe('StructuresService', () => { findOne: jest.fn(), exec: jest.fn(), find: jest.fn(), + populate: jest.fn(), }; const structuresSearchServiceMock = { @@ -152,6 +156,7 @@ describe('StructuresService', () => { linkedin: null, nbScanners: 1, otherDescription: null, + personalOffers: [], }, ]), }; @@ -258,6 +263,7 @@ describe('StructuresService', () => { describe('should searchForStructures', () => { jest.setTimeout(30000); mockStructureModel.find.mockReturnThis(); + mockStructureModel.populate.mockReturnThis(); mockStructureModel.exec.mockResolvedValue([ { _id: '6903ba0e2ab5775cfc01ed4d', @@ -322,6 +328,7 @@ describe('StructuresService', () => { linkedin: null, nbScanners: 1, otherDescription: null, + personalOffers: [], }, ]); @@ -401,6 +408,7 @@ describe('StructuresService', () => { ], pendingStructuresLink: [], structureOutdatedMailSent: [], + personalOffers: [], name: 'Jacques', surname: 'Dupont', phone: '06 06 06 06 06', @@ -428,7 +436,7 @@ describe('StructuresService', () => { updatedAt: 'Thu Jan 20 2022 10:06:19 GMT+0100 (heure normale d’Europe centrale)', dataShareConsentDate: new Date(), } as StructureDocument); - let res = await service.getAllDataConsentPendingStructures(user); + const res = await service.getAllDataConsentPendingStructures(user); expect(res.length).toBe(1); }); it('should get no consent', async () => { @@ -450,6 +458,7 @@ describe('StructuresService', () => { ], pendingStructuresLink: [], structureOutdatedMailSent: [], + personalOffers: [], name: 'Jacques', surname: 'Dupont', phone: '06 06 06 06 06', @@ -478,7 +487,7 @@ describe('StructuresService', () => { updatedAt: 'Thu Jan 20 2022 10:06:19 GMT+0100 (heure normale d’Europe centrale)', dataShareConsentDate: new Date(), } as StructureDocument); - let res = await service.getAllDataConsentPendingStructures(user); + const res = await service.getAllDataConsentPendingStructures(user); expect(res.length).toBe(0); }); }); @@ -489,4 +498,37 @@ describe('StructuresService', () => { res = service.reportStructureError(null, ''); expect(res).toBeTruthy(); }); + + describe('addPersonalOffer', () => { + const personalOfferDocumentMock: PersonalOfferDocument = personalOffersDataMock[0] as PersonalOfferDocument; + it('should add personal offer to the structure', async () => { + jest.spyOn(service, 'findOne').mockResolvedValue(structuresDocumentDataMock[0]); + const expectedResult = { ...structuresDocumentDataMock[0], personalOffers: [personalOfferDocumentMock] }; + expect(await service.addPersonalOffer('6093ba0e2ab5775cfc01ed3e', personalOfferDocumentMock)).toEqual( + expectedResult + ); + }); + it('should return exception if structure is not found for given id', async () => { + jest.spyOn(service, 'findOne').mockResolvedValue(null); + try { + await service.addPersonalOffer('abcd', personalOfferDocumentMock); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('Structure not found for the personal offer attachment'); + expect(e.status).toBe(400); + } + }); + it('should return exception if personal offer already exists in the structure', async () => { + jest.spyOn(service, 'findOne').mockResolvedValue(structuresDocumentDataMock[1]); + try { + await service.addPersonalOffer('6093ba0e2ab5775cfc01ed3e', personalOfferDocumentMock); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('Personal offer already exist in the structure'); + expect(e.status).toBe(400); + } + }); + }); }); diff --git a/src/structures/services/structures.service.ts b/src/structures/services/structures.service.ts index 4b209af4e..cdc1bd8a4 100644 --- a/src/structures/services/structures.service.ts +++ b/src/structures/services/structures.service.ts @@ -23,6 +23,7 @@ import { CategoriesOthers } from '../../categories/schemas/categoriesOthers.sche import { UnclaimedStructureDto } from '../../admin/dto/unclaimed-structure-dto'; import { depRegex } from '../common/regex'; import { CategoriesFormationsService } from '../../categories/services/categories-formations.service'; +import { PersonalOfferDocument } from '../../personal-offers/schemas/personal-offer.schema'; @Injectable() export class StructuresService { @@ -72,6 +73,7 @@ export class StructuresService { _id: { $in: ids }, $and: [...this.parseFilter(filters), { deletedAt: { $exists: false }, accountVerified: true }], }) + .populate('personalOffers') .exec() ).sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); } else if (filters?.length > 0 && multipleFilters?.length > 0) { @@ -82,6 +84,7 @@ export class StructuresService { $or: [...this.parseFilter(multipleFilters)], $and: [...this.parseFilter(filters), { deletedAt: { $exists: false }, accountVerified: true }], }) + .populate('personalOffers') .exec() ).sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); } else if (filters?.length == 0 && multipleFilters?.length > 0) { @@ -91,6 +94,7 @@ export class StructuresService { _id: { $in: ids }, $or: [...this.parseFilter(multipleFilters), { deletedAt: { $exists: false }, accountVerified: true }], }) + .populate('personalOffers') .exec() ).sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); } else { @@ -100,6 +104,7 @@ export class StructuresService { _id: { $in: ids }, $and: [{ deletedAt: { $exists: false }, accountVerified: true }], }) + .populate('personalOffers') .exec() ).sort((a, b) => ids.indexOf(a.id) - ids.indexOf(b.id)); } @@ -120,6 +125,7 @@ export class StructuresService { address: postition.address, coord: postition.coord, }) + .populate('personalOffers') .exec() ); }); @@ -145,16 +151,19 @@ export class StructuresService { { $text: { $search: searchString }, deletedAt: { $exists: false }, accountVerified: true }, ], }) + .populate('personalOffers') .exec(); } else if (filters) { return this.structureModel .find({ $and: [{ $and: this.parseFilter(filters), deletedAt: { $exists: false }, accountVerified: true }] }) + .populate('personalOffers') .exec(); } else { return this.structureModel .find({ $and: [{ $or: [{ $text: { $search: searchString }, deletedAt: { $exists: false }, accountVerified: true }] }], }) + .populate('personalOffers') .exec(); } } @@ -184,16 +193,23 @@ export class StructuresService { return this.getStructurePosition(structure).then((postition: StructureDocument) => { this.structureModel .findByIdAndUpdate(Types.ObjectId(structure._id), { address: postition.address, coord: postition.coord }) + .populate('personalOffers') .exec(); }); } }) ); - return this.structureModel.find({ deletedAt: { $exists: false }, accountVerified: true }).exec(); + return this.structureModel + .find({ deletedAt: { $exists: false }, accountVerified: true }) + .populate('personalOffers') + .exec(); } public async findAllUnclaimed(): Promise<UnclaimedStructureDto[]> { - const structures = await this.structureModel.find({ deletedAt: { $exists: false } }).exec(); + const structures = await this.structureModel + .find({ deletedAt: { $exists: false } }) + .populate('personalOffers') + .exec(); const unclaimedStructures = []; await Promise.all( structures.map(async (structure: StructureDocument) => { @@ -206,7 +222,10 @@ export class StructuresService { } public async populateES(): Promise<StructureDocument[]> { - const structures = await this.structureModel.find({ deletedAt: { $exists: false } }).exec(); + const structures = await this.structureModel + .find({ deletedAt: { $exists: false } }) + .populate('personalOffers') + .exec(); await Promise.all( structures.map((structure: StructureDocument) => { this.structuresSearchService.indexStructure(structure); @@ -220,7 +239,10 @@ export class StructuresService { accompagnementCategories: CategoriesAccompagnement[], otherCategories: CategoriesOthers[] ): Promise<StructureDocument[]> { - const structures = await this.structureModel.find({ deletedAt: { $exists: false } }).exec(); + const structures = await this.structureModel + .find({ deletedAt: { $exists: false } }) + .populate('personalOffers') + .exec(); // Update structures coord and address before sending them await Promise.all( structures.map((structure: StructureDocument) => { @@ -232,6 +254,7 @@ export class StructuresService { address: postition.address, coord: postition.coord, }) + .populate('personalOffers') .exec(); }); } @@ -241,6 +264,7 @@ export class StructuresService { await this.structureModel .find({ deletedAt: { $exists: false }, accountVerified: true }) .select('-_id -accountVerified -otherDescription') + .populate('personalOffers') .exec() ).map((structure) => { // If structure has temp email, hide it @@ -338,7 +362,7 @@ export class StructuresService { } public async findOne(idParam: string): Promise<StructureDocument> { - return await this.structureModel.findById(Types.ObjectId(idParam)).exec(); + return this.structureModel.findById(Types.ObjectId(idParam)).populate('personalOffers').exec(); } /** * Get structures positions and add marker corresponding to those positons on the map @@ -692,4 +716,30 @@ export class StructuresService { ); return data.filter((value) => value); } + + /** + * Add the personal offer to the structure + * @param structureId string + * @param personalOfferDocument PersonalOfferDocument + * @returns {Structure} structurew with personal offer added + */ + public async addPersonalOffer( + structureId: string, + personalOfferDocument: PersonalOfferDocument + ): Promise<StructureDocument> { + const structure: StructureDocument = await this.findOne(structureId); + if (!structure) { + throw new HttpException('Structure not found for the personal offer attachment', HttpStatus.BAD_REQUEST); + } + if ( + structure.personalOffers && + structure.personalOffers.findIndex((personalOffer) => personalOffer._id === personalOfferDocument._id) > -1 + ) { + throw new HttpException('Personal offer already exist in the structure', HttpStatus.BAD_REQUEST); + } else { + structure.personalOffers.push(personalOfferDocument); + } + await structure.save(); + return structure; + } } diff --git a/src/structures/structures.controller.spec.ts b/src/structures/structures.controller.spec.ts index 459a9dfaf..5cc943105 100644 --- a/src/structures/structures.controller.spec.ts +++ b/src/structures/structures.controller.spec.ts @@ -132,6 +132,7 @@ describe('AuthController', () => { pendingStructuresLink: null, structuresLink: null, structureOutdatedMailSent: null, + personalOffers: null, email: user.email, name: user.name, surname: user.surname, @@ -208,6 +209,7 @@ describe('AuthController', () => { pendingStructuresLink: null, structuresLink: null, structureOutdatedMailSent: null, + personalOffers: null, email: user.email, name: user.name, surname: user.surname, diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts index fd1b6680f..b2f6b6fbb 100644 --- a/src/users/dto/create-user.dto.ts +++ b/src/users/dto/create-user.dto.ts @@ -1,5 +1,6 @@ import { ApiProperty } from '@nestjs/swagger'; import { IsArray, IsEmail, IsNotEmpty, IsOptional, IsString } from 'class-validator'; +import { PersonalOffer } from '../../personal-offers/schemas/personal-offer.schema'; export class CreateUserDto { @ApiProperty({ type: String }) @@ -31,4 +32,8 @@ export class CreateUserDto { @IsArray() @IsOptional() structuresLink?: Array<string>; + + @IsArray() + @IsOptional() + personalOffers?: PersonalOffer[]; } diff --git a/src/users/guards/isPersonalOfferOwner.guard.spec.ts b/src/users/guards/isPersonalOfferOwner.guard.spec.ts new file mode 100644 index 000000000..8c0785048 --- /dev/null +++ b/src/users/guards/isPersonalOfferOwner.guard.spec.ts @@ -0,0 +1,104 @@ +import { Reflector } from '@nestjs/core'; +import { createMock } from '@golevelup/ts-jest'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ExecutionContext } from '@nestjs/common'; +import { UserRole } from '../enum/user-role.enum'; +import { IsPersonalOfferOwnerGuard } from './isPersonalOfferOwner.guard'; + +describe('isPersonalOfferOwner', () => { + let guard: IsPersonalOfferOwnerGuard; + let reflector: Reflector; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [], + providers: [ + IsPersonalOfferOwnerGuard, + { + provide: Reflector, + useValue: { + constructor: jest.fn(), + get: jest.fn(), + }, + }, + ], + }).compile(); + + guard = module.get<IsPersonalOfferOwnerGuard>(IsPersonalOfferOwnerGuard); + reflector = module.get<Reflector>(Reflector); + }); + + afterEach(async () => { + jest.clearAllMocks(); + }); + + it('should be defined', () => { + expect(guard).toBeDefined(); + }); + + it('should return true if structure is in user linked structures', async () => { + const context = createMock<ExecutionContext>({ + getHandler: jest.fn(), + switchToHttp: jest.fn().mockReturnValueOnce({ + getRequest: jest.fn().mockReturnValueOnce({ + user: { + structuresLink: [], + structureOutdatedMailSent: [], + pendingStructuresLink: [], + personalOffers: ['6001a38516b08100062e4161'], + newEmail: null, + changeEmailToken: null, + role: 0, + resetPasswordToken: null, + validationToken: null, + emailVerified: true, + email: 'jp@test.com', + name: 'Jean-Paul', + surname: 'DESCHAMPS', + phone: '06 11 11 11 11', + }, + params: { + id: '6001a38516b08100062e4161', + }, + }), + }), + }); + const result = await guard.canActivate(context); + + expect(result).toBeTruthy(); + }); + + it('should return false if structure is not user linked structures', async () => { + jest.spyOn(reflector, 'get').mockImplementation((a: any, b: any) => []); + const context = createMock<ExecutionContext>({ + getHandler: jest.fn(), + switchToHttp: jest.fn().mockReturnValue({ + getRequest: jest.fn().mockReturnValue({ + user: { + structuresLink: [], + structureOutdatedMailSent: [], + pendingStructuresLink: [], + personalOffers: ['unIdQuiExistePasTropTrop'], + newEmail: null, + changeEmailToken: null, + role: UserRole.user, + resetPasswordToken: null, + validationToken: null, + emailVerified: true, + email: 'jp@test.com', + name: 'Jean-Paul', + surname: 'DESCHAMPS', + phone: '06 11 11 11 11', + }, + params: { + id: '6001a38516b08100062e4161', + }, + }), + }), + }); + const result = await guard.canActivate(context); + + expect(result).toBeFalsy(); + expect(reflector.get).toBeCalled(); + }); +}); diff --git a/src/users/guards/isPersonalOfferOwner.guard.ts b/src/users/guards/isPersonalOfferOwner.guard.ts new file mode 100644 index 000000000..4d73a0377 --- /dev/null +++ b/src/users/guards/isPersonalOfferOwner.guard.ts @@ -0,0 +1,21 @@ +import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'; +import { Reflector } from '@nestjs/core'; +import { User } from '../schemas/user.schema'; +import { RolesGuard } from './roles.guard'; + +@Injectable() +export class IsPersonalOfferOwnerGuard extends RolesGuard implements CanActivate { + constructor(protected readonly reflector: Reflector) { + super(reflector); + } + + canActivate(context: ExecutionContext): boolean { + const req = context.switchToHttp().getRequest(); + const user: User = req.user; + const idPersonalOffer = req.params.id; + if (user.personalOffers && user.personalOffers.includes(idPersonalOffer)) { + return true; + } + return super.canActivate(context); + } +} diff --git a/src/users/guards/isStructureOwner.guard.spec.ts b/src/users/guards/isStructureOwner.guard.spec.ts index a92ddcf4f..2e1bca68f 100644 --- a/src/users/guards/isStructureOwner.guard.spec.ts +++ b/src/users/guards/isStructureOwner.guard.spec.ts @@ -4,7 +4,6 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ExecutionContext } from '@nestjs/common'; import { UserRole } from '../enum/user-role.enum'; import { IsStructureOwnerGuard } from './isStructureOwner.guard'; -import { Types } from 'mongoose'; describe('isStrructureOwner', () => { let guard: IsStructureOwnerGuard; @@ -46,6 +45,7 @@ describe('isStrructureOwner', () => { structuresLink: ['6001a38516b08100062e4161'], structureOutdatedMailSent: [], pendingStructuresLink: [], + personalOffers: [], newEmail: null, changeEmailToken: null, role: 0, @@ -78,6 +78,7 @@ describe('isStrructureOwner', () => { structuresLink: ['unIdQuiExistePasTropTrop'], structureOutdatedMailSent: [], pendingStructuresLink: [], + personalOffers: [], newEmail: null, changeEmailToken: null, role: UserRole.user, diff --git a/src/users/schemas/user.schema.ts b/src/users/schemas/user.schema.ts index 775a93d39..496365f8a 100644 --- a/src/users/schemas/user.schema.ts +++ b/src/users/schemas/user.schema.ts @@ -1,8 +1,10 @@ import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; import { Types } from 'mongoose'; +import { PersonalOfferDocument } from '../../personal-offers/schemas/personal-offer.schema'; import { Employer } from './employer.schema'; import { Job } from './job.schema'; import { UserRole } from '../enum/user-role.enum'; + @Schema() export class User { @Prop({ required: true }) @@ -47,6 +49,9 @@ export class User { @Prop({ default: null }) structureOutdatedMailSent: Types.ObjectId[]; + @Prop({ type: [{ type: Types.ObjectId, ref: 'PersonalOffer' }] }) + personalOffers: PersonalOfferDocument[]; + @Prop({ type: Types.ObjectId, ref: 'Employer' }) employer: Employer; diff --git a/src/users/services/users.service.spec.ts b/src/users/services/users.service.spec.ts index c6f0b08fd..2eaee5ce8 100644 --- a/src/users/services/users.service.spec.ts +++ b/src/users/services/users.service.spec.ts @@ -3,6 +3,10 @@ import { UsersService } from './users.service'; import { getModelToken } from '@nestjs/mongoose'; import { HttpException, HttpStatus } from '@nestjs/common'; import * as bcrypt from 'bcrypt'; + +import { personalOffersDataMock } from '../../../test/mock/data/personalOffers.mock.data'; +import { PersonalOfferDocument } from '../../personal-offers/schemas/personal-offer.schema'; +import { usersMockData } from '../../../test/mock/data/users.mock.data'; import { LoginDto } from '../../auth/login-dto'; import { ConfigurationModule } from '../../configuration/configuration.module'; import { MailerModule } from '../../mailer/mailer.module'; @@ -10,7 +14,7 @@ import { EmailChangeDto } from '../dto/change-email.dto'; import { CreateUserDto } from '../dto/create-user.dto'; import { User } from '../schemas/user.schema'; import { IUser } from '../interfaces/user.interface'; -import { Types } from 'mongoose'; + function hashPassword() { return bcrypt.hashSync(process.env.USER_PWD, process.env.SALT); } @@ -52,6 +56,7 @@ describe('UsersService', () => { structuresLink: [], structureOutdatedMailSent: [], pendingStructuresLink: [], + personalOffers: [], name: 'Jacques', surname: 'Dupont', phone: '06 06 06 06 06', @@ -121,6 +126,7 @@ describe('UsersService', () => { structuresLink: [], pendingStructuresLink: [], structureOutdatedMailSent: [], + personalOffers: [], name: 'Jacques', surname: 'Dupont', phone: '06 06 06 06 06', @@ -193,6 +199,7 @@ describe('UsersService', () => { structuresLink: [], pendingStructuresLink: [], structureOutdatedMailSent: [], + personalOffers: [], name: 'Jacques', surname: 'Dupont', phone: '06 06 06 06 06', @@ -237,6 +244,7 @@ describe('UsersService', () => { structuresLink: [], pendingStructuresLink: [], structureOutdatedMailSent: [], + personalOffers: [], name: 'Jacques', surname: 'Dupont', phone: '06 06 06 06 06', @@ -343,6 +351,7 @@ describe('UsersService', () => { structuresLink: [], pendingStructuresLink: [], structureOutdatedMailSent: [], + personalOffers: [], name: 'Jacques', surname: 'Dupont', phone: '06 06 06 06 06', @@ -363,4 +372,37 @@ describe('UsersService', () => { jest.spyOn(service, 'findAllUnVerified').mockImplementation(async (): Promise<IUser[]> => result); expect((await service.findAllUnVerified()).length).toBe(0); }); + + describe('addPersonalOffer', () => { + const personalOfferDocumentMock: PersonalOfferDocument = personalOffersDataMock[0] as PersonalOfferDocument; + it('should add personal offer to the user', async () => { + jest.spyOn(service, 'findById').mockResolvedValue(usersMockData[0]); + const expectedResult = { ...usersMockData[0], personalOffers: [personalOfferDocumentMock] }; + expect(await service.addPersonalOffer('6036721022462b001334c4bb', personalOfferDocumentMock)).toEqual( + expectedResult + ); + }); + it('should return exception if user is not found for given id', async () => { + jest.spyOn(service, 'findById').mockResolvedValue(null); + try { + await service.addPersonalOffer('abcd', personalOfferDocumentMock); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('User not found for the personal offer attachment'); + expect(e.status).toBe(400); + } + }); + it('should return exception if personal offer already exists in the user', async () => { + jest.spyOn(service, 'findById').mockResolvedValue(usersMockData[1]); + try { + await service.addPersonalOffer('6093ba0e2ab5775cfc01ed3e', personalOfferDocumentMock); + // Fail test if above expression doesn't throw anything. + expect(true).toBe(false); + } catch (e) { + expect(e.message).toBe('Personal offer already exist in the user'); + expect(e.status).toBe(400); + } + }); + }); }); diff --git a/src/users/services/users.service.ts b/src/users/services/users.service.ts index 60c61e764..518f13d88 100644 --- a/src/users/services/users.service.ts +++ b/src/users/services/users.service.ts @@ -14,9 +14,9 @@ import { PendingStructureDto } from '../../admin/dto/pending-structure.dto'; import { OwnerDto } from '../dto/owner.dto'; import { StructureDocument } from '../../structures/schemas/structure.schema'; import { ConfigurationService } from '../../configuration/configuration.service'; -import { ProfileDto } from '../dto/profile.dto'; -import { Employer, EmployerDocument } from '../schemas/employer.schema'; +import { EmployerDocument } from '../schemas/employer.schema'; import { JobDocument } from '../schemas/job.schema'; +import { PersonalOfferDocument } from '../../personal-offers/schemas/personal-offer.schema'; @Injectable() export class UsersService { @@ -54,7 +54,7 @@ export class UsersService { // Send verification email createUser = await this.verifyUserMail(createUser); createUser.save(); - return await this.findOne(createUserDto.email); + return this.findOne(createUserDto.email); } /** @@ -72,11 +72,11 @@ export class UsersService { } private async comparePassword(attempt: string, password: string): Promise<boolean> { - return await bcrypt.compare(attempt, password); + return bcrypt.compare(attempt, password); } private async hashPassword(password: string): Promise<string> { - return await bcrypt.hash(password, process.env.SALT); + return bcrypt.hash(password, process.env.SALT); } public async findOne(mail: string, passwordQuery?: boolean): Promise<IUser | undefined> { @@ -585,7 +585,29 @@ export class UsersService { } /** - * + * Add the personal offer to the user + * @param userId string + * @param personalOfferDocument PersonalOfferDocument + * @returns {IUser} user with personal offer added + */ + public async addPersonalOffer(userId: string, personalOfferDocument: PersonalOfferDocument): Promise<IUser> { + const user: IUser = await this.findById(userId); + if (!user) { + throw new HttpException('User not found for the personal offer attachment', HttpStatus.BAD_REQUEST); + } + if ( + user.personalOffers && + user.personalOffers.findIndex((personalOffer) => personalOffer._id === personalOfferDocument._id) > -1 + ) { + throw new HttpException('Personal offer already exist in the user', HttpStatus.BAD_REQUEST); + } else { + user.personalOffers.push(personalOfferDocument); + } + await user.save(); + return user; + } + + /** * @param profile * @param userId */ diff --git a/test/mock/data/personalOffers.mock.data.ts b/test/mock/data/personalOffers.mock.data.ts new file mode 100644 index 000000000..eefafa12c --- /dev/null +++ b/test/mock/data/personalOffers.mock.data.ts @@ -0,0 +1,81 @@ +import { CreatePersonalOfferDto } from '../../../src/personal-offers/dto/create-personal-offer.dto'; +import { PersonalOfferDto } from '../../../src/personal-offers/dto/personal-offer.dto'; +import { PersonalOffer } from '../../../src/personal-offers/schemas/personal-offer.schema'; + +export const personalOffersDtoDataMock: PersonalOfferDto[] = [ + { + proceduresAccompaniment: [], + baseSkills: [], + accessRight: [], + digitalCultureSecurity: [], + socialAndProfessional: [], + parentingHelp: [], + }, + { + proceduresAccompaniment: ['cpam', 'impots', 'carsat', 'poleEmploi'], + baseSkills: ['260', '1', '11', '38', '48', '74', '77'], + accessRight: ['84', '85', '86', '87', '88', '89', '93', '95'], + digitalCultureSecurity: ['2', '5', '9', '28', '34', '39', '42', '51', '52', '54', '65', '96', '97', '98'], + socialAndProfessional: ['6', '20', '66', '67', '68', '69', '124', '125', '127'], + parentingHelp: ['3', '22', '82', '94'], + }, +]; + +export const createPersonalOffersDtoDataMock: CreatePersonalOfferDto[] = [ + { + structureId: '6093ba0e2ab5775cfc01ed3e', + personalOffer: personalOffersDtoDataMock[0], + }, + { + structureId: '6093ba0e2ab5775cfc01fffe', + personalOffer: personalOffersDtoDataMock[1], + }, + { + structureId: '6093ba0e2ab5775cfc01fffe', + personalOffer: personalOffersDtoDataMock[0], + }, +]; + +export const updatePersonalOffersDtoDataMock: PersonalOfferDto[] = [ + { + proceduresAccompaniment: [], + baseSkills: [], + accessRight: [], + digitalCultureSecurity: [], + socialAndProfessional: [], + parentingHelp: [], + }, + { + proceduresAccompaniment: ['cpam', 'impots', 'carsat', 'poleEmploi'], + baseSkills: ['260', '1', '11', '38', '48', '74', '77'], + accessRight: ['84', '85', '86', '87', '88', '89', '93', '95'], + digitalCultureSecurity: ['2', '5', '9', '28', '34', '39', '42', '51', '52', '54', '65', '96', '97', '98'], + socialAndProfessional: ['6', '20', '66', '67', '68', '69', '124', '125', '127'], + parentingHelp: ['3', '22', '82', '94'], + }, +]; + +export const personalOffersDataMock: PersonalOffer[] = [ + { + _id: '1234ba0e2ab5775cfc01ed3e', + proceduresAccompaniment: [], + baseSkills: [], + accessRight: [], + digitalCultureSecurity: [], + socialAndProfessional: [], + parentingHelp: [], + createdAt: 'Wed Mar 16 2022 14:29:11 GMT+0100 (heure normale d’Europe centrale)', + updatedAt: 'Wed Mar 16 2022 17:29:11 GMT+0100 (heure normale d’Europe centrale)', + } as PersonalOffer, + { + _id: '2345ba0e2ab5775cfc01ed4d', + proceduresAccompaniment: ['cpam', 'impots', 'carsat', 'poleEmploi'], + baseSkills: ['260', '1', '11', '38', '48', '74', '77'], + accessRight: ['84', '85', '86', '87', '88', '89', '93', '95'], + digitalCultureSecurity: ['2', '5', '9', '28', '34', '39', '42', '51', '52', '54', '65', '96', '97', '98'], + socialAndProfessional: ['6', '20', '66', '67', '68', '69', '124', '125', '127'], + parentingHelp: ['3', '22', '82', '94'], + createdAt: 'Wed Mar 16 2022 14:29:11 GMT+0100 (heure normale d’Europe centrale)', + updatedAt: 'Wed Mar 16 2022 17:29:11 GMT+0100 (heure normale d’Europe centrale)', + } as PersonalOffer, +]; diff --git a/test/mock/data/structures.mock.data.ts b/test/mock/data/structures.mock.data.ts new file mode 100644 index 000000000..07f1135ac --- /dev/null +++ b/test/mock/data/structures.mock.data.ts @@ -0,0 +1,168 @@ +import { StructureDocument } from '../../../src/structures/schemas/structure.schema'; + +export const structuresDocumentDataMock: StructureDocument[] = [ + { + _id: '6093ba0e2ab5775cfc01ed3e', + coord: [4.8498155, 45.7514817], + equipmentsAndServices: ['wifiEnAccesLibre'], + digitalCultureSecurity: [], + parentingHelp: [], + socialAndProfessional: [], + accessRight: [], + baseSkills: [], + proceduresAccompaniment: [], + publicsAccompaniment: [], + publics: ['adultes'], + labelsQualifications: [], + accessModality: ['telephoneVisio'], + structureType: null, + structureName: 'a', + description: null, + lockdownActivity: null, + address: { + numero: null, + street: 'Rue Alphonse Daudet', + commune: 'Lyon 7ème Arrondissement', + }, + contactMail: '', + contactPhone: '', + website: '', + facebook: null, + twitter: null, + instagram: null, + linkedin: null, + hours: { + monday: { + open: false, + time: [], + }, + tuesday: { + open: false, + time: [], + }, + wednesday: { + open: false, + time: [], + }, + thursday: { + open: false, + time: [], + }, + friday: { + open: false, + time: [], + }, + saturday: { + open: false, + time: [], + }, + sunday: { + open: false, + time: [], + }, + }, + pmrAccess: false, + exceptionalClosures: null, + otherDescription: null, + nbComputers: 1, + nbPrinters: 1, + nbTablets: 1, + nbNumericTerminal: 1, + nbScanners: 1, + freeWorkShop: false, + accountVerified: true, + personalOffers: [], + createdAt: '2021-05-06T09:42:38.000Z', + updatedAt: '2021-05-06T09:42:50.000Z', + __v: 0, + save: jest.fn(), + } as any, + { + _id: '6093ba0e2ab5775cfc01ed3e', + coord: [4.8498155, 45.7514817], + equipmentsAndServices: ['wifiEnAccesLibre'], + digitalCultureSecurity: [], + parentingHelp: [], + socialAndProfessional: [], + accessRight: [], + baseSkills: [], + proceduresAccompaniment: [], + publicsAccompaniment: [], + publics: ['adultes'], + labelsQualifications: [], + accessModality: ['telephoneVisio'], + structureType: null, + structureName: 'a', + description: null, + lockdownActivity: null, + address: { + numero: null, + street: 'Rue Alphonse Daudet', + commune: 'Lyon 7ème Arrondissement', + }, + contactMail: '', + contactPhone: '', + website: '', + facebook: null, + twitter: null, + instagram: null, + linkedin: null, + hours: { + monday: { + open: false, + time: [], + }, + tuesday: { + open: false, + time: [], + }, + wednesday: { + open: false, + time: [], + }, + thursday: { + open: false, + time: [], + }, + friday: { + open: false, + time: [], + }, + saturday: { + open: false, + time: [], + }, + sunday: { + open: false, + time: [], + }, + }, + pmrAccess: false, + exceptionalClosures: null, + otherDescription: null, + nbComputers: 1, + nbPrinters: 1, + nbTablets: 1, + nbNumericTerminal: 1, + nbScanners: 1, + freeWorkShop: false, + accountVerified: true, + personalOffers: [ + { + _id: '1234ba0e2ab5775cfc01ed3e', + proceduresAccompaniment: [], + baseSkills: [], + accessRight: [], + digitalCultureSecurity: [], + socialAndProfessional: [], + parentingHelp: [], + createdAt: 'Wed Mar 16 2022 14:29:11 GMT+0100 (heure normale d’Europe centrale)', + updatedAt: 'Wed Mar 16 2022 17:29:11 GMT+0100 (heure normale d’Europe centrale)', + }, + ], + createdAt: '2021-05-06T09:42:38.000Z', + updatedAt: '2021-05-06T09:42:50.000Z', + __v: 0, + save: jest.fn(), + } as any, +] as StructureDocument[]; diff --git a/test/mock/data/users.mock.data.ts b/test/mock/data/users.mock.data.ts new file mode 100644 index 000000000..5e1285cd3 --- /dev/null +++ b/test/mock/data/users.mock.data.ts @@ -0,0 +1,58 @@ +import { IUser } from '../../../src/users/interfaces/user.interface'; + +export const usersMockData: IUser[] = [ + { + structureOutdatedMailSent: [], + pendingStructuresLink: ['6001a48e16b08100062e4180', '6093ba0e2ab5775cfc01fffe'], + structuresLink: ['6093ba0e2ab5775cfc01fffe'], + newEmail: null, + changeEmailToken: null, + role: 0, + resetPasswordToken: null, + validationToken: + 'b2b6caca1d38ca26d203b5f12b0d925df2928fab8ee7ccf9bbe78802ffa625f5abce825783bc62d0b11be5a90132cf5045a9a7776f01694c63b60bf64b0f680f', + emailVerified: false, + _id: '6036721022462b001334c4bb', + email: 'a@a.com', + name: 'Xavier', + surname: 'NIEL', + phone: '06 11 11 11 11', + password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu', + personalOffers: [], + __v: 1, + save: jest.fn(), + } as any, + { + structureOutdatedMailSent: [], + pendingStructuresLink: ['6001a48e16b08100062e4180', '6093ba0e2ab5775cfc01fffe'], + structuresLink: ['6093ba0e2ab5775cfc01fffe'], + newEmail: null, + changeEmailToken: null, + role: 0, + resetPasswordToken: null, + validationToken: + 'b2b6caca1d38ca26d203b5f12b0d925df2928fab8ee7ccf9bbe78802ffa625f5abce825783bc62d0b11be5a90132cf5045a9a7776f01694c63b60bf64b0f680f', + emailVerified: false, + _id: '6036721022462b001334c4bb', + email: 'a@a.com', + name: 'Xavier', + surname: 'NIEL', + phone: '06 11 11 11 11', + password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu', + personalOffers: [ + { + _id: '1234ba0e2ab5775cfc01ed3e', + proceduresAccompaniment: [], + baseSkills: [], + accessRight: [], + digitalCultureSecurity: [], + socialAndProfessional: [], + parentingHelp: [], + createdAt: 'Wed Mar 16 2022 14:29:11 GMT+0100 (heure normale d’Europe centrale)', + updatedAt: 'Wed Mar 16 2022 17:29:11 GMT+0100 (heure normale d’Europe centrale)', + }, + ], + __v: 1, + save: jest.fn(), + } as any, +] as IUser[]; diff --git a/test/mock/guards/isPersonalOfferOwner.mock.guard.ts b/test/mock/guards/isPersonalOfferOwner.mock.guard.ts new file mode 100644 index 000000000..be66dd9a5 --- /dev/null +++ b/test/mock/guards/isPersonalOfferOwner.mock.guard.ts @@ -0,0 +1,2 @@ +import { CanActivate } from '@nestjs/common'; +export const mockIsPersonalOfferOwnerGuard: CanActivate = { canActivate: jest.fn(() => true) }; diff --git a/test/mock/services/personalOffers.mock.service.ts b/test/mock/services/personalOffers.mock.service.ts new file mode 100644 index 000000000..a9fd932a5 --- /dev/null +++ b/test/mock/services/personalOffers.mock.service.ts @@ -0,0 +1,47 @@ +import { HttpException, HttpStatus } from '@nestjs/common'; +import { CreatePersonalOfferDto } from '../../../src/personal-offers/dto/create-personal-offer.dto'; +import { PersonalOfferDto } from '../../../src/personal-offers/dto/personal-offer.dto'; +import { createPersonalOffersDtoDataMock, personalOffersDataMock } from '../data/personalOffers.mock.data'; + +export class PersonalOffersServiceMock { + findOne(id: string) { + if (id === '1234ba0e2ab5775cfc01ed3e') { + return personalOffersDataMock[0]; + } + if (id === '2345ba0e2ab5775cfc01ed4d') { + return personalOffersDataMock[1]; + } + throw new HttpException('Personal offer does not exist', HttpStatus.NOT_FOUND); + } + + create(createPersonalOfferDto: CreatePersonalOfferDto) { + if (createPersonalOfferDto === createPersonalOffersDtoDataMock[1]) { + return { + ...createPersonalOfferDto.personalOffer, + _id: 'unIdQuiExisteDeja', + createdAt: 'Wed Mar 16 2022 14:29:11 GMT+0100 (heure normale d’Europe centrale)', + updatedAt: 'Wed Mar 16 2022 17:29:11 GMT+0100 (heure normale d’Europe centrale)', + }; + } + return { + ...createPersonalOfferDto.personalOffer, + _id: '1234ba0e2ab5775cfc01ed3e', + createdAt: 'Wed Mar 16 2022 14:29:11 GMT+0100 (heure normale d’Europe centrale)', + updatedAt: 'Wed Mar 16 2022 17:29:11 GMT+0100 (heure normale d’Europe centrale)', + }; + } + + update(id: string, updateDto: PersonalOfferDto) { + if (id === '2345ba0e2ab5775cfc01ed4d') { + return { ...personalOffersDataMock[1], ...updateDto }; + } + throw new HttpException('Invalid personal offer id for update', HttpStatus.BAD_REQUEST); + } + + delete(id: string) { + if (id === '2345ba0e2ab5775cfc01ed4d') { + return personalOffersDataMock[1]; + } + throw new HttpException('Invalid personal offer id for deletion', HttpStatus.BAD_REQUEST); + } +} diff --git a/test/mock/services/structures.mock.service.ts b/test/mock/services/structures.mock.service.ts index d758744af..8c6379a76 100644 --- a/test/mock/services/structures.mock.service.ts +++ b/test/mock/services/structures.mock.service.ts @@ -1,4 +1,6 @@ import { HttpException, HttpStatus } from '@nestjs/common'; +import { PersonalOffer, PersonalOfferDocument } from '../../../src/personal-offers/schemas/personal-offer.schema'; +import { StructureDocument } from '../../../src/structures/schemas/structure.schema'; export class StructuresServiceMock { findOne(id) { @@ -73,6 +75,7 @@ export class StructuresServiceMock { nbScanners: 1, freeWorkShop: false, accountVerified: true, + personalOffers: [], createdAt: '2021-05-06T09:42:38.000Z', updatedAt: '2021-05-06T09:42:50.000Z', __v: 0, @@ -198,6 +201,7 @@ export class StructuresServiceMock { linkedin: null, nbScanners: 1, otherDescription: null, + personalOffers: [], }; } @@ -324,6 +328,7 @@ export class StructuresServiceMock { linkedin: null, nbScanners: 1, otherDescription: null, + personalOffers: [], }; } @@ -402,6 +407,7 @@ export class StructuresServiceMock { nbScanners: 1, freeWorkShop: false, accountVerified: true, + personalOffers: [], createdAt: '2021-05-06T09:42:38.000Z', updatedAt: '2021-05-06T09:42:50.000Z', __v: 0, @@ -476,6 +482,7 @@ export class StructuresServiceMock { nbScanners: 1, freeWorkShop: false, accountVerified: true, + personalOffers: [], createdAt: '2021-05-06T09:42:38.000Z', updatedAt: '2021-05-06T09:42:50.000Z', __v: 0, @@ -555,6 +562,7 @@ export class StructuresServiceMock { nbScanners: 1, freeWorkShop: false, accountVerified: true, + personalOffers: [], createdAt: '2021-05-06T09:42:38.000Z', updatedAt: '2021-05-06T09:42:50.000Z', __v: 0, @@ -629,6 +637,7 @@ export class StructuresServiceMock { nbScanners: 1, freeWorkShop: false, accountVerified: true, + personalOffers: [], createdAt: '2021-05-06T09:42:38.000Z', updatedAt: '2021-05-06T09:42:50.000Z', __v: 0, @@ -708,6 +717,7 @@ export class StructuresServiceMock { nbScanners: 1, freeWorkShop: false, accountVerified: true, + personalOffers: [], createdAt: '2021-05-06T09:42:38.000Z', updatedAt: '2021-05-06T09:42:50.000Z', __v: 0, @@ -782,6 +792,7 @@ export class StructuresServiceMock { nbScanners: 1, freeWorkShop: false, accountVerified: true, + personalOffers: [], createdAt: '2021-05-06T09:42:38.000Z', updatedAt: '2021-05-06T09:42:50.000Z', __v: 0, @@ -860,6 +871,7 @@ export class StructuresServiceMock { nbScanners: 1, freeWorkShop: false, accountVerified: true, + personalOffers: [], createdAt: '2021-05-06T09:42:38.000Z', updatedAt: '2021-05-06T09:42:50.000Z', __v: 0, @@ -869,4 +881,92 @@ export class StructuresServiceMock { async getAllUserCompletedStructures(users: any[]): Promise<any[]> { return []; } + + async addPersonalOffer( + structureId: string, + personalOfferDocument: PersonalOfferDocument + ): Promise<StructureDocument> { + if (structureId === '6093ba0e2ab5775cfc01ed3e') { + const personalOffer: PersonalOffer = { ...personalOfferDocument } as PersonalOffer; + return { + _id: '6093ba0e2ab5775cfc01ed3e', + coord: [4.8498155, 45.7514817], + digitalCultureSecurity: [], + parentingHelp: [], + socialAndProfessional: [], + accessRight: [], + baseSkills: ['260', '1', '11', '38', '48', '74', '77'], + equipmentsAndServices: ['ordinateurs', 'imprimantes'], + proceduresAccompaniment: [], + publicsAccompaniment: [], + publics: ['adultes'], + labelsQualifications: [], + accessModality: ['telephoneVisio'], + structureType: null, + structureName: 'a', + description: null, + lockdownActivity: null, + address: { + numero: null, + street: 'Rue Alphonse Daudet', + commune: 'Lyon 7ème Arrondissement', + }, + contactMail: '', + contactPhone: '', + website: '', + facebook: null, + twitter: null, + instagram: null, + linkedin: null, + hours: { + monday: { + open: false, + time: [], + }, + tuesday: { + open: false, + time: [], + }, + wednesday: { + open: false, + time: [], + }, + thursday: { + open: false, + time: [], + }, + friday: { + open: false, + time: [], + }, + saturday: { + open: false, + time: [], + }, + sunday: { + open: false, + time: [], + }, + }, + pmrAccess: false, + exceptionalClosures: null, + otherDescription: null, + nbComputers: 1, + nbPrinters: 1, + nbTablets: 1, + nbNumericTerminal: 1, + nbScanners: 1, + freeWorkShop: false, + accountVerified: true, + personalOffers: [personalOffer], + createdAt: '2021-05-06T09:42:38.000Z', + updatedAt: '2021-05-06T09:42:50.000Z', + __v: 0, + } as StructureDocument; + } + if (personalOfferDocument._id === 'unIdQuiExisteDeja') { + throw new HttpException('Personal offer already exist in the structure', HttpStatus.BAD_REQUEST); + } + throw new HttpException('Structure not found for the personal offer attachment', HttpStatus.BAD_REQUEST); + } } diff --git a/test/mock/services/user.mock.service.ts b/test/mock/services/user.mock.service.ts index 5b3fc7d57..e13d72346 100644 --- a/test/mock/services/user.mock.service.ts +++ b/test/mock/services/user.mock.service.ts @@ -1,6 +1,8 @@ import { HttpException, HttpStatus } from '@nestjs/common'; import { PendingStructureDto } from '../../../src/admin/dto/pending-structure.dto'; import { LoginDto } from '../../../src/auth/login-dto'; +import { PersonalOffer, PersonalOfferDocument } from '../../../src/personal-offers/schemas/personal-offer.schema'; +import { IUser } from '../../../src/users/interfaces/user.interface'; import { User } from '../../../src/users/schemas/user.schema'; export class UsersServiceMock { @@ -16,6 +18,7 @@ export class UsersServiceMock { role: 0, name: 'DUPONT', surname: 'Pauline', + personalOffers: [], }; } if (mail === 'jacques.dupont@mii.com') { @@ -28,6 +31,7 @@ export class UsersServiceMock { email: 'jacques.dupont@mii.com', password: '$2a$12$vLQjJ9zAWyUwiFLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu', role: 0, + personalOffers: [], }; } return { @@ -37,6 +41,7 @@ export class UsersServiceMock { emailVerified: false, email: 'jacques.dupont@mii.com', role: 0, + personalOffers: [], }; } return null; @@ -93,6 +98,7 @@ export class UsersServiceMock { name: 'DUPONT', surname: 'Pauline', structuresLink: ['abcdefgh', '18sfqfq'], + personalOffers: [], }; } throw new HttpException('Invalid user id', HttpStatus.BAD_REQUEST); @@ -112,6 +118,7 @@ export class UsersServiceMock { name: 'DUPONT', surname: 'Pauline', structuresLink: ['abcdefgh', '18sfqfq', '6093ba0e2ab5775cfc01fffe'], + personalOffers: [], }); }); } @@ -140,6 +147,7 @@ export class UsersServiceMock { surname: 'NIEL', phone: '06 11 11 11 11', password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu', + personalOffers: [], __v: 1, }, ]; @@ -166,6 +174,7 @@ export class UsersServiceMock { surname: 'NIEL', phone: '06 11 11 11 11', password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu', + personalOffers: [], __v: 1, }, { @@ -184,6 +193,7 @@ export class UsersServiceMock { surname: 'hh', phone: '05 79 87 65 78', password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.3KdcYb.cLqZzkX2rstcpc2QTgbJ0FlC', + personalOffers: [], __v: 3, }, ]; @@ -200,4 +210,34 @@ export class UsersServiceMock { async findAllUnVerified(): Promise<User[]> { return await []; } + + public async addPersonalOffer(userId: string, personalOfferDocument: PersonalOfferDocument): Promise<IUser> { + if (userId === '6036721022462b001334c4bb') { + const personalOffer: PersonalOffer = { ...personalOfferDocument } as PersonalOffer; + return { + structureOutdatedMailSent: [], + pendingStructuresLink: [], + structuresLink: [], + newEmail: null, + changeEmailToken: null, + role: 0, + resetPasswordToken: null, + validationToken: + 'b2b6caca1d38ca26d203b5f12b0d925df2928fab8ee7ccf9bbe78802ffa625f5abce825783bc62d0b11be5a90132cf5045a9a7776f01694c63b60bf64b0f680f', + emailVerified: false, + _id: '6036721022462b001334c4bb', + email: 'a@a.com', + name: 'Xavier', + surname: 'NIEL', + phone: '06 11 11 11 11', + password: '$2a$12$vLQjJ9zAWyUwiXLeQDa6w.XzrlgPBhw.2GWrjog/yuEjIaZnQwmZu', + personalOffers: [personalOffer], + __v: 1, + } as IUser; + } + if (personalOfferDocument._id === 'unIdQuiExisteDeja') { + throw new HttpException('Personal offer already exist in the user', HttpStatus.BAD_REQUEST); + } + throw new HttpException('User not found for the personal offer attachment', HttpStatus.BAD_REQUEST); + } } -- GitLab