diff --git a/src/personal-offers/personal-offers.module.ts b/src/personal-offers/personal-offers.module.ts index d4b634fd1964803aaeba4a8c60d6c6f00d34f608..a86967cf7a37716c14416fb8ab963c8d1fd763eb 100644 --- a/src/personal-offers/personal-offers.module.ts +++ b/src/personal-offers/personal-offers.module.ts @@ -1,4 +1,4 @@ -import { Module } from '@nestjs/common'; +import { forwardRef, Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; import { StructuresModule } from '../structures/structures.module'; import { UsersModule } from '../users/users.module'; @@ -9,10 +9,11 @@ import { PersonalOffer, PersonalOfferSchema } from './schemas/personal-offer.sch @Module({ imports: [ MongooseModule.forFeature([{ name: PersonalOffer.name, schema: PersonalOfferSchema }]), - StructuresModule, + forwardRef(() => StructuresModule), UsersModule, ], controllers: [PersonalOffersController], + exports: [PersonalOffersService], 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 index 835635d5915ba6fab5cc7c3aa56c4af87a887c16..e5e4c885b188f80cc5a1bc61b40d326003be1f79 100644 --- a/src/personal-offers/personal-offers.service.spec.ts +++ b/src/personal-offers/personal-offers.service.spec.ts @@ -1,3 +1,5 @@ +import { StructuresServiceMock } from './../../test/mock/services/structures.mock.service'; +import { StructuresService } from './../structures/services/structures.service'; import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; import { @@ -6,6 +8,8 @@ import { personalOffersDataMock, } from '../../test/mock/data/personalOffers.mock.data'; import { PersonalOffersService } from './personal-offers.service'; +import { UsersService } from '../users/services/users.service'; +import { UsersServiceMock } from '../../test/mock/services/user.mock.service'; describe('PersonalOffersService', () => { let service: PersonalOffersService; @@ -26,6 +30,14 @@ describe('PersonalOffersService', () => { provide: getModelToken('PersonalOffer'), useValue: personalOfferModelMock, }, + { + provide: StructuresService, + useClass: StructuresServiceMock, + }, + { + provide: UsersService, + useClass: UsersServiceMock, + }, ], }).compile(); diff --git a/src/personal-offers/personal-offers.service.ts b/src/personal-offers/personal-offers.service.ts index 13d00a67e1dc9900232c005ed5634ae1a517501e..7df3b4600e17dcdabaa71768006ee107ca2bd004 100644 --- a/src/personal-offers/personal-offers.service.ts +++ b/src/personal-offers/personal-offers.service.ts @@ -1,15 +1,24 @@ -import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { UsersService } from './../users/services/users.service'; +import { HttpException, HttpStatus, Injectable, Logger } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; +import { StructuresService } from '../structures/services/structures.service'; 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>) {} + private readonly logger = new Logger(PersonalOffersService.name); + + constructor( + private structuresService: StructuresService, + private usersService: UsersService, + @InjectModel(PersonalOffer.name) private personalOfferModel: Model<PersonalOfferDocument> + ) {} public async findOne(id: string): Promise<PersonalOffer> { + this.logger.debug(`findOne`); const result: PersonalOfferDocument = await this.personalOfferModel.findById(id).exec(); if (!result) { throw new HttpException('Personal offer does not exist', HttpStatus.NOT_FOUND); @@ -18,10 +27,12 @@ export class PersonalOffersService { } public async create(createDto: CreatePersonalOfferDto): Promise<PersonalOfferDocument> { + this.logger.debug(`create`); return this.personalOfferModel.create(createDto.personalOffer); } public async update(id: string, updatePersonalOfferDto: PersonalOfferDto): Promise<PersonalOfferDocument> { + this.logger.debug(`updating personal offer | ${id}`); const result: PersonalOfferDocument = await this.personalOfferModel .findByIdAndUpdate(id, updatePersonalOfferDto) .exec(); @@ -31,11 +42,20 @@ export class PersonalOffersService { return result; } + /** + * Delete a personal offer and remove it from both user and structure offers + */ public async delete(id: string): Promise<PersonalOfferDocument> { - const result: PersonalOfferDocument = await this.personalOfferModel.findByIdAndDelete(id).exec(); - if (!result) { + this.logger.debug(`delete`); + const structure = await this.structuresService.findByPersonalOfferId(id); + this.structuresService.removePersonalOffer(structure, id); + + const user = await this.usersService.findByPersonalOfferId(id); + this.usersService.removePersonalOffer(user, id); + const deletedOffer: PersonalOfferDocument = await this.personalOfferModel.findByIdAndDelete(id).exec(); + if (!deletedOffer) { throw new HttpException('Invalid personal offer id for deletion', HttpStatus.BAD_REQUEST); } - return result; + return deletedOffer; } } diff --git a/src/structures/services/structures.service.ts b/src/structures/services/structures.service.ts index bf78edcae2240492f5220f3cc2fff92c98771e01..32196dcb07b35377bcb5a39ba340db141d779fe4 100644 --- a/src/structures/services/structures.service.ts +++ b/src/structures/services/structures.service.ts @@ -1071,4 +1071,28 @@ export class StructuresService { const deepClone = Object.assign(oldStructure, { idCNFS }); return this.structureModel.findByIdAndUpdate(Types.ObjectId(idStructure), deepClone).exec(); } + + /** + * Remove a personal offer from structure offers + */ + public async removePersonalOffer(structure: StructureDocument, offerId: string): Promise<PersonalOfferDocument[]> { + if (!structure) { + throw new HttpException('Invalid structure', HttpStatus.NOT_FOUND); + } + structure.personalOffers = structure.personalOffers.filter( + (personalOffer) => !personalOffer._id.equals(Types.ObjectId(offerId)) + ); + await structure.save(); + return structure.personalOffers; + } + + public async findByPersonalOfferId(id: string): Promise<StructureDocument> { + this.logger.debug('findByPersonalOfferId'); + return this.structureModel + .findOne({ + personalOffers: { $all: [Types.ObjectId(id)] }, + }) + .populate('personalOffers') + .exec(); + } } diff --git a/src/structures/structures.controller.spec.ts b/src/structures/structures.controller.spec.ts index 9aea0f72658cda6efbdf18cd482dabf0231604a3..ec56a40844fc2873166300a0012fed19a9d4cd06 100644 --- a/src/structures/structures.controller.spec.ts +++ b/src/structures/structures.controller.spec.ts @@ -1,4 +1,5 @@ import { HttpService } from '@nestjs/axios'; +import { HttpStatus } from '@nestjs/common'; import { Test, TestingModule } from '@nestjs/testing'; import { CategoriesServiceMock } from '../../test/mock/services/categories.mock.service'; import { HttpServiceMock } from '../../test/mock/services/http.mock.service'; @@ -6,13 +7,18 @@ import { StructuresServiceMock } from '../../test/mock/services/structures.mock. import { TempUserServiceMock } from '../../test/mock/services/tempUser.mock.service'; import { UsersServiceMock } from '../../test/mock/services/user.mock.service'; import { CategoriesService } from '../categories/services/categories.service'; +import { PersonalOffersService } from '../personal-offers/personal-offers.service'; import { TempUserService } from '../temp-user/temp-user.service'; import { UsersService } from '../users/services/users.service'; +import { PersonalOffersServiceMock } from './../../test/mock/services/personalOffers.mock.service'; +import { PersonalOffer, PersonalOfferDocument } from './../personal-offers/schemas/personal-offer.schema'; import { CreateStructureDto } from './dto/create-structure.dto'; import { StructuresService } from './services/structures.service'; import { StructuresController } from './structures.controller'; describe('AuthController', () => { let controller: StructuresController; + let usersService: UsersService; + let personalOffersService: PersonalOffersService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -31,6 +37,10 @@ describe('AuthController', () => { provide: UsersService, useClass: UsersServiceMock, }, + { + provide: PersonalOffersService, + useClass: PersonalOffersServiceMock, + }, { provide: TempUserService, useClass: TempUserServiceMock, @@ -41,8 +51,9 @@ describe('AuthController', () => { }, ], }).compile(); - controller = module.get<StructuresController>(StructuresController); + usersService = module.get<UsersService>(UsersService); + personalOffersService = module.get<PersonalOffersService>(PersonalOffersService); }); it('should be defined', () => { @@ -182,16 +193,55 @@ describe('AuthController', () => { expect(res).toBeTruthy(); }); - it('should remove Owner', async () => { - let res = controller.removeOwner('6093ba0e2ab5775cfc01ed3e', 'tsfsf6296'); - expect(res).toBeTruthy(); - res = controller.removeOwner('6093ba0e2ab5775cfc01ed3e', '1'); - expect(res).toBeTruthy(); - res = controller.removeOwner('', '1'); - expect(res).toBeTruthy(); + describe('removeOwner', () => { + it('should return invalid structure', async () => { + try { + await controller.removeOwner('invalidstructure', '63639058685ba134c32bc495'); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toEqual('Invalid Structure'); + expect(e.status).toEqual(HttpStatus.NOT_FOUND); + } + }); + it('should return invalid user', async () => { + try { + await controller.removeOwner('6093ba0e2ab5775cfc01ed3e', 'invaliduser'); + expect(true).toBe(false); + } catch (e) { + expect(e.message).toEqual('Invalid User'); + expect(e.status).toEqual(HttpStatus.NOT_FOUND); + } + }); + it('should remove structure link without offer', async () => { + jest.spyOn(usersService, 'getPersonalOfferInStructure').mockImplementationOnce(() => null); + const removeFromStructureLinkedSpyer = jest.spyOn(usersService, 'removeFromStructureLinked'); + const deleteOfferSpyer = jest.spyOn(personalOffersService, 'delete'); + await controller.removeOwner('6093ba0e2ab5775cfc01ed3e', '63639058685ba134c32bc495'); + expect(removeFromStructureLinkedSpyer).toBeCalledTimes(1); + expect(deleteOfferSpyer).toBeCalledTimes(0); + }); + + it('should remove structure link with offer', async () => { + const result = ({ + _id: '2345ba0e2ab5775cfc01ed4d', + categories: { + onlineProcedures: ['caf', 'cpam'], + baseSkills: [], + advancedSkills: [], + }, + 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) as PersonalOfferDocument; + jest.spyOn(usersService, 'getPersonalOfferInStructure').mockImplementationOnce(() => result); + const removeFromStructureLinkedSpyer = jest.spyOn(usersService, 'removeFromStructureLinked'); + const deleteOfferSpyer = jest.spyOn(personalOffersService, 'delete'); + await controller.removeOwner('6093ba0e2ab5775cfc01ed3e', '63639058685ba134c32bc495'); + expect(removeFromStructureLinkedSpyer).toBeCalledTimes(1); + expect(deleteOfferSpyer).toBeCalledTimes(1); + }); }); - it('should remove Owner', async () => { + it('should remove temp user', async () => { const res = controller.removeTempUser('6093ba0e2ab5775cfc01ed3e', 'tsfsf6296'); expect(res).toBeTruthy(); }); diff --git a/src/structures/structures.controller.ts b/src/structures/structures.controller.ts index 4362ca80d15010f2904e705cbcde76565f16bf20..b1b72e6420b806d80d4ad4a5e1e8fae7fc27afa7 100644 --- a/src/structures/structures.controller.ts +++ b/src/structures/structures.controller.ts @@ -21,20 +21,21 @@ import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { CategoriesService } from '../categories/services/categories.service'; import { CreateTempUserDto } from '../temp-user/dto/create-temp-user.dto'; import { ITempUser } from '../temp-user/temp-user.interface'; +import { TempUser } from '../temp-user/temp-user.schema'; import { TempUserService } from '../temp-user/temp-user.service'; import { Roles } from '../users/decorators/roles.decorator'; import { IsStructureOwnerGuard } from '../users/guards/isStructureOwner.guard'; import { RolesGuard } from '../users/guards/roles.guard'; -import { User } from '../users/schemas/user.schema'; import { IUser } from '../users/interfaces/user.interface'; +import { User } from '../users/schemas/user.schema'; import { UsersService } from '../users/services/users.service'; +import { PersonalOffersService } from './../personal-offers/personal-offers.service'; import { depRegex } from './common/regex'; import { CreateStructureDto } from './dto/create-structure.dto'; import { QueryStructure } from './dto/query-structure.dto'; import { UpdateStructureDto } from './dto/update-structure.dto'; import { Structure, StructureDocument } from './schemas/structure.schema'; import { StructuresService } from './services/structures.service'; -import { TempUser } from '../temp-user/temp-user.schema'; @ApiTags('structures') @Controller('structures') @@ -45,7 +46,8 @@ export class StructuresController { private readonly structureService: StructuresService, private readonly userService: UsersService, private readonly tempUserService: TempUserService, - private readonly categoriesService: CategoriesService + private readonly categoriesService: CategoriesService, + private readonly personalOffersService: PersonalOffersService ) {} /** @@ -240,6 +242,7 @@ export class StructuresController { @ApiParam({ name: 'id', type: String, required: true }) @ApiParam({ name: 'userId', type: String, required: true }) public async removeOwner(@Param('id') id: string, @Param('userId') userId: string): Promise<void> { + this.logger.debug(`removeOwner`); // Get structure const structure = await this.structureService.findOne(id); if (!structure) { @@ -247,10 +250,16 @@ export class StructuresController { } // Get user const userFromDb = await this.userService.findById(userId); - if (!userFromDb || !userFromDb.structuresLink.includes(Types.ObjectId(id))) { + if (!userFromDb || !userFromDb.structuresLink.some((structure) => structure.equals(id))) { throw new HttpException('Invalid User', HttpStatus.NOT_FOUND); } - this.userService.removeFromStructureLinked(userFromDb, id); + await this.userService.removeFromStructureLinked(userFromDb, id); + + // If user has personal offer, delete it and remove it from both user and structure offers + const personalOffer = this.userService.getPersonalOfferInStructure(userFromDb, structure); + if (personalOffer) { + this.personalOffersService.delete(personalOffer._id); + } } @Delete(':id/tempUser/:userId') diff --git a/src/structures/structures.module.ts b/src/structures/structures.module.ts index 9b1ad89acf612f7a17e79caa33354c38183ac143..b15f1e65feb5220d43827a59a6835ac40ef3354b 100644 --- a/src/structures/structures.module.ts +++ b/src/structures/structures.module.ts @@ -1,21 +1,22 @@ -import { forwardRef, Module } from '@nestjs/common'; import { HttpModule } from '@nestjs/axios'; +import { forwardRef, Module } from '@nestjs/common'; import { MongooseModule } from '@nestjs/mongoose'; -import { TempUserModule } from '../temp-user/temp-user.module'; +import { CategoriesModule } from '../categories/categories.module'; import { MailerModule } from '../mailer/mailer.module'; +import { ParametersService } from '../parameters/parameters.service'; +import { Parameters, ParametersSchema } from '../parameters/schemas/parameters.schema'; +import { PersonalOffersModule } from '../personal-offers/personal-offers.module'; +import { SearchModule } from '../search/search.module'; +import { TempUserModule } from '../temp-user/temp-user.module'; import { UsersModule } from '../users/users.module'; import { Structure, StructureSchema } from './schemas/structure.schema'; -import { StructuresController } from './structures.controller'; -import { StructuresService } from './services/structures.service'; import { ApticStructuresService } from './services/aptic-structures.service'; +import { StructuresSearchService } from './services/structures-search.service'; +import { StructuresService } from './services/structures.service'; import { StructureTypeController } from './structure-type/structure-type.controller'; -import { StructureTypeService } from './structure-type/structure-type.service'; import { StructureType, StructureTypeSchema } from './structure-type/structure-type.schema'; -import { CategoriesModule } from '../categories/categories.module'; -import { StructuresSearchService } from './services/structures-search.service'; -import { SearchModule } from '../search/search.module'; -import { ParametersService } from '../parameters/parameters.service'; -import { Parameters, ParametersSchema } from '../parameters/schemas/parameters.schema'; +import { StructureTypeService } from './structure-type/structure-type.service'; +import { StructuresController } from './structures.controller'; @Module({ imports: [ @@ -27,6 +28,7 @@ import { Parameters, ParametersSchema } from '../parameters/schemas/parameters.s HttpModule, MailerModule, forwardRef(() => UsersModule), + forwardRef(() => PersonalOffersModule), CategoriesModule, TempUserModule, SearchModule, diff --git a/src/users/services/users.service.ts b/src/users/services/users.service.ts index 9fe7e330b92a70b99c86a93139de7a2e5e01db5a..011c593c7c18e59d01e34ff22b71fe17c67302dd 100644 --- a/src/users/services/users.service.ts +++ b/src/users/services/users.service.ts @@ -3,13 +3,14 @@ import { InjectModel } from '@nestjs/mongoose'; import * as bcrypt from 'bcrypt'; import * as crypto from 'crypto'; import * as ejs from 'ejs'; +import { DateTime } from 'luxon'; import { Model, Types } from 'mongoose'; import { PendingStructureDto } from '../../admin/dto/pending-structure.dto'; import { LoginDto } from '../../auth/login-dto'; import { ConfigurationService } from '../../configuration/configuration.service'; import { MailerService } from '../../mailer/mailer.service'; import { PersonalOfferDocument } from '../../personal-offers/schemas/personal-offer.schema'; -import { StructureDocument } from '../../structures/schemas/structure.schema'; +import { Structure, StructureDocument } from '../../structures/schemas/structure.schema'; import { EmailChangeDto } from '../dto/change-email.dto'; import { CreateUserDto } from '../dto/create-user.dto'; import { DescriptionDto } from '../dto/description.dto'; @@ -20,7 +21,6 @@ import { EmployerDocument } from '../schemas/employer.schema'; import { JobDocument } from '../schemas/job.schema'; import { User } from '../schemas/user.schema'; import { UserRegistrySearchService } from './userRegistry-search.service'; -import { DateTime } from 'luxon'; @Injectable() export class UsersService { @@ -701,6 +701,20 @@ export class UsersService { return user; } + /** + * Remove a personal offer from user offers + */ + public async removePersonalOffer(user: IUser, offerId: string): Promise<PersonalOfferDocument[]> { + if (!user) { + throw new HttpException('Invalid user', HttpStatus.NOT_FOUND); + } + user.personalOffers = user.personalOffers.filter( + (personalOffer) => !personalOffer._id.equals(Types.ObjectId(offerId)) + ); + await user.save(); + return user.personalOffers; + } + /** * * @param profile @@ -776,4 +790,22 @@ export class UsersService { this.userRegistrySearchService.update(populatedResult); return result; } + + public getPersonalOfferInStructure(user: User, structure: Structure): PersonalOfferDocument { + if (!user.job.hasPersonalOffer || user.personalOffers.length === 0) return null; + // Check if structure has personal offers + if (structure.personalOffers.length === 0) return null; + // Return personal offer if the user has one in this structure + return structure.personalOffers.filter((structureOffer) => user.personalOffers.includes(structureOffer._id))[0]; + } + + public async findByPersonalOfferId(id: string): Promise<IUser> { + this.logger.debug('findByPersonalOfferId'); + return this.userModel + .findOne({ + personalOffers: { $all: [Types.ObjectId(id)] }, + }) + .populate('personalOffers') + .exec(); + } } diff --git a/test/mock/services/structures.mock.service.ts b/test/mock/services/structures.mock.service.ts index 4f33cf189593f15de56c77f8f6a9a652460e7175..f1d2fbf55bc9c3424f2fc1100f827f27654c0b90 100644 --- a/test/mock/services/structures.mock.service.ts +++ b/test/mock/services/structures.mock.service.ts @@ -1229,6 +1229,14 @@ export class StructuresServiceMock { async bindCNFSids() { return `2 structures affected`; } + + async removePersonalOffer() { + return []; + } + + async findByPersonalOfferId() { + return {}; + } } export const mockCNFSStructures: Array<CNFSStructure> = [ diff --git a/test/mock/services/user.mock.service.ts b/test/mock/services/user.mock.service.ts index ffc83aa89f4401682d4d007fdf090c0013f74b67..fe40d8bad5c15ec138b671ed76f172faebddcd62 100644 --- a/test/mock/services/user.mock.service.ts +++ b/test/mock/services/user.mock.service.ts @@ -1,10 +1,12 @@ import { HttpException, HttpStatus } from '@nestjs/common'; -import { isValidObjectId } from 'mongoose'; +import { isValidObjectId, Types } from 'mongoose'; 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 { Structure } from '../../../src/structures/schemas/structure.schema'; import { IUser } from '../../../src/users/interfaces/user.interface'; import { User } from '../../../src/users/schemas/user.schema'; +import { personalOffersDataMock } from '../data/personalOffers.mock.data'; export class UsersServiceMock { findOne(mail: string, passwordQuery?: boolean) { @@ -80,6 +82,7 @@ export class UsersServiceMock { name: 'DUPONT', surname: 'Pauline', personalOffers: [], + structuresLink: [Types.ObjectId('6093ba0e2ab5775cfc01ed3e')], }; } else { return null; @@ -312,4 +315,21 @@ export class UsersServiceMock { async updateUserEmployer() { return await []; } + + async removeFromStructureLinked(): Promise<Types.ObjectId[]> { + return []; + } + + async removePersonalOffer() { + return []; + } + + async findByPersonalOfferId() { + return {}; + } + + getPersonalOfferInStructure(user: User, _structure: Structure): PersonalOfferDocument { + if (user.job?.hasPersonalOffer) return personalOffersDataMock[0] as PersonalOfferDocument; + return null; + } }