diff --git a/.gitignore b/.gitignore index a60b41b70d05fc80340cc59824669e40ab84e290..58b0057200d389ee28a40be1e1dd97c346ee003c 100644 --- a/.gitignore +++ b/.gitignore @@ -389,3 +389,7 @@ Temporary Items # Local .env dist + +# Migrations +.migrate +src/migrations/data/ \ No newline at end of file diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml index 79869be065068220ec5a1da241c04e416590fc95..bb71762235a1187989c66f732efe2aa222b128a2 100644 --- a/.gitlab-ci.yml +++ b/.gitlab-ci.yml @@ -35,12 +35,17 @@ deploy_dev: - docker-compose pull service-ram - docker-compose up -d --force-recreate service-ram - docker system prune -a -f + environment: + name: dev + url: https://resin-dev.grandlyon.com test: stage: test image: node:14.15.4 before_script: - export GHOST_HOST_AND_PORT=http://localhost:2368 + - export GHOST_ADMIN_API_KEY=60142bc9e33940000156bccc:6217742e2671e322612e89cac9bab61fcd01822709fe5d8f5e6a5b3e54d5e6bb + - export SALT=$TEST_SALT script: - npm i - npm run test:cov diff --git a/CHANGELOG.md b/CHANGELOG.md index 5c9270be77d32993eeba2f1bd9ca3d2aaecc4f57..a59f202c1f017b324006555b280e9291176e900f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,28 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +## [1.7.0](https://forge.grandlyon.com///compare/v1.6.1...v1.7.0) (2021-04-12) + + +### Features + +* add aptic service offer query ([7602b41](https://forge.grandlyon.com///commit/7602b412f08047cd8dea2cb99af2e47b83d08c08)) +* add endpoint for sending error report for structure ([bc64158](https://forge.grandlyon.com///commit/bc64158c06f6c1ecad7753dbac62a19a81f84a42)) +* add newsletter data insert script ([a1a743d](https://forge.grandlyon.com///commit/a1a743d5c3391c2c54ec095a6cf12e4f2aeaae58)) +* add newsletter subscription ([c507f53](https://forge.grandlyon.com///commit/c507f53646c9f10d40ebe1d6ea518b7aeea27876)) +* edit request for structure service (not working) ([a156819](https://forge.grandlyon.com///commit/a1568197036f8a87d902b2563fc35430a79d13c3)) +* TU for admin mailer and posts ([1b1724e](https://forge.grandlyon.com///commit/1b1724efcca24c2e5706e122f13f6740bb9b4ee9)) +* zoom on town ([7eec8f1](https://forge.grandlyon.com///commit/7eec8f160286746c713b451d7d8576bc50314c9b)) + + +### Bug Fixes + +* migration script path ([0b549b8](https://forge.grandlyon.com///commit/0b549b86d961d25f95d3977cab2c2f86f5b0e6b1)) +* typo in .gitignore ([ede567e](https://forge.grandlyon.com///commit/ede567e7fdd4542f5c5ccced51b3a627434bfd98)) +* **structures:** prevent access to deleted structures ([ea0bf7b](https://forge.grandlyon.com///commit/ea0bf7bf698486cda875113852d6cc5c957f1f55)) +* update search filter query ([44bb26e](https://forge.grandlyon.com///commit/44bb26ea836d95c3c5bc5437553eb633cf0092f1)) +* **mail:** add link to structure in admin new structure mail ([b067483](https://forge.grandlyon.com///commit/b067483a91fb97ec7506f17a4999cd4fd65614be)) + ### [1.6.1](https://forge.grandlyon.com///compare/v1.6.0...v1.6.1) (2021-04-01) diff --git a/package-lock.json b/package-lock.json index fe3585896802626aeef6f70056c64594d82e9e7d..d787d4c561026f1861332126e9218bc649e0b94c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "ram_server", - "version": "1.6.1", + "version": "1.7.0", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -9731,6 +9731,33 @@ } } }, + "migrate": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/migrate/-/migrate-1.7.0.tgz", + "integrity": "sha512-I63YykITgWyI+ET4KO8xGePYkR9U7CtSe/RrR13vLbZSpUcAh4/ry2GswNv7Lywcsp3BaDHj7YdjC7ihVYCFmw==", + "requires": { + "chalk": "^2.4.1", + "commander": "^2.19.0", + "dateformat": "^3.0.3", + "dotenv": "^6.1.0", + "inherits": "^2.0.3", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.1", + "slug": "^0.9.2" + }, + "dependencies": { + "commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==" + }, + "dotenv": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-6.2.0.tgz", + "integrity": "sha512-HygQCKUBSFl8wKQZBSemMywRWcEDNidvNbjGVyZu3nbZ8qq9ubiPoGLMdRDpfSrpkkm9BXYFkpKxxFX38o/76w==" + } + } + }, "miller-rabin": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", @@ -11975,6 +12002,14 @@ "resolved": "https://registry.npmjs.org/sliced/-/sliced-1.0.1.tgz", "integrity": "sha1-CzpmK10Ewxd7GSa+qCsD+Dei70E=" }, + "slug": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/slug/-/slug-0.9.4.tgz", + "integrity": "sha512-3YHq0TeJ4+AIFbJm+4UWSQs5A1mmeWOTQqydW3OoPmQfNKxlO96NDRTIrp+TBkmvEsEFrd+Z/LXw8OD/6OlZ5g==", + "requires": { + "unicode": ">= 0.3.1" + } + }, "snapdragon": { "version": "0.8.2", "resolved": "https://registry.npmjs.org/snapdragon/-/snapdragon-0.8.2.tgz", @@ -13260,6 +13295,11 @@ "debug": "^2.2.0" } }, + "unicode": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/unicode/-/unicode-13.0.0.tgz", + "integrity": "sha512-osNPLT4Lqna/sV6DQikrB8m4WxR61/k0fnhfKnkPGcZImczW3IysRXvWxfdqGUjh0Ju2o/tGGgu46mlfc/cpZw==" + }, "union-value": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/union-value/-/union-value-1.0.1.tgz", diff --git a/package.json b/package.json index c241c3f401eecdef10bc1888cd8b9331afa67f8a..79114f2ed061d6b141ac421bafe63a3df12d5d55 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "ram_server", "private": true, - "version": "1.6.1", + "version": "1.7.0", "description": "Nest TypeScript starter repository", "license": "MIT", "scripts": { @@ -19,7 +19,10 @@ "test:watch": "jest --watch", "test:cov": "jest --config ./test/jest.json --coverage --ci --reporters=default --reporters=jest-junit", "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", - "test:e2e": "jest --config ./test/jest-e2e.json" + "test:e2e": "jest --config ./test/jest-e2e.json", + "migrate:create": "migrate create --template-file ./src/migrations/migrations-utils/template.ts --migrations-dir=\"./src/migrations/scripts\" --compiler=\"ts:./src/migrations/migrations-utils/ts-compiler.js\"", + "migrate:up": "migrate --migrations-dir=\"./src/migrations/scripts\" --compiler=\"ts:./src/migrations/migrations-utils/ts-compiler.js\" up", + "migrate:down": "migrate --migrations-dir=\"./src/migrations/scripts\" --compiler=\"ts:./src/migrations/migrations-utils/ts-compiler.js\" down" }, "dependencies": { "@nestjs/common": "^7.6.13", @@ -39,6 +42,7 @@ "ejs": "^3.1.5", "form-data": "^3.0.0", "luxon": "^1.25.0", + "migrate": "^1.7.0", "mongoose": "^5.10.15", "passport": "^0.4.1", "passport-jwt": "^4.0.0", diff --git a/src/admin/admin.controller.spec.ts b/src/admin/admin.controller.spec.ts index 65e9b88e4fb7b56a04dfa670134a6d6a69475f55..84dbf424a36ea0a54a4e59cf28ff657f8d546082 100644 --- a/src/admin/admin.controller.spec.ts +++ b/src/admin/admin.controller.spec.ts @@ -1,13 +1,16 @@ -import { HttpModule } from '@nestjs/common'; +import { HttpException, HttpModule, HttpStatus } from '@nestjs/common'; import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; import { ConfigurationModule } from '../configuration/configuration.module'; import { MailerService } from '../mailer/mailer.service'; +import { NewsletterSubscription } from '../newsletter/newsletter-subscription.schema'; +import { NewsletterService } from '../newsletter/newsletter.service'; import { Structure } from '../structures/schemas/structure.schema'; import { StructuresService } from '../structures/services/structures.service'; import { User } from '../users/schemas/user.schema'; import { UsersService } from '../users/users.service'; import { AdminController } from './admin.controller'; +import { PendingStructureDto } from './dto/pending-structure.dto'; describe('AdminController', () => { let controller: AdminController; @@ -18,11 +21,16 @@ describe('AdminController', () => { providers: [ UsersService, StructuresService, + NewsletterService, MailerService, { provide: getModelToken('User'), useValue: User, }, + { + provide: getModelToken('NewsletterSubscription'), + useValue: NewsletterSubscription, + }, { provide: getModelToken('Structure'), useValue: Structure, @@ -37,4 +45,24 @@ describe('AdminController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + it('should get pending attachments', async () => { + const result = [{name: "MJC Route de vienne", address: "14 chemin des platanes"}, {name: "Mairie Lyon 7eme", address: "21 boulevard martin"}]; + jest.spyOn(controller, 'getPendingAttachments').mockImplementation(async (): Promise<any> => result); + expect(await controller.getPendingAttachments()).toBe(result); + }); + + it('should validate pending structure', async () => { + const result = [{name: "MJC Route de vienne", address: "14 chemin des platanes"}]; + const structure: PendingStructureDto = {userEmail:"martin@mjc.fr", structureId: "1", structureName:"MJC Route de vienne"}; + jest.spyOn(controller, 'validatePendingStructure').mockImplementation(async (): Promise<any> => result); + expect(await controller.validatePendingStructure(structure)).toBe(result); + }); + + it('should refuse pending structure', async () => { + const result = [{name: "MJC Route de vienne", address: "14 chemin des platanes"}, {name: "Mairie Lyon 7eme", address: "21 boulevard martin"}]; + const structure: PendingStructureDto = {userEmail:"martin@mjc.fr", structureId: "1", structureName:"MJC Route de vienne"}; + jest.spyOn(controller, 'refusePendingStructure').mockImplementation(async (): Promise<any> => result); + expect(await controller.refusePendingStructure(structure)).toBe(result); + }); }); diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts index 3947dd029646e816d889b6445a0e142aa4b17619..03442f8e55e2210e3bf1fc52f96e0c4617ff3add 100644 --- a/src/admin/admin.controller.ts +++ b/src/admin/admin.controller.ts @@ -2,6 +2,7 @@ import { Body, Delete, Param } from '@nestjs/common'; import { Controller, Get, Post, UseGuards } from '@nestjs/common'; import { ApiOperation, ApiParam } from '@nestjs/swagger'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; +import { NewsletterService } from '../newsletter/newsletter.service'; import { StructuresService } from '../structures/services/structures.service'; import { Roles } from '../users/decorators/roles.decorator'; import { RolesGuard } from '../users/guards/roles.guard'; @@ -10,7 +11,11 @@ import { PendingStructureDto } from './dto/pending-structure.dto'; @Controller('admin') export class AdminController { - constructor(private usersService: UsersService, private structuresService: StructuresService) {} + constructor( + private usersService: UsersService, + private structuresService: StructuresService, + private newsletterService: NewsletterService + ) {} @UseGuards(JwtAuthGuard, RolesGuard) @Roles('admin') @@ -96,4 +101,21 @@ export class AdminController { return this.usersService.searchUsers(searchString.searchString); else return this.usersService.findAll(); } + + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin') + @Post('searchNewsletterSubscriptions') + public async getNewsletterSubscriptions(@Body() searchString: { searchString: string }) { + if (searchString && searchString.searchString.length > 0) + return this.newsletterService.searchNewsletterSubscription(searchString.searchString); + else return this.newsletterService.findAll(); + } + + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin') + @Delete('newsletterSubscription/:email') + @ApiParam({ name: 'email', type: String, required: true }) + public async unsubscribeUserFromNewsletter(@Param() params) { + return await this.newsletterService.deleteOneEmail(params.email); + } } diff --git a/src/admin/admin.module.ts b/src/admin/admin.module.ts index a13616a1802d24dc4f58d513714a9647193c591b..7854c19b255df99cc35837f56d9f9dfd637d7bf5 100644 --- a/src/admin/admin.module.ts +++ b/src/admin/admin.module.ts @@ -1,11 +1,12 @@ import { Module } from '@nestjs/common'; +import { NewsletterModule } from '../newsletter/newsletter.module'; import { StructuresModule } from '../structures/structures.module'; import { UsersModule } from '../users/users.module'; import { AdminController } from './admin.controller'; import { AdminService } from './admin.service'; @Module({ - imports: [UsersModule, StructuresModule], + imports: [UsersModule, StructuresModule, NewsletterModule], controllers: [AdminController], providers: [AdminService], }) diff --git a/src/app.module.ts b/src/app.module.ts index 467fe404c31f6fdeadc948aec96bdf136aa676f3..ed6a03004e4e90edc2decf97c41b7f3478ca4f69 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -12,6 +12,7 @@ import { TclModule } from './tcl/tcl.module'; import { AdminModule } from './admin/admin.module'; import { PostsModule } from './posts/posts.module'; import { TempUserModule } from './temp-user/temp-user.module'; +import { NewsletterModule } from './newsletter/newsletter.module'; @Module({ imports: [ ConfigurationModule, @@ -28,6 +29,7 @@ import { TempUserModule } from './temp-user/temp-user.module'; AdminModule, PostsModule, TempUserModule, + NewsletterModule ], controllers: [AppController], }) diff --git a/src/categories/categories.module.ts b/src/categories/categories.module.ts index 380468396bceb0829ce6ad1a93e5b029a4722799..bc6f3c86841008cb9bed60f0c70bbd517b947403 100644 --- a/src/categories/categories.module.ts +++ b/src/categories/categories.module.ts @@ -19,6 +19,7 @@ import { CategoriesAccompagnement, CategoriesAccompagnementSchema } from './sche ]), ], controllers: [CategoriesFormationsController, CategoriesAccompagnementController, CategoriesOthersController], + exports: [CategoriesFormationsService], providers: [CategoriesFormationsService, CategoriesAccompagnementService, CategoriesOthersService], }) export class CategoriesModule {} diff --git a/src/categories/services/categories-formations.service.ts b/src/categories/services/categories-formations.service.ts index 05ecae4af07f33c273ecdd2ded6a89c734358c8c..71a6c1073ad5245541c651836b272195dd96397c 100644 --- a/src/categories/services/categories-formations.service.ts +++ b/src/categories/services/categories-formations.service.ts @@ -16,4 +16,8 @@ export class CategoriesFormationsService { public async findAll(): Promise<CategoriesFormations[]> { return this.structureModel.find().exec(); } + + public findOne(categoryId: string): Promise<any> { + return this.structureModel.findOne({ id: categoryId }).select({ 'modules.id': 1 }).exec(); + } } diff --git a/src/configuration/config.ts b/src/configuration/config.ts index 3d6969e35463dda86086db80ee2bd8f9c762132a..0506dc2ae8a5b211ad5883852d50858f71e84fda 100644 --- a/src/configuration/config.ts +++ b/src/configuration/config.ts @@ -49,5 +49,9 @@ export const config = { ejs: 'adminStructureCreate.ejs', json: 'adminStructureCreate.json', }, + structureErrorReport: { + ejs: 'structureErrorReport.ejs', + json: 'structureErrorReport.json', + }, }, }; diff --git a/src/mailer/mail-templates/adminStructureCreate.ejs b/src/mailer/mail-templates/adminStructureCreate.ejs index 41d58470fa059925b790e440caec3c5606dbda1c..47b79daa15b999739415094be15f42e9f641ede0 100644 --- a/src/mailer/mail-templates/adminStructureCreate.ejs +++ b/src/mailer/mail-templates/adminStructureCreate.ejs @@ -1,3 +1,9 @@ Bonjour<br /> <br /> -Une nouvelle structure a été créé: <strong><%= name %></strong>. +Une nouvelle structure a été créé: +<a href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/acteurs?id=<%= id %>" + ><strong><%= name %></strong></a +> +<br /> +Il est possible que la structure ne soit pas immédiatement visible sur la carto. L'utilisateur doit valider son compte +pour que celle-ci soit visible. diff --git a/src/mailer/mail-templates/structureErrorReport.ejs b/src/mailer/mail-templates/structureErrorReport.ejs new file mode 100644 index 0000000000000000000000000000000000000000..829029e95f157b6034c60889e7ee0798f2518d48 --- /dev/null +++ b/src/mailer/mail-templates/structureErrorReport.ejs @@ -0,0 +1,11 @@ +Bonjour<br /> +<br /> +Un utilisateur de Res'in a relevé une erreur sur la fiche de votre structure (<%= structureName %>). +<br /> +Voici le message:<br /> +<br /> +<strong><%= content %></strong><br /> +<br /> +<a href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/acteurs?id=<%= id %>" + >Acceder à votre structure</a +>. diff --git a/src/mailer/mail-templates/structureErrorReport.json b/src/mailer/mail-templates/structureErrorReport.json new file mode 100644 index 0000000000000000000000000000000000000000..3faeacf33799d9891558bedba0a2771ef7344782 --- /dev/null +++ b/src/mailer/mail-templates/structureErrorReport.json @@ -0,0 +1,3 @@ +{ + "subject": "Une erreur a été remontée sur votre structure, Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon" +} diff --git a/src/mailer/mail-templates/structureOutdatedInfo.ejs b/src/mailer/mail-templates/structureOutdatedInfo.ejs index 589ec35605915f4ffb5a62ce3a6d5c7827d1064f..5427f8b22825c64ab25c3da4d903b3dc27742cbd 100644 --- a/src/mailer/mail-templates/structureOutdatedInfo.ejs +++ b/src/mailer/mail-templates/structureOutdatedInfo.ejs @@ -3,6 +3,6 @@ Bonjour<br /> Vous recevez ce message, parce que votre structure <strong><%= name %></strong> est référencée sur RES'in, le réseau des acteurs de l'inclusion numérique de la Métropole de Lyon. Pouvez-vous nous aider en vérifiant que vos données sont bien à jour en -<a href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/home?id=<%= id %>" +<a href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/acteurs?id=<%= id %>" >cliquant ici</a >. diff --git a/src/mailer/mailer.service.spec.ts b/src/mailer/mailer.service.spec.ts index 086056cab0a7d8efe90a3fde1e61ac0b833e84ee..2fd12e46668bdc0cc4a32940715c2a7fc9043cdf 100644 --- a/src/mailer/mailer.service.spec.ts +++ b/src/mailer/mailer.service.spec.ts @@ -18,4 +18,28 @@ describe('MailerService', () => { it('should be defined', () => { expect(service).toBeDefined(); }); + + it('should send email', async () => { + const result = "AxiosResponse" + jest.spyOn(service, 'send').mockImplementation(async (): Promise<any> => result); + expect(await service.send("to", "subject", "html")).toBe(result); + }); + + it('should get template location', async () => { + const result = "/path/to/template"; + jest.spyOn(service, 'getTemplateLocation').mockImplementation(() => {return result}); + expect(await service.getTemplateLocation("filename")).toBe(result); + }); + + it('should load Json Config', async () => { + const result = null; + jest.spyOn(service, 'loadJsonConfig').mockImplementation(async (): Promise<any> => result); + expect(await service.loadJsonConfig("filename")).toBe(result); + }); + + it('should add signature', async () => { + const result = "signed html"; + jest.spyOn(service, 'addSignature').mockImplementation(() => {return result}); + expect(await service.addSignature("html")).toBe(result); + }); }); diff --git a/src/mailer/mailer.service.ts b/src/mailer/mailer.service.ts index 0650bc48c0255866d15f86ed725254b665ad01bf..c19d4b2b2830a1ba83c34669ab195df78c90a9ff 100644 --- a/src/mailer/mailer.service.ts +++ b/src/mailer/mailer.service.ts @@ -21,15 +21,15 @@ export class MailerService { * @param {string} html * @param {string} text */ - public async send(to: string, subject: string, html: string): Promise<AxiosResponse<any>> { + public async send(to: string | { email: string }[], subject: string, html: string): Promise<AxiosResponse<any>> { + const emailsToSend = typeof to === 'string' ? [{ email: to }] : to; const formData = new FormData(); - const data = JSON.stringify({ // eslint-disable-next-line camelcase from_email: this.config.from, // eslint-disable-next-line camelcase from_name: this.config.from_name, - to: [{ email: to }], + to: emailsToSend, reply_to: 'inclusionnumerique@grandlyon.com', subject: subject, content: this.addSignature(html), diff --git a/src/migrations/migrations-utils/db.ts b/src/migrations/migrations-utils/db.ts new file mode 100644 index 0000000000000000000000000000000000000000..4a5284a096c12204c8e3d1b9e3b5b620729d0092 --- /dev/null +++ b/src/migrations/migrations-utils/db.ts @@ -0,0 +1,11 @@ +import { MongoClient } from 'mongodb'; +import { config } from 'dotenv'; + +export const getDb = async () => { + config(); // Required for reading .env + const client: MongoClient = await MongoClient.connect( + `mongodb://${process.env.MONGO_NON_ROOT_USERNAME}:${process.env.MONGO_NON_ROOT_PASSWORD}@${process.env.MONGO_DB_HOST_AND_PORT}/ram`, + { useUnifiedTopology: true } + ); + return client.db(); +}; diff --git a/src/migrations/migrations-utils/template.ts b/src/migrations/migrations-utils/template.ts new file mode 100644 index 0000000000000000000000000000000000000000..d3f00960db09db7b37081642dadc328fa876ce13 --- /dev/null +++ b/src/migrations/migrations-utils/template.ts @@ -0,0 +1,13 @@ +import { Db } from 'mongodb'; +import { getDb } from '../migrations-utils/db'; + +export const up = async () => { + const db: Db = await getDb(); +}; + +export const down = async () => { + const db: Db = await getDb(); + /* + Code you downgrade script here! + */ +}; diff --git a/src/migrations/migrations-utils/ts-compiler.js b/src/migrations/migrations-utils/ts-compiler.js new file mode 100644 index 0000000000000000000000000000000000000000..1424f7e7c44aef954e45570d48ac921490e0f583 --- /dev/null +++ b/src/migrations/migrations-utils/ts-compiler.js @@ -0,0 +1,3 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const tsNode = require('ts-node'); +module.exports = tsNode.register; diff --git a/src/migrations/scripts/1617284203579-apticid.ts b/src/migrations/scripts/1617284203579-apticid.ts new file mode 100644 index 0000000000000000000000000000000000000000..b7d5efcf3f43e4000ae6805540ed185073f392df --- /dev/null +++ b/src/migrations/scripts/1617284203579-apticid.ts @@ -0,0 +1,394 @@ +import { getDb } from '../migrations-utils/db'; + +class ApticModule { + id: string; + display_id: string; + display_name: string; + url: string; + last_update_time: string; + receipt_time: string; +} + +class ApticDoc { + id: string; + modules: ApticModule[]; +} + +export const up = async () => { + const db = await getDb(); + await updateStructuresId(db); + await updateApticReferential(db); +}; + +export const down = async () => { + const db = await getDb(); + await downgradeApticReferential(db); + await downgradeStructuresId(db); +}; + +async function downgradeStructuresId(db) { + await db + .collection('structures') + .find({}) + .forEach((doc) => { + const newDoc = downgradeStructure(doc); + db.collection('structures').updateMany({ _id: doc._id }, [{ $set: newDoc }]); + }); +} + +async function updateStructuresId(db) { + await db + .collection('structures') + .find({}) + .forEach((doc) => { + const newDoc = updateStructure(doc); + db.collection('structures').updateMany({ _id: doc._id }, [{ $set: newDoc }]); + }); +} + +function updateStructure(structure): any { + let newArray = []; + // Social and professional + newArray.push(switchStructureId(structure.socialAndProfessional, '254', '6')); + newArray.push(switchStructureId(structure.socialAndProfessional, '240', '20')); + newArray.push(switchStructureId(structure.socialAndProfessional, '194', '66')); + newArray.push(switchStructureId(structure.socialAndProfessional, '193', '67')); + newArray.push(switchStructureId(structure.socialAndProfessional, '192', '68')); + newArray.push(switchStructureId(structure.socialAndProfessional, '191', '69')); + newArray.push(switchStructureId(structure.socialAndProfessional, '262', '124')); + newArray.push(switchStructureId(structure.socialAndProfessional, '263', '125')); + newArray.push(switchStructureId(structure.socialAndProfessional, '003', '127')); + // Remove null cases + newArray = newArray.filter((obj) => obj); + structure.socialAndProfessional = newArray; + + // Base Skills + newArray = []; + newArray.push(switchStructureId(structure.baseSkills, '260', '260')); //TODO: + newArray.push(switchStructureId(structure.baseSkills, '259', '1')); + newArray.push(switchStructureId(structure.baseSkills, '261', '11')); + newArray.push(switchStructureId(structure.baseSkills, '222', '38')); + newArray.push(switchStructureId(structure.baseSkills, '212', '48')); + newArray.push(switchStructureId(structure.baseSkills, '186', '74')); + newArray.push(switchStructureId(structure.baseSkills, '183', '77')); + // Remove null cases + newArray = newArray.filter((obj) => obj); + structure.baseSkills = newArray; + + // Access Right + newArray = []; + newArray.push(switchStructureId(structure.accessRight, '176', '84')); + newArray.push(switchStructureId(structure.accessRight, '175', '85')); + newArray.push(switchStructureId(structure.accessRight, '174', '86')); + newArray.push(switchStructureId(structure.accessRight, '173', '87')); + newArray.push(switchStructureId(structure.accessRight, '172', '88')); + newArray.push(switchStructureId(structure.accessRight, '171', '89')); + newArray.push(switchStructureId(structure.accessRight, '167', '93')); + newArray.push(switchStructureId(structure.accessRight, '165', '95')); + // Remove null cases + newArray = newArray.filter((obj) => obj); + structure.accessRight = newArray; + + // Parenting + newArray = []; + newArray.push(switchStructureId(structure.parentingHelp, '257', '3')); + newArray.push(switchStructureId(structure.parentingHelp, '238', '22')); + newArray.push(switchStructureId(structure.parentingHelp, '178', '82')); + newArray.push(switchStructureId(structure.parentingHelp, '166', '94')); + // Remove null cases + newArray = newArray.filter((obj) => obj); + structure.parentingHelp = newArray; + + // Digital Security + newArray = []; + newArray.push(switchStructureId(structure.digitalCultureSecurity, '264', '2')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '255', '5')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '265', '9')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '232', '28')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '225', '34')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '221', '39')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '218', '42')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '209', '51')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '208', '52')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '206', '54')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '195', '65')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '164', '96')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '163', '97')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '162', '98')); + // Remove null cases + newArray = newArray.filter((obj) => obj); + structure.digitalCultureSecurity = newArray; + + return structure; +} + +function downgradeStructure(structure): any { + let newArray = []; + // Social and professional + newArray.push(switchStructureId(structure.socialAndProfessional, '6', '254')); + newArray.push(switchStructureId(structure.socialAndProfessional, '20', '240')); + newArray.push(switchStructureId(structure.socialAndProfessional, '66', '194')); + newArray.push(switchStructureId(structure.socialAndProfessional, '67', '193')); + newArray.push(switchStructureId(structure.socialAndProfessional, '68', '192')); + newArray.push(switchStructureId(structure.socialAndProfessional, '69', '191')); + newArray.push(switchStructureId(structure.socialAndProfessional, '124', '262')); + newArray.push(switchStructureId(structure.socialAndProfessional, '125', '263')); + newArray.push(switchStructureId(structure.socialAndProfessional, '127', '003')); + // Remove null cases + newArray = newArray.filter((obj) => obj); + structure.socialAndProfessional = newArray; + + // Base Skills + newArray = []; + newArray.push(switchStructureId(structure.baseSkills, '260', '260')); + newArray.push(switchStructureId(structure.baseSkills, '1', '259')); + newArray.push(switchStructureId(structure.baseSkills, '11', '261')); + newArray.push(switchStructureId(structure.baseSkills, '38', '222')); + newArray.push(switchStructureId(structure.baseSkills, '48', '212')); + newArray.push(switchStructureId(structure.baseSkills, '74', '186')); + newArray.push(switchStructureId(structure.baseSkills, '77', '183')); + // Remove null cases + newArray = newArray.filter((obj) => obj); + structure.baseSkills = newArray; + + // Access Right + newArray = []; + newArray.push(switchStructureId(structure.accessRight, '84', '176')); + newArray.push(switchStructureId(structure.accessRight, '85', '175')); + newArray.push(switchStructureId(structure.accessRight, '86', '174')); + newArray.push(switchStructureId(structure.accessRight, '87', '173')); + newArray.push(switchStructureId(structure.accessRight, '88', '172')); + newArray.push(switchStructureId(structure.accessRight, '89', '171')); + newArray.push(switchStructureId(structure.accessRight, '93', '167')); + newArray.push(switchStructureId(structure.accessRight, '95', '165')); + // Remove null cases + newArray = newArray.filter((obj) => obj); + structure.accessRight = newArray; + + // Parenting + newArray = []; + newArray.push(switchStructureId(structure.parentingHelp, '3', '257')); + newArray.push(switchStructureId(structure.parentingHelp, '22', '238')); + newArray.push(switchStructureId(structure.parentingHelp, '82', '178')); + newArray.push(switchStructureId(structure.parentingHelp, '94', '166')); + // Remove null cases + newArray = newArray.filter((obj) => obj); + structure.parentingHelp = newArray; + + // Digital Security + newArray = []; + newArray.push(switchStructureId(structure.digitalCultureSecurity, '2', '264')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '5', '255')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '9', '265')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '28', '232')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '34', '225')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '39', '221')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '42', '218')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '51', '209')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '52', '208')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '54', '206')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '65', '195')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '96', '164')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '97', '163')); + newArray.push(switchStructureId(structure.digitalCultureSecurity, '98', '162')); + // Remove null cases + newArray = newArray.filter((obj) => obj); + structure.digitalCultureSecurity = newArray; + + return structure; +} + +async function updateApticReferential(db) { + // Base Skills + let newModule = { modules: [] }; + const baseSkills: ApticDoc = await db.collection('categoriesformations').findOne({ id: 'baseSkills' }); + newModule.modules.push(switchId(baseSkills.modules, '260', '260')); + newModule.modules.push(switchId(baseSkills.modules, '259', '1')); + newModule.modules.push(switchId(baseSkills.modules, '261', '11')); + newModule.modules.push(switchId(baseSkills.modules, '222', '38')); + newModule.modules.push(switchId(baseSkills.modules, '212', '48')); + newModule.modules.push(switchId(baseSkills.modules, '186', '74')); + newModule.modules.push(switchId(baseSkills.modules, '183', '77')); + db.collection('categoriesformations').findOneAndUpdate( + { id: 'baseSkills' }, + { $set: { modules: newModule.modules } } + ); + + // Access Right + newModule = { modules: [] }; + const accessRight: ApticDoc = await db.collection('categoriesformations').findOne({ id: 'accessRight' }); + newModule.modules.push(switchId(accessRight.modules, '176', '84')); + newModule.modules.push(switchId(accessRight.modules, '175', '85')); + newModule.modules.push(switchId(accessRight.modules, '174', '86')); + newModule.modules.push(switchId(accessRight.modules, '173', '87')); + newModule.modules.push(switchId(accessRight.modules, '172', '88')); + newModule.modules.push(switchId(accessRight.modules, '171', '89')); + newModule.modules.push(switchId(accessRight.modules, '167', '93')); + newModule.modules.push(switchId(accessRight.modules, '165', '95')); + db.collection('categoriesformations').findOneAndUpdate( + { id: 'accessRight' }, + { $set: { modules: newModule.modules } } + ); + + // Parenting + newModule = { modules: [] }; + const parentingHelp: ApticDoc = await db.collection('categoriesformations').findOne({ id: 'parentingHelp' }); + newModule.modules.push(switchId(parentingHelp.modules, '257', '3')); + newModule.modules.push(switchId(parentingHelp.modules, '238', '22')); + newModule.modules.push(switchId(parentingHelp.modules, '178', '82')); + newModule.modules.push(switchId(parentingHelp.modules, '166', '94')); + db.collection('categoriesformations').findOneAndUpdate( + { id: 'parentingHelp' }, + { $set: { modules: newModule.modules } } + ); + + // Social and professional + newModule = { modules: [] }; + const socialAndProfessional: ApticDoc = await db + .collection('categoriesformations') + .findOne({ id: 'socialAndProfessional' }); + newModule.modules.push(switchId(socialAndProfessional.modules, '254', '6')); + newModule.modules.push(switchId(socialAndProfessional.modules, '240', '20')); + newModule.modules.push(switchId(socialAndProfessional.modules, '194', '66')); + newModule.modules.push(switchId(socialAndProfessional.modules, '193', '67')); + newModule.modules.push(switchId(socialAndProfessional.modules, '192', '68')); + newModule.modules.push(switchId(socialAndProfessional.modules, '191', '69')); + newModule.modules.push(switchId(socialAndProfessional.modules, '262', '124')); + newModule.modules.push(switchId(socialAndProfessional.modules, '263', '125')); + newModule.modules.push(switchId(socialAndProfessional.modules, '003', '127')); + db.collection('categoriesformations').findOneAndUpdate( + { id: 'socialAndProfessional' }, + { $set: { modules: newModule.modules } } + ); + + // Digital security + newModule = { modules: [] }; + const digitalCultureSecurity: ApticDoc = await db + .collection('categoriesformations') + .findOne({ id: 'digitalCultureSecurity' }); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '264', '2')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '255', '5')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '265', '9')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '232', '28')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '225', '34')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '221', '39')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '218', '42')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '209', '51')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '208', '52')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '206', '54')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '195', '65')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '164', '96')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '163', '97')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '162', '98')); + db.collection('categoriesformations').findOneAndUpdate( + { id: 'digitalCultureSecurity' }, + { $set: { modules: newModule.modules } } + ); +} + +async function downgradeApticReferential(db) { + // Base Skills + let newModule = { modules: [] }; + const baseSkills: ApticDoc = await db.collection('categoriesformations').findOne({ id: 'baseSkills' }); + newModule.modules.push(switchId(baseSkills.modules, '260', '260')); //TODO: + newModule.modules.push(switchId(baseSkills.modules, '1', '259')); + newModule.modules.push(switchId(baseSkills.modules, '11', '261')); + newModule.modules.push(switchId(baseSkills.modules, '38', '222')); + newModule.modules.push(switchId(baseSkills.modules, '48', '212')); + newModule.modules.push(switchId(baseSkills.modules, '74', '186')); + newModule.modules.push(switchId(baseSkills.modules, '77', '183')); + db.collection('categoriesformations').findOneAndUpdate( + { id: 'baseSkills' }, + { $set: { modules: newModule.modules } } + ); + + // Access Right + newModule = { modules: [] }; + const accessRight: ApticDoc = await db.collection('categoriesformations').findOne({ id: 'accessRight' }); + newModule.modules.push(switchId(accessRight.modules, '84', '176')); + newModule.modules.push(switchId(accessRight.modules, '85', '175')); + newModule.modules.push(switchId(accessRight.modules, '86', '174')); + newModule.modules.push(switchId(accessRight.modules, '87', '173')); + newModule.modules.push(switchId(accessRight.modules, '88', '172')); + newModule.modules.push(switchId(accessRight.modules, '89', '171')); + newModule.modules.push(switchId(accessRight.modules, '93', '167')); + newModule.modules.push(switchId(accessRight.modules, '95', '165')); + db.collection('categoriesformations').findOneAndUpdate( + { id: 'accessRight' }, + { $set: { modules: newModule.modules } } + ); + + // Parenting + newModule = { modules: [] }; + const parentingHelp: ApticDoc = await db.collection('categoriesformations').findOne({ id: 'parentingHelp' }); + newModule.modules.push(switchId(parentingHelp.modules, '3', '257')); + newModule.modules.push(switchId(parentingHelp.modules, '22', '238')); + newModule.modules.push(switchId(parentingHelp.modules, '82', '178')); + newModule.modules.push(switchId(parentingHelp.modules, '94', '166')); + db.collection('categoriesformations').findOneAndUpdate( + { id: 'parentingHelp' }, + { $set: { modules: newModule.modules } } + ); + + // Social and professional + newModule = { modules: [] }; + const socialAndProfessional: ApticDoc = await db + .collection('categoriesformations') + .findOne({ id: 'socialAndProfessional' }); + newModule.modules.push(switchId(socialAndProfessional.modules, '6', '254')); + newModule.modules.push(switchId(socialAndProfessional.modules, '20', '240')); + newModule.modules.push(switchId(socialAndProfessional.modules, '66', '194')); + newModule.modules.push(switchId(socialAndProfessional.modules, '67', '193')); + newModule.modules.push(switchId(socialAndProfessional.modules, '68', '192')); + newModule.modules.push(switchId(socialAndProfessional.modules, '69', '191')); + newModule.modules.push(switchId(socialAndProfessional.modules, '124', '262')); + newModule.modules.push(switchId(socialAndProfessional.modules, '125', '263')); + newModule.modules.push(switchId(socialAndProfessional.modules, '127', '003')); + db.collection('categoriesformations').findOneAndUpdate( + { id: 'socialAndProfessional' }, + { $set: { modules: newModule.modules } } + ); + + // Digital security + newModule = { modules: [] }; + const digitalCultureSecurity: ApticDoc = await db + .collection('categoriesformations') + .findOne({ id: 'digitalCultureSecurity' }); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '2', '264')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '5', '255')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '9', '265')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '28', '232')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '34', '225')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '39', '221')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '42', '218')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '51', '209')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '52', '208')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '54', '206')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '65', '195')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '96', '164')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '97', '163')); + newModule.modules.push(switchId(digitalCultureSecurity.modules, '98', '162')); + db.collection('categoriesformations').findOneAndUpdate( + { id: 'digitalCultureSecurity' }, + { $set: { modules: newModule.modules } } + ); +} + +function switchStructureId(moduleArray: [], originalId: string, newId: string): string { + const module = moduleArray.find((id) => id === originalId); + if (module) { + return newId; + } + return null; +} + +function switchId(moduleArray: ApticModule[], originalId: string, newId: string): ApticModule { + const module = moduleArray.find((skill) => skill.id === originalId); + module.id = newId; + module.display_id = newId; + module.display_name = `Modules APTIC - n°${newId}`; + delete module.receipt_time; + delete module.last_update_time; + delete module.url; + return module; +} diff --git a/src/migrations/scripts/1617962328658-add-newsletter-data.ts b/src/migrations/scripts/1617962328658-add-newsletter-data.ts new file mode 100644 index 0000000000000000000000000000000000000000..17821b927c47a2d58fb3544f34b98917d7f465e4 --- /dev/null +++ b/src/migrations/scripts/1617962328658-add-newsletter-data.ts @@ -0,0 +1,17 @@ +import { Db } from 'mongodb'; +import { getDb } from '../migrations-utils/db'; +import * as fs from 'fs'; + +export const up = async () => { + const db: Db = await getDb(); + const data = fs.readFileSync('/app/src/migrations/data/newsletter-data.json', 'utf8'); + const parsedData = JSON.parse(data); + db.collection('newslettersubscriptions').insertMany(parsedData); +}; + +export const down = async () => { + const db: Db = await getDb(); + /* + Code you downgrade script here! + */ +}; diff --git a/src/newsletter/interface/newsletter-subscription.interface.ts b/src/newsletter/interface/newsletter-subscription.interface.ts new file mode 100644 index 0000000000000000000000000000000000000000..382e0911c13768f56e091db0c420ba4df013b3fd --- /dev/null +++ b/src/newsletter/interface/newsletter-subscription.interface.ts @@ -0,0 +1,5 @@ +import { Document } from 'mongoose'; + +export interface INewsletterSubscription extends Document { + email: string; +} diff --git a/src/newsletter/newsletter-subscription.schema.ts b/src/newsletter/newsletter-subscription.schema.ts new file mode 100644 index 0000000000000000000000000000000000000000..9ff18ab015a2e00b018ac1929bed0fb0a7a7ac48 --- /dev/null +++ b/src/newsletter/newsletter-subscription.schema.ts @@ -0,0 +1,12 @@ +import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'; +import { Document } from 'mongoose'; + +export type NewsletterSubscriptionDocument = NewsletterSubscription & Document; + +@Schema({ timestamps: { createdAt: 'createdAt', updatedAt: 'updatedAt' } }) +export class NewsletterSubscription { + @Prop({ required: true }) + email: string; +} + +export const NewsletterSubscriptionSchema = SchemaFactory.createForClass(NewsletterSubscription); diff --git a/src/newsletter/newsletter.controller.spec.ts b/src/newsletter/newsletter.controller.spec.ts new file mode 100644 index 0000000000000000000000000000000000000000..2e5adbb61affbb5606e32e2768ee9c60e09f1173 --- /dev/null +++ b/src/newsletter/newsletter.controller.spec.ts @@ -0,0 +1,44 @@ +import { HttpModule } from '@nestjs/common'; +import { getModelToken } from '@nestjs/mongoose'; +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigurationModule } from '../configuration/configuration.module'; +import { NewsletterSubscription } from './newsletter-subscription.schema'; +import { NewsletterController } from './newsletter.controller'; +import { NewsletterService } from './newsletter.service'; +describe('NewsletterController', () => { + let controller: NewsletterController; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + imports: [ConfigurationModule, HttpModule], + providers: [ + NewsletterService, + { + provide: getModelToken('NewsletterSubscription'), + useValue: NewsletterSubscription, + }, + ], + controllers: [NewsletterController], + }).compile(); + + controller = module.get<NewsletterController>(NewsletterController); + }); + + it('should be defined', () => { + expect(controller).toBeDefined(); + }); + + it('should subscribe user', async () => { + const result = { email: 'email@test.com' }; + jest.spyOn(controller, 'newsletterSubscribe').mockImplementation(async (): Promise<{ email }> => result); + const email = { email: 'email@test.com' }; + expect(await controller.newsletterSubscribe(email)).toBe(result); + }); + + it('should unsubscribe user', async () => { + const result = { email: 'email@test.com' }; + jest.spyOn(controller, 'newsletterUnsubscribe').mockImplementation(async (): Promise<{ email }> => result); + const email = { email: 'email@test.com' }; + expect(await controller.newsletterUnsubscribe(email)).toBe(result); + }); +}); diff --git a/src/newsletter/newsletter.controller.ts b/src/newsletter/newsletter.controller.ts new file mode 100644 index 0000000000000000000000000000000000000000..8a7c77d35689d37df3e66c9c42d9405cc35c6655 --- /dev/null +++ b/src/newsletter/newsletter.controller.ts @@ -0,0 +1,17 @@ +import { Body, Controller, Post } from '@nestjs/common'; +import { NewsletterService } from './newsletter.service'; + +@Controller('newsletter') +export class NewsletterController { + constructor(private newsletterService: NewsletterService) {} + + @Post('subscribe') + public async newsletterSubscribe(@Body() email: { email: string }) { + return this.newsletterService.newsletterSubscribe(email.email); + } + + @Post('unsubscribe') + public async newsletterUnsubscribe(@Body() email: { email: string }) { + return this.newsletterService.newsletterUnsubscribe(email.email); + } +} diff --git a/src/newsletter/newsletter.module.ts b/src/newsletter/newsletter.module.ts new file mode 100644 index 0000000000000000000000000000000000000000..2f949c385cffe4e07c54b645c30d0221f41b31f8 --- /dev/null +++ b/src/newsletter/newsletter.module.ts @@ -0,0 +1,15 @@ +import { HttpModule, Module } from '@nestjs/common'; +import { MongooseModule } from '@nestjs/mongoose'; +import { NewsletterService } from './newsletter.service'; +import { NewsletterController } from './newsletter.controller'; +import { NewsletterSubscription, NewsletterSubscriptionSchema } from './newsletter-subscription.schema'; +@Module({ + imports: [ + MongooseModule.forFeature([{ name: NewsletterSubscription.name, schema: NewsletterSubscriptionSchema }]), + HttpModule, + ], + providers: [NewsletterService], + exports: [NewsletterService], + controllers: [NewsletterController], +}) +export class NewsletterModule {} diff --git a/src/newsletter/newsletter.service.ts b/src/newsletter/newsletter.service.ts new file mode 100644 index 0000000000000000000000000000000000000000..c1ca5efc0e07368586b7a3ac99c7248c43d8a85f --- /dev/null +++ b/src/newsletter/newsletter.service.ts @@ -0,0 +1,50 @@ +import { HttpException, HttpStatus, Injectable } from '@nestjs/common'; +import { InjectModel } from '@nestjs/mongoose'; +import { Model, Types } from 'mongoose'; +import { INewsletterSubscription } from './interface/newsletter-subscription.interface'; +import { NewsletterSubscription } from './newsletter-subscription.schema'; + +@Injectable() +export class NewsletterService { + constructor( + @InjectModel(NewsletterSubscription.name) private newsletterSubscriptionModel: Model<INewsletterSubscription> + ) {} + + public async newsletterSubscribe(email: string): Promise<NewsletterSubscription> { + const existingEmail = await this.findOne(email); + if (existingEmail) { + throw new HttpException('Email already exists', HttpStatus.BAD_REQUEST); + } + const createSubscription = new this.newsletterSubscriptionModel({ email: email }); + createSubscription.save(); + return await this.findOne(email); + } + + public async newsletterUnsubscribe(email: string): Promise<NewsletterSubscription> { + const subscription = await this.newsletterSubscriptionModel.findOne({ email: email }).exec(); + if (!subscription) { + throw new HttpException('Invalid email', HttpStatus.BAD_REQUEST); + } + return subscription.deleteOne(); + } + + public async findOne(mail: string): Promise<NewsletterSubscription | undefined> { + return this.newsletterSubscriptionModel.findOne({ email: mail }).exec(); + } + + public async searchNewsletterSubscription(searchString: string) { + return this.newsletterSubscriptionModel.find({ email: new RegExp(searchString, 'i') }).exec(); + } + + public async deleteOneEmail(mail: string): Promise<NewsletterSubscription | undefined> { + const subscription = await this.newsletterSubscriptionModel.findOne({ email: mail }).exec(); + if (!subscription) { + throw new HttpException('Invalid email', HttpStatus.BAD_REQUEST); + } + return subscription.deleteOne(); + } + + public async findAll(): Promise<NewsletterSubscription[]> { + return await this.newsletterSubscriptionModel.find().exec(); + } +} diff --git a/src/posts/posts.controller.spec.ts b/src/posts/posts.controller.spec.ts index 0c512080183b819060a4fe2451f6433a4a784e8a..39b091029861448339fa4a8c755dd634664a6d77 100644 --- a/src/posts/posts.controller.spec.ts +++ b/src/posts/posts.controller.spec.ts @@ -3,6 +3,7 @@ import { Test, TestingModule } from '@nestjs/testing'; import { ConfigurationModule } from '../configuration/configuration.module'; import { PostsController } from './posts.controller'; import { PostsService } from './posts.service'; +import { PostWithMeta } from './schemas/postWithMeta.schema'; describe('PostsController', () => { let controller: PostsController; @@ -20,4 +21,24 @@ describe('PostsController', () => { it('should be defined', () => { expect(controller).toBeDefined(); }); + + it('should get pending attachments', async () => { + const result:PostWithMeta = {posts:[], meta:{pagination: null}}; + const query = ""; + jest.spyOn(controller, 'findAll').mockImplementation(async (): Promise<any> => result); + expect(await controller.findAll(query)).toBe(result); + }); + + it('should get pending attachments', async () => { + const result = { posts:[] }; + jest.spyOn(controller, 'findAllTags').mockImplementation(async (): Promise<any> => result); + expect(await controller.findAllTags()).toBe(result); + }); + + it('should get pending attachments', async () => { + const result = { public:[], comune:[], others:[] }; + const id = "78945945" + jest.spyOn(controller, 'getPostbyId').mockImplementation(async (): Promise<any> => result); + expect(await controller.getPostbyId(id)).toBe(result); + }); }); diff --git a/src/structures/schemas/aptic-catalog.schema.ts b/src/structures/schemas/aptic-catalog.schema.ts new file mode 100644 index 0000000000000000000000000000000000000000..fa98f194fb72dbf15ae1888b07753845d97c1614 --- /dev/null +++ b/src/structures/schemas/aptic-catalog.schema.ts @@ -0,0 +1,4 @@ +export class ApticCatalog { + code: string; + label: string; +} diff --git a/src/structures/schemas/aptic-structure.schema.ts b/src/structures/schemas/aptic-structure.schema.ts index afb95430cb303041c529327f43d417c6d46792b3..cd8b6b56dcb6af349084b8c7cec68346e01853e6 100644 --- a/src/structures/schemas/aptic-structure.schema.ts +++ b/src/structures/schemas/aptic-structure.schema.ts @@ -1,43 +1,37 @@ -export class ApticStructure { - presence_id: string; - - presence_name: string; - - presence_phone: string; - - presence_address: string; - - organization_id: string; - - organization_legal_status: string; - - organization_type: string; +import { ApticCatalog } from './aptic-catalog.schema'; - gps_lat: number; - - gps_lng: number; - - postal_code: string; - - city: string; - - city_lat: number; - - city_lng: number; +export class ApticStructure { + id: string; - department: string; + name: string; - department_code: string; + phone: string; - region_name: string; + website: string; - region_code: string; + address: { + main: string; + extra: string; + zip: string; + city: string; + gpsLat: number; + gpsLng: number; + department: string; + departmentCode: string; + region: string; + regionCode: string; + }; - catalog_id: string; + organization: { + id: string; + name: string; + type: string; + legalStatus: string; + }; - service_count: number; + catalogs: string[]; - created: string; + services: ApticCatalog[]; - updated: string; + serviceCount: number; } diff --git a/src/structures/services/aptic-structures.service.ts b/src/structures/services/aptic-structures.service.ts index fff5c3326547acc5f68bebbc93d0e81ef050b198..87ab19d74d06d59d56c7adfe46afe198c044c1ab 100644 --- a/src/structures/services/aptic-structures.service.ts +++ b/src/structures/services/aptic-structures.service.ts @@ -11,47 +11,63 @@ import { Structure, StructureDocument } from '../schemas/structure.schema'; import { ApticStructure } from '../schemas/aptic-structure.schema'; import { Address } from '../schemas/address.schema'; import { UsersService } from '../../users/users.service'; +import { ApticCatalog } from '../schemas/aptic-catalog.schema'; +import { CategoriesFormationsService } from '../../categories/services/categories-formations.service'; @Injectable() export class ApticStructuresService { constructor( private readonly httpService: HttpService, private readonly userService: UsersService, + private readonly categoriesFormationsService: CategoriesFormationsService, @InjectModel(Structure.name) private structureModel: Model<StructureDocument> ) {} - public formatApticStructures(postalCodeData: any[]): any { + /** + * Get all aptic structures in the given postal codes. + * @param postalCodeData + */ + public formatApticStructures(postalCodeData: any[]): void { // Get all postal code in one array - const postalCodeArray = _.flatten( - postalCodeData.map((data) => { - return data.codesPostaux; - }) - ); + const postalCodeArray = _.flatten(postalCodeData.map((data) => data.codesPostaux)); // Call APTIC Api's - const postalCodePromises = postalCodeArray.map((postalCode) => { - return this.getApticStructures(postalCode).toPromise(); - }); - - Promise.all(postalCodePromises).then((data) => { - const structuresData = _.flatten( - data.map((tmp: { data }) => { - return tmp.data.data; - }) + postalCodeArray.map((postalCode) => { + return this.getApticStructures(postalCode).subscribe( + (res) => { + res.data.presencePoints.forEach((structure) => { + // Call aptic api for offer + this.getApticStructureOffer(structure.catalogs[0]).subscribe( + (serviceData) => { + structure.services = serviceData.data.services; + // Create structure + this.createApticStructures(structure); + }, + (err) => { + Logger.log(err); + } + ); + }); + }, + (err) => { + Logger.log(`getApticStructures error on postal code: ${postalCode}. Code: ${err}`); + } ); - // Create structures if possible - structuresData.forEach((structure) => this.createApticStructures(structure)); }); } + /** + * Create a structure for app database given an aptic structure + * @param structure ApticStructure + */ private async createApticStructures(structure: ApticStructure): Promise<any> { - this.structureAlreadyExist(structure).then((exist) => { + this.structureAlreadyExist(structure).then(async (exist) => { if (!exist) { - Logger.log(`Create structure : ${structure.presence_name}`, 'ApticStructuresService - createApticStructures'); + Logger.log(`Create structure : ${structure.name}`, 'ApticStructuresService - createApticStructures'); const createdStructure = new this.structureModel(); // Known fields - createdStructure.structureName = structure.presence_name; - createdStructure.contactPhone = structure.presence_phone; + createdStructure.structureName = structure.name; + createdStructure.contactPhone = structure.phone; // Unkown fields (but mandatory) createdStructure.contactMail = 'unknown@unknown.com'; createdStructure.labelsQualifications = ['passNumerique']; @@ -67,8 +83,14 @@ export class ApticStructuresService { createdStructure.nbTablets = null; createdStructure.nbNumericTerminal = null; // Address - createdStructure.coord = [structure.gps_lng, structure.gps_lat]; + createdStructure.coord = [structure.address.gpsLng, structure.address.gpsLat]; createdStructure.address = this.formatAddress(structure); + // Set structure offer + createdStructure.parentingHelp = await this.setModules(structure, 'parentingHelp'); + createdStructure.baseSkills = await this.setModules(structure, 'baseSkills'); + createdStructure.accessRight = await this.setModules(structure, 'accessRight'); + createdStructure.socialAndProfessional = await this.setModules(structure, 'socialAndProfessional'); + createdStructure.digitalCultureSecurity = await this.setModules(structure, 'digitalCultureSecurity'); createdStructure.save(); // Send admin weird structure mail this.verifyDuplication(createdStructure); @@ -76,23 +98,74 @@ export class ApticStructuresService { }); } + /** + * Given an aptic structure, this method return the corresponding services id by category. + * @param structure ApticStructure + * @param moduleCategory string + * @returns Promise<string[]> + */ + private async setModules(structure: ApticStructure, moduleCategory: string): Promise<string[]> { + const referentialIds = await this.categoriesFormationsService + .findOne(moduleCategory) + .then((referential) => referential.modules.map((element) => element.id)); + + return structure.services + .filter((service) => referentialIds.includes(service.code)) + .map((service) => { + return service.code; + }); + } + + /** + * Verifiy if an aptic structure already exist in database. + * - If it's true return true and update fields if needed + * - If it's false return false + * @param structure ApticStructure + * @returns boolean + */ private async structureAlreadyExist(structure: ApticStructure): Promise<boolean> { let existingStructure = await this.structureModel .findOne({ - structureName: { $regex: structure.presence_name, $options: 'i' }, + structureName: { $regex: structure.name, $options: 'i' }, }) .exec(); // Check without regex for case like 'TINEBRA*DANIEL/DANIEL/' if (!existingStructure) { - existingStructure = await this.structureModel.findOne({ structureName: structure.presence_name }).exec(); + existingStructure = await this.structureModel.findOne({ structureName: structure.name }).exec(); } if (existingStructure) { // Add aptic label if it's not the case if (!existingStructure.labelsQualifications.includes('passNumerique')) { existingStructure.labelsQualifications.push('passNumerique'); - existingStructure.save(); } + // Update service offer + existingStructure.parentingHelp = _.unionWith( + existingStructure.parentingHelp, + await this.setModules(structure, 'parentingHelp'), + _.isEqual + ); + existingStructure.baseSkills = _.unionWith( + existingStructure.baseSkills, + await this.setModules(structure, 'baseSkills'), + _.isEqual + ); + existingStructure.accessRight = _.unionWith( + existingStructure.accessRight, + await this.setModules(structure, 'accessRight'), + _.isEqual + ); + existingStructure.socialAndProfessional = _.unionWith( + existingStructure.socialAndProfessional, + await this.setModules(structure, 'socialAndProfessional'), + _.isEqual + ); + existingStructure.digitalCultureSecurity = _.unionWith( + existingStructure.digitalCultureSecurity, + await this.setModules(structure, 'digitalCultureSecurity'), + _.isEqual + ); + existingStructure.save(); return true; } return false; @@ -128,11 +201,21 @@ export class ApticStructuresService { return this.httpService.get(encodeURI(req)); } - public getApticStructures(postalCodeData: string): Observable<AxiosResponse<any>> { + public getApticStructures(postalCodeData: string): Observable<AxiosResponse<{ presencePoints: ApticStructure[] }>> { + const req = `https://aptisearch-api.aptic.fr/v1/postal-code/${postalCodeData}`; + Logger.log(`Request : ${req}`, 'ApticStructuresService'); + return this.httpService.get(req, { + headers: { + api_key: process.env.APTIC_TOKEN, + }, + }); + } + + public getApticStructureOffer(catalogId: string): Observable<AxiosResponse<{ services: ApticCatalog[] }>> { const agent = new https.Agent({ rejectUnauthorized: false, }); - const req = `https://presence.aptic.fr/postal_code/${postalCodeData}`; + const req = `https://aptisearch-api.aptic.fr/v1/catalog/${catalogId}/services`; Logger.log(`Request : ${req}`, 'ApticStructuresService'); return this.httpService.get(req, { httpsAgent: agent, @@ -161,13 +244,13 @@ export class ApticStructuresService { const address = new Address(); const regexWithSpace = /\d+\s/g; // NOSONAR const regex = /\d+/g; // NOSONAR - if (structure.presence_address.match(regex)) { - address.numero = structure.presence_address.match(regex)[0]; - address.street = structure.presence_address.replace(regexWithSpace, ''); + if (structure.address.main.match(regex)) { + address.numero = structure.address.main.match(regex)[0]; + address.street = structure.address.main.replace(regexWithSpace, ''); } else { - address.street = structure.presence_address; + address.street = structure.address.main; } - address.commune = structure.city; + address.commune = structure.address.city; return address; } } diff --git a/src/structures/services/structures.service.ts b/src/structures/services/structures.service.ts index 5f1f1fec7da52bd97fd2e3f4c0091186d2123c01..71b6bb189b6aef32dcf401e1bfcdcebe08febdb9 100644 --- a/src/structures/services/structures.service.ts +++ b/src/structures/services/structures.service.ts @@ -1,6 +1,6 @@ import { HttpException, HttpService, Injectable, HttpStatus, Logger } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; -import { Types, Model } from 'mongoose'; +import { Types, Model, FilterQuery, DocumentDefinition } from 'mongoose'; import { Observable } from 'rxjs'; import { AxiosResponse } from 'axios'; import { Structure, StructureDocument } from '../schemas/structure.schema'; @@ -41,7 +41,7 @@ export class StructuresService { user.save(); // Senc admin notification mail - this.userService.sendAdminNewStructureMail(createdStructure.structureName); + this.userService.sendAdminNewStructureMail(createdStructure.structureName, createdStructure._id); return createdStructure; } @@ -58,7 +58,7 @@ export class StructuresService { .exec(); } else if (filters) { return this.structureModel - .find({ $and: [{ $or: this.parseFilter(filters), deletedAt: { $exists: false }, accountVerified: true }] }) + .find({ $and: [{ $and: this.parseFilter(filters), deletedAt: { $exists: false }, accountVerified: true }] }) .exec(); } else { return this.structureModel @@ -162,7 +162,7 @@ export class StructuresService { const req = 'https://download.data.grandlyon.com/geocoding/photon-bal/api?q=' + data.searchQuery + - '&lat=45.75&lon=4.85&lang=fr&limit=5&osm_tag=:!construction&osm_tag=:!bus_stop'; + '&lat=45.75&lon=4.85&lang=fr&limit=50&osm_tag=:!construction&osm_tag=:!bus_stop'; return new Promise((resolve, reject) => { this.httpService .request({ @@ -187,14 +187,29 @@ export class StructuresService { * @param key structure key * @return [{id: 'key', count: 'value'}] */ - public async countByStructureKey(key: string): Promise<any> { + public async countByStructureKey(key: string, selected: { id: string; text: string }[]): Promise<any> { const uniqueElements = await this.structureModel.distinct(key).exec(); return await Promise.all( uniqueElements.map(async (value) => { + const keyList: FilterQuery<DocumentDefinition<StructureDocument>>[] = []; + keyList.push({ + [key]: { $elemMatch: { $eq: value } }, + deletedAt: { $exists: false }, + }); + if (selected && selected.length > 0) { + for (const val of selected) { + keyList.push({ + [val.text]: { $elemMatch: { $eq: val.id } }, + deletedAt: { $exists: false }, + }); + } + } return { id: value, count: await this.structureModel - .countDocuments({ $and: [{ [key]: { $elemMatch: { $eq: value } }, deletedAt: { $exists: false } }] }) + .countDocuments({ + $and: keyList, + }) .exec(), }; }) @@ -316,7 +331,7 @@ export class StructuresService { return this.userService.getStructureOwners(structureId); } - public async updateAccountVerified(idStructure: string, emailUser: string): Promise<Structure> { + public async updateAccountVerified(idStructure: string): Promise<Structure> { const structureLinked = await this.findOne(idStructure); const structure = new this.structureModel(structureLinked); if (!structure) { @@ -338,4 +353,31 @@ export class StructuresService { const owners = await this.userService.getStructureOwnersMails(idStructure, emailUser); return { structure: structure, owners: owners }; } + + public async reportStructureError(structureId: string, content: string) { + const structure = await this.findOne(structureId); + if (!structure) { + throw new HttpException('Invalid structure id', HttpStatus.BAD_REQUEST); + } + const owners = await this.userService.getStructureOwnersMails(structure._id, ''); + const admins = await this.userService.getAdmins(); + const emails = owners.map((owner) => owner.email).concat(admins.map((admin) => admin.email)); + const uniqueEmails = [...new Set(emails)]; + + const emailsObject: { email: string }[] = uniqueEmails.map((item) => { + return { email: item }; + }); + + const config = this.mailerService.config; + const ejsPath = this.mailerService.getTemplateLocation(config.templates.structureErrorReport.ejs); + const jsonConfig = this.mailerService.loadJsonConfig(config.templates.structureErrorReport.json); + + const html = await ejs.renderFile(ejsPath, { + config, + content: content, + id: structure._id, + structureName: structure.structureName, + }); + this.mailerService.send(emailsObject, jsonConfig.subject, html); + } } diff --git a/src/structures/structures.controller.ts b/src/structures/structures.controller.ts index b4aeea9bc83c7d3a6198af2d44909e4300b60f89..67e12c9a15469d46919601363dbfbe8882344fce 100644 --- a/src/structures/structures.controller.ts +++ b/src/structures/structures.controller.ts @@ -4,6 +4,7 @@ import { Delete, Get, HttpException, + HttpService, HttpStatus, Param, Post, @@ -29,11 +30,28 @@ import { StructuresService } from './services/structures.service'; @Controller('structures') export class StructuresController { constructor( + private readonly httpService: HttpService, private readonly structureService: StructuresService, private readonly userService: UsersService, private readonly tempUserService: TempUserService ) {} + /** + * Return points of given town exist. + * @param zipcode + * @returns Array of points + */ + @Get('coordinates/:zipcode') + @ApiParam({ name: 'zipcode', type: String, required: true }) + public async getCoordinates(@Param('zipcode') city: string): Promise<any> { + return await this.httpService + .get(encodeURI('https://download.data.grandlyon.com/geocoding/photon-bal/api?q=' + city)) + .toPromise() + .then(async (res) => res.data.features) + .then((data) => data.filter((cityPoint) => cityPoint.properties.city.toLowerCase().includes(city.toLowerCase()))) + .then((data) => data.map((filteredCityPoint) => filteredCityPoint.geometry.coordinates)); + } + @Post() public async create(@Body() createStructureDto: CreateStructureDto): Promise<Structure> { return this.structureService.create(createStructureDto.idUser, createStructureDto.structure); @@ -45,11 +63,8 @@ export class StructuresController { } @Put('updateAfterOwnerVerify/:id') - public async updateAfterOwnerVerify( - @Param('id') id: string, - @Body() body: { emailUser: string } - ): Promise<Structure> { - return this.structureService.updateAccountVerified(id, body.emailUser); + public async updateAfterOwnerVerify(@Param('id') id: string): Promise<Structure> { + return this.structureService.updateAccountVerified(id); } @Put(':id') @@ -74,22 +89,25 @@ export class StructuresController { return this.userService.updateStructureLinkedClaim(user.email, idStructure); } - @Get('count') - public async countCategories(): Promise<Array<{ id: string; count: number }>> { + @Post('count') + public async countCategories( + @Body() + selectedFilter: { id: string; text: string }[] + ): Promise<Array<{ id: string; count: number }>> { const data = await Promise.all([ - this.structureService.countByStructureKey('proceduresAccompaniment'), - - this.structureService.countByStructureKey('accessRight'), - this.structureService.countByStructureKey('baseSkills'), - this.structureService.countByStructureKey('parentingHelp'), - this.structureService.countByStructureKey('digitalCultureSecurity'), - this.structureService.countByStructureKey('socialAndProfessional'), - - this.structureService.countByStructureKey('publicsAccompaniment'), - this.structureService.countByStructureKey('labelsQualifications'), - this.structureService.countByStructureKey('publics'), - this.structureService.countByStructureKey('accessModality'), - this.structureService.countByStructureKey('equipmentsAndServices'), + this.structureService.countByStructureKey('proceduresAccompaniment', selectedFilter), + + this.structureService.countByStructureKey('accessRight', selectedFilter), + this.structureService.countByStructureKey('baseSkills', selectedFilter), + this.structureService.countByStructureKey('parentingHelp', selectedFilter), + this.structureService.countByStructureKey('digitalCultureSecurity', selectedFilter), + this.structureService.countByStructureKey('socialAndProfessional', selectedFilter), + + this.structureService.countByStructureKey('publicsAccompaniment', selectedFilter), + this.structureService.countByStructureKey('labelsQualifications', selectedFilter), + this.structureService.countByStructureKey('publics', selectedFilter), + this.structureService.countByStructureKey('accessModality', selectedFilter), + this.structureService.countByStructureKey('equipmentsAndServices', selectedFilter), ]); // Return a concat of all arrays return data.reduce((a, b) => [...a, ...b]); @@ -102,7 +120,12 @@ export class StructuresController { @Get(':id') public async find(@Param('id') id: string) { - return this.structureService.findOne(id); + const result = await this.structureService.findOne(id); + if (!result || result.deletedAt) { + throw new HttpException('Structure does not exist', HttpStatus.NOT_FOUND); + } else { + return result; + } } @Post(':id/withOwners') @@ -217,4 +240,9 @@ export class StructuresController { } this.userService.removeFromStructureLinked(userFromDb.email, id); } + + @Post('reportStructureError') + public async reportStructureError(@Body() data: { structureId: string; content: string }): Promise<void> { + return await this.structureService.reportStructureError(data.structureId, data.content); + } } diff --git a/src/structures/structures.module.ts b/src/structures/structures.module.ts index 51ec4d11ca7c6d9640944f69b156cfaac87f1aa3..1445999d289ff826e1504e3754d814f10ed06322 100644 --- a/src/structures/structures.module.ts +++ b/src/structures/structures.module.ts @@ -10,6 +10,7 @@ import { ApticStructuresService } from './services/aptic-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'; @Module({ imports: [ @@ -20,6 +21,7 @@ import { StructureType, StructureTypeSchema } from './structure-type/structure-t HttpModule, MailerModule, forwardRef(() => UsersModule), + CategoriesModule, TempUserModule, ], controllers: [StructuresController, StructureTypeController], diff --git a/src/users/users.service.ts b/src/users/users.service.ts index e4a081cb3fb15b0d1f88bb68abb706069729be9e..00cb1129c062731347632cc2c3b64359a63e9839 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -159,7 +159,7 @@ export class UsersService { /** * Send to all admins notification email for new structures */ - public async sendAdminNewStructureMail(structureName: string): Promise<any> { + public async sendAdminNewStructureMail(structureName: string, structureId: string): Promise<any> { const config = this.mailerService.config; const ejsPath = this.mailerService.getTemplateLocation(config.templates.adminStructureCreate.ejs); const jsonConfig = this.mailerService.loadJsonConfig(config.templates.adminStructureCreate.json); @@ -167,6 +167,7 @@ export class UsersService { const html = await ejs.renderFile(ejsPath, { config, name: structureName, + id: structureId, }); const admins = await this.getAdmins(); admins.forEach((admin) => {