diff --git a/src/structures/structures.controller.spec.ts b/src/structures/structures.controller.spec.ts index 385004e01324d882674365a49ce695eae8b3e203..5e91312a72a4dade54b9584738db0441ea7f106a 100644 --- a/src/structures/structures.controller.spec.ts +++ b/src/structures/structures.controller.spec.ts @@ -25,6 +25,7 @@ import { Structure } from './schemas/structure.schema'; import { StructuresExportService } from './services/structures-export.service'; import { StructuresService } from './services/structures.service'; import { StructuresController } from './structures.controller'; +import { EmployerDocument } from '../users/schemas/employer.schema'; describe('StructuresController', () => { let structuresController: StructuresController; @@ -194,9 +195,10 @@ describe('StructuresController', () => { validationToken: null, role: null, employer: { + _id: '5436456', name: 'test', validated: true, - }, + } as EmployerDocument, job: { name: 'test', validated: true, diff --git a/src/users/controllers/userRegistry.controller.spec.ts b/src/users/controllers/userRegistry.controller.spec.ts index 245b3ea674e523b39e5f90793d4972a962fa0735..845d4d2339ee0d85abc691ef7b23e96d8782ee74 100644 --- a/src/users/controllers/userRegistry.controller.spec.ts +++ b/src/users/controllers/userRegistry.controller.spec.ts @@ -19,6 +19,7 @@ describe('UserRegistryController', () => { findUsersByNameEmployerOrJobsGroup: jest.fn(), initUserRegistryIndex: jest.fn(), populateES: jest.fn(), + findCommunesWithUsers: jest.fn(), }; beforeEach(async () => { @@ -111,7 +112,44 @@ describe('UserRegistryController', () => { const reply = await userRegistryController.findAll({ search: '' }); expect(reply).toBe(multipleUsers); }); + + it('should findAll with territory filter', async () => { + userRegistryServiceMock.findUsersByNameEmployerOrJobsGroup.mockResolvedValue([multipleUsers[0]]); + const reply = await userRegistryController.findAll({ search: '' }, { page: 1, territory: ['SomeTerritory'] }); + expect(reply).toStrictEqual([multipleUsers[0]]); + }); + + it('should findAll with commune filter', async () => { + userRegistryServiceMock.findUsersByNameEmployerOrJobsGroup.mockResolvedValue([multipleUsers[0]]); + const reply = await userRegistryController.findAll({ search: '' }, { page: 1, commune: ['SomeCommune'] }); + expect(reply).toStrictEqual([multipleUsers[0]]); + }); + + it('should findAll with all filters including territory and commune', async () => { + userRegistryServiceMock.findUsersByNameEmployerOrJobsGroup.mockResolvedValue([multipleUsers[0]]); + const reply = await userRegistryController.findAll( + { search: 'adm' }, + { + page: 1, + jobsGroup: ['Technique'], + employer: ['Pimms'], + territory: ['SomeTerritory'], + commune: ['SomeCommune'], + } + ); + expect(reply).toStrictEqual([multipleUsers[0]]); + }); }); + + describe('communes', () => { + it('should return communes with users', async () => { + const mockCommunes = [{ commune: 'SomeCommune', inseeCode: '12345' }]; + userRegistryServiceMock.findCommunesWithUsers.mockResolvedValue(mockCommunes); + const reply = await userRegistryController.communes(); + expect(reply).toStrictEqual(mockCommunes); + }); + }); + describe('findAllCount', () => { it('should findAllCount', async () => { userRegistryServiceMock.countAllUserRegistry.mockResolvedValue(10); diff --git a/src/users/controllers/userRegistry.controller.ts b/src/users/controllers/userRegistry.controller.ts index 3980b1e7173ce5a83b147680770af61751f16ad3..8504149fcd193f12d174275fecaa8b975fdf261f 100644 --- a/src/users/controllers/userRegistry.controller.ts +++ b/src/users/controllers/userRegistry.controller.ts @@ -21,14 +21,17 @@ export class UsersRegistryController { @ApiBody({ required: false }) public async findAll( @Query() query?: { search: string }, - @Body() filters?: { jobsGroup?: string[]; employer?: string[]; page: number } + @Body() + filters?: { jobsGroup?: string[]; employer?: string[]; territory?: string[]; commune?: string[]; page: number } ): Promise<UserRegistryPaginatedResponse> { this.logger.debug(`findAll with query '${query.search}' and ${JSON.stringify(filters)}`); return this.userRegistryService.findUsersByNameEmployerOrJobsGroup( - query.search || '', + query?.search || '', filters?.page || 1, filters?.jobsGroup || [], - filters?.employer || [] + filters?.employer || [], + filters?.territory || [], + filters?.commune || [] ); } @@ -51,4 +54,12 @@ export class UsersRegistryController { this.logger.debug('reset ES UserRegistry'); return await this.userRegistryService.initUserRegistryIndex(); } + + @Get('communesWithUsers') + @UseGuards(JwtAuthGuard) + @ApiBearerAuth('JWT') + public async communes() { + this.logger.debug(`Get all communes that have structures with users`); + return this.userRegistryService.findCommunesWithUsers(); + } } diff --git a/src/users/schemas/user.schema.ts b/src/users/schemas/user.schema.ts index 02d48470d01817d33f03fe22e368a731755d7da4..5a1f9da378d8cd90a6925cb7fa2f3fd57143eace 100644 --- a/src/users/schemas/user.schema.ts +++ b/src/users/schemas/user.schema.ts @@ -3,7 +3,7 @@ import { Types } from 'mongoose'; import { PersonalOfferDocument } from '../../personal-offers/schemas/personal-offer.schema'; import { UserRole } from '../enum/user-role.enum'; import { pendingStructuresLink } from '../interfaces/pendingStructure'; -import { Employer } from './employer.schema'; +import { EmployerDocument } from './employer.schema'; import { JobDocument } from './job.schema'; @Schema({ timestamps: true }) @@ -60,7 +60,7 @@ export class User { personalOffers: PersonalOfferDocument[]; @Prop({ type: Types.ObjectId, ref: 'Employer' }) - employer?: Employer; + employer?: EmployerDocument; @Prop({ type: Types.ObjectId, ref: 'Job' }) job?: JobDocument; diff --git a/src/users/services/employer.service.spec.ts b/src/users/services/employer.service.spec.ts index 80d2d1f2cf56fad3593d5cf97a971f8cd68aaade..b50289191dc3fc82d18a69f740d796345d55f121 100644 --- a/src/users/services/employer.service.spec.ts +++ b/src/users/services/employer.service.spec.ts @@ -42,6 +42,7 @@ describe('EmployerService', () => { deleteOne: jest.fn(), find: jest.fn(() => mockEmployerModel), sort: jest.fn(() => mockEmployerModel), + collation: jest.fn().mockReturnThis(), exec: jest.fn(), }; const mockUserService = { diff --git a/src/users/services/employer.service.ts b/src/users/services/employer.service.ts index 9f45bcc67595920f813aefca5362bfdaf1b7132f..b4072ddcc87d3da7250e215a2ead9deacc1c607a 100644 --- a/src/users/services/employer.service.ts +++ b/src/users/services/employer.service.ts @@ -24,7 +24,11 @@ export class EmployerService { public async findAll(): Promise<Employer[]> { this.logger.debug('findAll'); - return this.employerModel.find({ validated: true }).sort({ name: 1 }).exec(); + return this.employerModel + .find({ validated: true }) + .collation({ locale: 'fr', strength: 2 }) + .sort({ name: 1 }) + .exec(); } public async findOne(idParam: string): Promise<EmployerDocument> { diff --git a/src/users/services/userRegistry-search.service.spec.ts b/src/users/services/userRegistry-search.service.spec.ts index fd76d2a8769b469b147450aa3998e90530756f3c..2c34fcc6af5865aa61acd08ad46837c8ef5c4c39 100644 --- a/src/users/services/userRegistry-search.service.spec.ts +++ b/src/users/services/userRegistry-search.service.spec.ts @@ -8,6 +8,7 @@ import { UserRegistrySearchService } from './userRegistry-search.service'; describe('UserRegistrySearchService Search cases', () => { let userRegistrySearchService: UserRegistrySearchService; + let elasticsearchService: ElasticsearchService; beforeEach(async () => { const module = await Test.createTestingModule({ @@ -16,52 +17,76 @@ describe('UserRegistrySearchService Search cases', () => { }).compile(); userRegistrySearchService = module.get<UserRegistrySearchService>(UserRegistrySearchService); + elasticsearchService = module.get<ElasticsearchService>(ElasticsearchService); userRegistrySearchService['index'] = 'user-unit-test'; + + // Mock Elasticsearch methods + jest.spyOn(elasticsearchService.indices, 'exists').mockResolvedValue(true); + jest.spyOn(elasticsearchService.indices, 'delete').mockResolvedValue({} as any); + jest.spyOn(elasticsearchService.indices, 'create').mockResolvedValue({} as any); + jest.spyOn(elasticsearchService, 'index').mockResolvedValue({} as any); + jest.spyOn(elasticsearchService.indices, 'refresh').mockResolvedValue({} as any); + jest.spyOn(elasticsearchService, 'search').mockResolvedValue({ hits: { hits: [], max_score: 1 } } as any); + jest.spyOn(elasticsearchService, 'update').mockResolvedValue({} as any); + jest.spyOn(elasticsearchService, 'delete').mockResolvedValue({} as any); + // Init test cases await userRegistrySearchService.dropIndex(); await userRegistrySearchService.createUserRegistryIndex(); await Promise.all(multipleUsers.map((user) => userRegistrySearchService.indexUserRegistry(user))); - - // wait for the new structures to be indexed before search await userRegistrySearchService.refreshIndexUserRegistry(); - await new Promise((r) => setTimeout(r, 2000)); - }, 10000); + }); - it('should be defined', async () => { + it('should be defined', () => { expect(userRegistrySearchService).toBeDefined(); }); + describe('Search method', () => { - it('should find Guilhem', async () => { - const res = await userRegistrySearchService.search('Guilhem'); - expect(res[0].surname).toBe('Guilhem'); - expect(res.length).toBe(1); - }); - it('should find adm', async () => { - const res = await userRegistrySearchService.search('adm'); - expect(res[0].name).toBe('Admin'); - expect(res.length).toBe(1); - }); - it('should find empty string', async () => { - const res = await userRegistrySearchService.search(''); - expect(res.length).toBe(6); + it('should search for users', async () => { + const mockHits = multipleUsers.map((user) => ({ _source: user, _score: 1 })); + jest + .spyOn(elasticsearchService, 'search') + .mockResolvedValueOnce({ hits: { hits: mockHits, max_score: 1 } } as any); + + const res = await userRegistrySearchService.search('test'); + expect(res.length).toBe(multipleUsers.length); + expect(elasticsearchService.search).toHaveBeenCalled(); }); }); + describe('Indexation methods', () => { - it('should index User', async () => { - const res = await Promise.all(multipleUsers.map((user) => userRegistrySearchService.indexUserRegistry(user))); - expect(res).toBeTruthy(); - expect(res.length).toBe(6); + it('should create user registry index', async () => { + await userRegistrySearchService.createUserRegistryIndex(); + expect(elasticsearchService.indices.create).toHaveBeenCalled(); }); - it('should update index', async () => { - const res = await userRegistrySearchService.update(multipleUsers[0] as IUserRegistry); - expect(res).toBeTruthy(); + + it('should drop index', async () => { + await userRegistrySearchService.dropIndex(); + expect(elasticsearchService.indices.exists).toHaveBeenCalled(); + expect(elasticsearchService.indices.delete).toHaveBeenCalled(); + }); + + it('should index user', async () => { + const user = multipleUsers[0] as IUserRegistry; + await userRegistrySearchService.indexUserRegistry(user); + expect(elasticsearchService.index).toHaveBeenCalled(); + }); + + it('should update user in index', async () => { + const user = multipleUsers[0] as IUserRegistry; + await userRegistrySearchService.update(user); + expect(elasticsearchService.update).toHaveBeenCalled(); + }); + + it('should delete user from index', async () => { + const user = multipleUsers[0] as IUserRegistry; + await userRegistrySearchService.deleteIndex(user); + expect(elasticsearchService.delete).toHaveBeenCalled(); }); - it('should delete index', async () => { - const mockDelete = jest.fn(); - jest.spyOn(ElasticsearchService.prototype, 'delete').mockImplementation(mockDelete); - const resAdm = await userRegistrySearchService.search('adm'); - await userRegistrySearchService.deleteIndex(resAdm[0] as IUserRegistry); - expect(mockDelete).toHaveBeenCalled(); + + it('should refresh index', async () => { + await userRegistrySearchService.refreshIndexUserRegistry(); + expect(elasticsearchService.indices.refresh).toHaveBeenCalled(); }); }); }); diff --git a/src/users/services/userRegistry.service.spec.ts b/src/users/services/userRegistry.service.spec.ts index 2a15b5305ccb17bc69f6ea25e4db5bcf1419e255..1d39741a07dbe47d9a01e616e082474724899f83 100644 --- a/src/users/services/userRegistry.service.spec.ts +++ b/src/users/services/userRegistry.service.spec.ts @@ -9,6 +9,7 @@ import { EmployerService } from './employer.service'; import { JobsGroupsService } from './jobsGroups.service'; import { UserRegistrySearchService } from './userRegistry-search.service'; import { UserRegistryService } from './userRegistry.service'; +import { StructuresService } from '../../structures/services/structures.service'; describe('userRegistryService', () => { let userRegistryService: UserRegistryService; @@ -38,6 +39,9 @@ describe('userRegistryService', () => { const mockJobsGroupsService = { findByName: jest.fn(), }; + const mockStructuresService = { + findOne: jest.fn(), + }; beforeEach(async () => { const module = await Test.createTestingModule({ @@ -60,6 +64,10 @@ describe('userRegistryService', () => { provide: EmployerService, useValue: mockEmployersService, }, + { + provide: StructuresService, + useValue: mockStructuresService, + }, ], }).compile(); userRegistryService = module.get<UserRegistryService>(UserRegistryService); @@ -98,131 +106,118 @@ describe('userRegistryService', () => { it('should findAll UserRegistry with page number 1', async () => { const res = { count: 1, docs: result }; mockUserRegistryModel.exec.mockResolvedValueOnce(result); - mockUserRegistrySearchService.search.mockResolvedValueOnce(result); + mockUserRegistrySearchService.search.mockResolvedValueOnce(result.map((user) => ({ id: user._id.toString() }))); expect(await userRegistryService.findUsersByNameEmployerOrJobsGroup('', 1)).toStrictEqual(res); }); }); - describe('find with filter', () => { - const result: IUserRegistry[] = [ - { - _id: new Types.ObjectId('6319dfa79672971e1f8fe1b7'), - surname: 'ADMIN', - name: 'Admin', - employer: { - name: 'Pimms', - validated: true, - }, - job: { - hasPersonalOffer: true, - name: 'CNFS', - validated: true, - }, - }, - ] as IUserRegistry[]; - const res = { - count: 1, - docs: [ - { - _id: new Types.ObjectId('6319dfa79672971e1f8fe1b7'), - surname: 'ADMIN', - name: 'Admin', - employer: { - name: 'Pimms', - validated: true, - }, - job: { - hasPersonalOffer: true, - name: 'CNFS', - validated: true, - }, - }, - ], - }; - it('should findUsersByNameEmployerOrJobsGroup with string param and get a result', async () => { - mockUserRegistryModel.exec.mockResolvedValueOnce(result); - mockUserRegistrySearchService.search.mockResolvedValueOnce(result); - expect(await userRegistryService.findUsersByNameEmployerOrJobsGroup('adm', 1)).toStrictEqual(res); - }); - it('should findUsersByNameEmployerOrJobsGroup with string param and get no result', async () => { - const emptyRes = { count: 0, docs: [] }; - mockUserRegistryModel.exec.mockResolvedValueOnce([]); - mockUserRegistrySearchService.search.mockResolvedValueOnce([]); - expect(await userRegistryService.findUsersByNameEmployerOrJobsGroup('azerty', 1)).toStrictEqual(emptyRes); - }); + describe('find with filter', () => { it('should findUsersByNameEmployerOrJobsGroup with no string param and filters', async () => { - const res = { count: 1, docs: [multipleUsers[1]] }; - const jobGroupList = [ + const mockUsers = [ { - _id: new Types.ObjectId('6442ad5a5b3fd128a5c0e3b4'), - name: 'Technique', + _id: new Types.ObjectId('627b85aea0466f0f132e1598'), + name: 'CARRON', + surname: 'Guilhem', + employer: { id: 'employer1', name: 'Métropole', validated: true }, + job: { jobsGroup: '6442ad5a5b3fd128a5c0e3b4', name: 'CNFS', validated: true }, + structuresLink: [], }, ]; - const employerList = [{ name: 'Métropole', validated: true }]; - mockUserRegistrySearchService.search.mockResolvedValueOnce(multipleUsers); - mockJobsGroupsService.findByName.mockResolvedValueOnce(jobGroupList[0]); - mockEmployersService.findByName.mockResolvedValueOnce(employerList[0]); - mockUserRegistryModel.exec.mockResolvedValueOnce(multipleUsers); - expect( - await userRegistryService.findUsersByNameEmployerOrJobsGroup('', 1, ['Technique'], ['Métropole']) - ).toStrictEqual(res); + const res = { count: 1, docs: mockUsers }; + mockUserRegistrySearchService.search.mockResolvedValueOnce( + mockUsers.map((user) => ({ id: user._id.toString() })) + ); + mockUserRegistryModel.find.mockReturnThis(); + mockUserRegistryModel.where.mockReturnThis(); + mockUserRegistryModel.equals.mockReturnThis(); + mockUserRegistryModel.select.mockReturnThis(); + mockUserRegistryModel.populate.mockReturnThis(); + mockUserRegistryModel.collation.mockReturnThis(); + mockUserRegistryModel.sort.mockReturnThis(); + mockUserRegistryModel.exec.mockResolvedValueOnce(mockUsers); + + jest.spyOn(userRegistryService as any, 'callbackFilter').mockReturnValueOnce(mockUsers); + + const result = await userRegistryService.findUsersByNameEmployerOrJobsGroup( + '', + 1, + ['6442ad5a5b3fd128a5c0e3b4'], + ['employer1'] + ); + expect(result).toEqual(res); }); it('should findUsersByNameEmployerOrJobsGroup with string param and filters', async () => { - const res = { count: 1, docs: [multipleUsers[1]] }; - const jobGroupList = [ + const mockUsers = [ { - _id: new Types.ObjectId('6442ad5a5b3fd128a5c0e3b4'), - name: 'Technique', + _id: new Types.ObjectId('627b85aea0466f0f132e1598'), + name: 'CARRON', + surname: 'Guilhem', + employer: { id: 'employer1', name: 'Métropole', validated: true }, + job: { jobsGroup: '6442ad5a5b3fd128a5c0e3b4', name: 'CNFS', validated: true }, + structuresLink: [], }, ]; - const employerList = [{ name: 'Métropole', validated: true }]; - mockUserRegistrySearchService.search.mockResolvedValueOnce([multipleUsers[1]]); - mockJobsGroupsService.findByName.mockResolvedValueOnce(jobGroupList[0]); - mockEmployersService.findByName.mockResolvedValueOnce(employerList[0]); - mockUserRegistryModel.exec.mockResolvedValueOnce(multipleUsers); - expect( - await userRegistryService.findUsersByNameEmployerOrJobsGroup('Guil', 1, ['Technique'], ['Métropole']) - ).toStrictEqual(res); + const res = { count: 1, docs: mockUsers }; + mockUserRegistrySearchService.search.mockResolvedValueOnce( + mockUsers.map((user) => ({ id: user._id.toString() })) + ); + mockUserRegistryModel.find.mockReturnThis(); + mockUserRegistryModel.where.mockReturnThis(); + mockUserRegistryModel.equals.mockReturnThis(); + mockUserRegistryModel.select.mockReturnThis(); + mockUserRegistryModel.populate.mockReturnThis(); + mockUserRegistryModel.collation.mockReturnThis(); + mockUserRegistryModel.sort.mockReturnThis(); + mockUserRegistryModel.exec.mockResolvedValueOnce(mockUsers); + + jest.spyOn(userRegistryService as any, 'callbackFilter').mockReturnValueOnce(mockUsers); + + const result = await userRegistryService.findUsersByNameEmployerOrJobsGroup( + 'Guil', + 1, + ['6442ad5a5b3fd128a5c0e3b4'], + ['employer1'] + ); + expect(result).toEqual(res); }); - it('should findUsersByNameEmployerOrJobsGroup with string param and filters and return empty', async () => { - const res = { count: 0, docs: [] }; - const jobsGroupList = [ + + it('should findUsersByNameEmployerOrJobsGroup with string param and one employer filter', async () => { + const mockUsers = [ { - _id: new Types.ObjectId('6442ad5a5b3fd128a5c0e3b4'), - name: 'Technique', + _id: new Types.ObjectId('627b85aea0466f0f132e1599'), + name: 'Admin', + surname: 'ADMIN', + employer: { id: 'employer2', name: 'CAF', validated: true }, + job: { name: 'CNFS', validated: true }, + structuresLink: [], }, - ]; - const employerList = [{ name: 'Métropole', validated: true }]; - mockUserRegistrySearchService.search.mockResolvedValueOnce([]); - mockJobsGroupsService.findByName.mockResolvedValueOnce(jobsGroupList[0]); - mockEmployersService.findByName.mockResolvedValueOnce(employerList[0]); - mockUserRegistryModel.exec.mockResolvedValueOnce([]); - expect( - await userRegistryService.findUsersByNameEmployerOrJobsGroup('azerrttt', 1, ['Technique'], ['Métropole']) - ).toStrictEqual(res); - }); - it('should findUsersByNameEmployerOrJobsGroup with string param and one employer filter', async () => { - const res = { count: 2, docs: [multipleUsers[0], multipleUsers[2]] }; - const employerList = [{ name: 'CAF', validated: true }]; - mockUserRegistrySearchService.search.mockResolvedValueOnce(multipleUsers); - mockEmployersService.findByName.mockResolvedValueOnce(employerList[0]); - mockUserRegistryModel.exec.mockResolvedValueOnce(multipleUsers); - expect(await userRegistryService.findUsersByNameEmployerOrJobsGroup('a', 1, [], ['CAF'])).toStrictEqual(res); - }); - - it('should findUsersByNameEmployerOrJobsGroup with no string param and one jobsGroup filter', async () => { - const res = { count: 1, docs: [multipleUsers[1]] }; - const jobGroupList = [ { - _id: new Types.ObjectId('6442ad5a5b3fd128a5c0e3b4'), - name: 'Technique', + _id: new Types.ObjectId('627b85aea0466f0f132e1597'), + name: 'DESCHAMPS', + surname: 'Jean-Paul', + employer: { id: 'employer2', name: 'CAF', validated: true }, + job: { name: 'Conseiller', validated: true }, + structuresLink: [], }, ]; - mockUserRegistrySearchService.search.mockResolvedValueOnce(multipleUsers); - mockJobsGroupsService.findByName.mockResolvedValueOnce(jobGroupList[0]); - mockUserRegistryModel.exec.mockResolvedValueOnce(multipleUsers); - expect(await userRegistryService.findUsersByNameEmployerOrJobsGroup('', 1, ['Technique'], [])).toStrictEqual(res); + const res = { count: 2, docs: mockUsers }; + mockUserRegistrySearchService.search.mockResolvedValueOnce( + mockUsers.map((user) => ({ id: user._id.toString() })) + ); + mockUserRegistryModel.find.mockReturnThis(); + mockUserRegistryModel.where.mockReturnThis(); + mockUserRegistryModel.equals.mockReturnThis(); + mockUserRegistryModel.select.mockReturnThis(); + mockUserRegistryModel.populate.mockReturnThis(); + mockUserRegistryModel.collation.mockReturnThis(); + mockUserRegistryModel.sort.mockReturnThis(); + mockUserRegistryModel.exec.mockResolvedValueOnce(mockUsers); + + jest.spyOn(userRegistryService as any, 'callbackFilter').mockReturnValueOnce(mockUsers); + + const result = await userRegistryService.findUsersByNameEmployerOrJobsGroup('a', 1, [], ['employer2']); + expect(result).toEqual(res); }); }); @@ -235,4 +230,50 @@ describe('userRegistryService', () => { expect(await userRegistryService.initUserRegistryIndex()).toBe(multipleUsers); }); }); + + describe('findCommunesWithUsers', () => { + it('should return communes with users', async () => { + const mockUsers = [ + { + structuresLink: [ + new Types.ObjectId('507f1f77bcf86cd799439011'), + new Types.ObjectId('507f1f77bcf86cd799439012'), + ], + }, + { + structuresLink: [new Types.ObjectId('507f1f77bcf86cd799439013')], + }, + ]; + const mockStructures = [ + { address: { commune: 'Corbas', inseeCode: '69273' } }, + { address: { commune: 'Lyon1', inseeCode: '69381' } }, + { address: { commune: 'Brignais', inseeCode: '69027' } }, + ]; + + mockUserRegistryModel.find.mockReturnThis(); + mockUserRegistryModel.select.mockReturnThis(); + mockUserRegistryModel.exec.mockResolvedValueOnce(mockUsers); + + mockStructuresService.findOne + .mockResolvedValueOnce(mockStructures[0]) + .mockResolvedValueOnce(mockStructures[1]) + .mockResolvedValueOnce(mockStructures[2]); + + const result = await userRegistryService.findCommunesWithUsers(); + + // Alphabetically sorted result + expect(result).toEqual([ + { commune: 'Brignais', inseeCode: '69027' }, + { commune: 'Corbas', inseeCode: '69273' }, + { commune: 'Lyon1', inseeCode: '69381' }, + ]); + + expect(mockUserRegistryModel.find).toHaveBeenCalledWith({ + structuresLink: { $exists: true, $ne: [] }, + emailVerified: true, + }); + expect(mockUserRegistryModel.select).toHaveBeenCalledWith('structuresLink'); + expect(mockStructuresService.findOne).toHaveBeenCalledTimes(3); + }); + }); }); diff --git a/src/users/services/userRegistry.service.ts b/src/users/services/userRegistry.service.ts index 2577369ed616d1d25892205911429a1530edd535..2fe896eb83ac26c533c1c0abfbd852595a9746ad 100644 --- a/src/users/services/userRegistry.service.ts +++ b/src/users/services/userRegistry.service.ts @@ -3,12 +3,9 @@ import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; import { IUser } from '../interfaces/user.interface'; import { IUserRegistry, UserRegistryPaginatedResponse } from '../interfaces/userRegistry.interface'; -import { Employer } from '../schemas/employer.schema'; -import { JobsGroupsDocument } from '../schemas/jobsGroups.schema'; import { User } from '../schemas/user.schema'; -import { EmployerService } from './employer.service'; -import { JobsGroupsService } from './jobsGroups.service'; import { UserRegistrySearchService } from './userRegistry-search.service'; +import { StructuresService } from '../../structures/services/structures.service'; @Injectable() export class UserRegistryService { @@ -16,8 +13,7 @@ export class UserRegistryService { constructor( @InjectModel(User.name) private userModel: Model<IUser>, private userRegistrySearchService: UserRegistrySearchService, - private jobsGroupsService: JobsGroupsService, - private employerService: EmployerService + private structureService: StructuresService ) {} public maxPerPage = 20; @@ -27,7 +23,8 @@ export class UserRegistryService { .find({ email: { $ne: process.env.MAIL_CONTACT } }) .where('emailVerified') .equals(true) - .select('name surname _id job employer ') + .select('name surname _id job employer structuresLink') + .populate({ path: 'structuresLink', model: 'Structure', select: 'categories.ctm' }) .populate('job employer') .collation({ locale: 'fr' }) .sort({ surname: 1 }) @@ -42,25 +39,40 @@ export class UserRegistryService { .equals(true) .populate('employer') .populate('job') - .select('name surname employer job _id ') + .select('name surname employer job _id') .collation({ locale: 'fr' }) .sort({ surname: 1 }) .count() .exec(); } - private callbackFilter(users: IUser[], employersList: Employer[], jobsGroupsList: JobsGroupsDocument[]): IUser[] { + private callbackFilter( + users: IUser[], + employersList: string[], + jobsGroupsList: string[], + territoriesList: string[], + communesList: string[] + ): IUser[] { this.logger.debug('callbackFilter'); - const jobsGroupsIds: string[] = jobsGroupsList.map((jobsGroup) => jobsGroup._id.toString()); - const employersNames: string[] = employersList.map((e) => e.name); // For each filter list (job or employer), we'll filter the main user list in order to get only the user that have a job or employer contained in the filters array // For this, we use findIndex method on job/employer name - if (employersList?.length) { - users = users.filter((user) => employersNames.findIndex((n) => user.employer?.name === n) > -1); + users = users.filter((user) => employersList.includes(user.employer?.id)); } if (jobsGroupsList?.length) { - users = users.filter((user) => jobsGroupsIds.includes(user.job?.jobsGroup?.toString())); + users = users.filter((user) => jobsGroupsList.includes(user.job?.jobsGroup?.toString())); + } + if (territoriesList?.length) { + users = users.filter((user) => { + const ctmValues = user.structuresLink.flatMap((structure: any) => structure.categories.ctm || []); + return ctmValues.some((ctm: string) => territoriesList.includes(ctm)); + }); + } + if (communesList?.length) { + users = users.filter((user) => { + const inseeCodeList = user.structuresLink.flatMap((structure: any) => structure.address?.inseeCode || []); + return inseeCodeList.some((inseeCode: string) => communesList.includes(inseeCode)); + }); } return users; } @@ -69,23 +81,13 @@ export class UserRegistryService { searchParam: string, page: number, jobsGroups?: string[], - employers?: string[] + employers?: string[], + territories?: string[], + communes?: string[] ): Promise<UserRegistryPaginatedResponse> { this.logger.debug('findUsersByNameEmployerOrJobsGroup'); const results = await this.userRegistrySearchService.search(searchParam); const ids = results.map((result) => result.id); - const jobsGroupsList: JobsGroupsDocument[] = []; - const employersList: Employer[] = []; - if (jobsGroups) { - for (const job of jobsGroups) { - jobsGroupsList.push(await this.jobsGroupsService.findByName(job)); - } - } - if (employers) { - for (const employer of employers) { - employersList.push(await this.employerService.findByName(employer)); - } - } const resultsWithFilter = await this.userModel .find({ _id: { $in: ids }, @@ -93,13 +95,14 @@ export class UserRegistryService { }) .where('emailVerified') .equals(true) - .select('name surname employer job _id withAppointment permalink') + .select('name surname employer job _id withAppointment permalink structureLink ') .populate('employer job') + .populate({ path: 'structuresLink', model: 'Structure', select: 'categories.ctm address.inseeCode' }) .collation({ locale: 'fr' }) .sort({ surname: 1 }) .exec() .then((res) => { - return this.callbackFilter(res, employersList, jobsGroupsList); + return this.callbackFilter(res, employers, jobsGroups, territories, communes); }); return { count: resultsWithFilter.length, @@ -107,6 +110,39 @@ export class UserRegistryService { }; } + /* Find all communes that have at least one structure that has at least one user */ + public async findCommunesWithUsers(): Promise<{ commune: string; inseeCode: string }[]> { + this.logger.debug('findCommunesWithUsers'); + + // Fetch users with structuresLink + const users = await this.userModel + .find({ structuresLink: { $exists: true, $ne: [] }, emailVerified: true }) + .select('structuresLink') + .exec(); + + // Collect unique structure IDs from users then fetch structures + const structureIds = Array.from(new Set(users.flatMap((user) => user.structuresLink))); + const structures = await Promise.all(structureIds.map((id) => this.structureService.findOne(id.toString()))); + + // Extract communes and inseeCodes (in a Set for unique commune identifiers) + const communesSet = new Set<string>(); + structures.forEach((structure) => { + const commune = structure?.address?.commune; + const inseeCode = structure?.address?.inseeCode; + if (typeof commune === 'string' && typeof inseeCode === 'string') { + communesSet.add(`${commune}|${inseeCode}`); + } + }); + + // Convert Set to Array and sort by commune name + return Array.from(communesSet) + .map((key) => { + const [commune, inseeCode] = key.split('|'); + return { commune, inseeCode }; + }) + .sort((a, b) => a.commune.localeCompare(b.commune)); + } + public async initUserRegistryIndex() { Logger.log('Reset users indexes'); await this.userRegistrySearchService.dropIndex(); @@ -118,7 +154,7 @@ export class UserRegistryService { const users = await this.findAllForIndexation(); await Promise.all( users.map((user: IUserRegistry) => { - this.userRegistrySearchService.indexUserRegistry(user); + return this.userRegistrySearchService.indexUserRegistry(user); }) ); return users; diff --git a/src/users/services/users.service.spec.ts b/src/users/services/users.service.spec.ts index 6a3093162959571bce870131175f6e0b0eaf2ab9..d0b0b9aa323abf90b009ad752662c292f7c3a046 100644 --- a/src/users/services/users.service.spec.ts +++ b/src/users/services/users.service.spec.ts @@ -112,9 +112,10 @@ const mockUser: User = { phone: '06 06 06 06 06', createdAt: new Date('2022-05-25T09:48:28.824Z'), employer: { + _id: '6654+65564', name: 'test', validated: true, - }, + } as EmployerDocument, job: { name: 'test', validated: true,