diff --git a/package-lock.json b/package-lock.json index 3a9b022857765131e6a81f0c1973ef3a366285c7..e43f775d70e72e5ae2030c3079c539d465b56af1 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5454,15 +5454,6 @@ "@types/node": "*" } }, - "@types/bson": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@types/bson/-/bson-4.0.3.tgz", - "integrity": "sha512-mVRvYnTOZJz3ccpxhr3wgxVmSeiYinW+zlzQz3SXWaJmD1DuL05Jeq7nKw3SnbKmbleW5qrLG5vdyWe/A9sXhw==", - "dev": true, - "requires": { - "@types/node": "*" - } - }, "@types/connect": { "version": "3.4.33", "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.33.tgz", @@ -5602,26 +5593,6 @@ "integrity": "sha512-jhuKLIRrhvCPLqwPcx6INqmKeiA5EWrsCOPhrlFSrbrmU4ZMPjj5Ul/oLCMDO98XRUIwVm78xICz4EPCektzeQ==", "dev": true }, - "@types/mongodb": { - "version": "3.5.34", - "resolved": "https://registry.npmjs.org/@types/mongodb/-/mongodb-3.5.34.tgz", - "integrity": "sha512-73iy3+MiH+wxSM+hVA5jcW9ZTUaor2WKvM7hW+htOSgVb7E6/JBHOWaxj7rL1/vaxEBziKRr/VPecy3YAKqLuQ==", - "dev": true, - "requires": { - "@types/bson": "*", - "@types/node": "*" - } - }, - "@types/mongoose": { - "version": "5.10.1", - "resolved": "https://registry.npmjs.org/@types/mongoose/-/mongoose-5.10.1.tgz", - "integrity": "sha512-5yqbLHOyCQhUb7GPGW0A2dauUbhwgBvUWMzYcaUQiHdLZ8slgRp2R6i8FETZ+t5xeXpfhylYp9U7dAng7WamqQ==", - "dev": true, - "requires": { - "@types/mongodb": "*", - "@types/node": "*" - } - }, "@types/node": { "version": "14.14.6", "resolved": "https://registry.npmjs.org/@types/node/-/node-14.14.6.tgz", @@ -15103,6 +15074,11 @@ "resolved": "https://registry.npmjs.org/slug/-/slug-5.3.0.tgz", "integrity": "sha512-h7yD2UDVyMcQRv/WLSjq7HDH6ToO/22MB381zfx6/ebtdWUlGcyxpJNVHl6WFvKjIMHf5ZxANFp/srsy4mfT/w==" }, + "slugify": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/slugify/-/slugify-1.6.5.tgz", + "integrity": "sha512-8mo9bslnBO3tr5PEVFzMPIWwWnipGS0xVbYf65zxDqfNwmzYn1LpiKNrR6DlClusuvo+hDHd1zKpmfAe83NQSQ==" + }, "smart-buffer": { "version": "4.2.0", "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", diff --git a/package.json b/package.json index de7bd8d23b4c0ec75812c67592a7e0d346db95fa..7be2075c1ec1b18f00741d3d1484e6aa9cd419a6 100644 --- a/package.json +++ b/package.json @@ -60,6 +60,7 @@ "rimraf": "^3.0.2", "rxjs": "^7.5.5", "sanitize-html": "^2.7.3", + "slugify": "^1.6.5", "swagger-ui-express": "^4.6.0" }, "devDependencies": { diff --git a/scripts/data/structures.js b/scripts/data/structures.js index 72de0768adcd26c62562c8bd701b4135aac2a319..9de10021f046f404cc0eb4673f5b53e6022b8456 100644 --- a/scripts/data/structures.js +++ b/scripts/data/structures.js @@ -141,6 +141,7 @@ module.exports = { street: 'Rue de la Mairie', commune: 'Feyzin', postcode: '69320', + inseeCode: '69291', }, accountVerified: true, coord: [4.8563106, 45.6693619], @@ -257,7 +258,8 @@ module.exports = { numero: '27', street: 'Avenue de la République', commune: 'Vénissieux', - postcode: '69259', + postcode: '69200', + inseeCode: '69259', }, accountVerified: true, coord: [4.8678548, 45.7075853], @@ -305,6 +307,7 @@ module.exports = { street: 'Rue Saint Polycarpe', commune: 'Lyon 1er', postcode: '69001', + inseeCode: '69381', }, hours: { monday: { @@ -465,7 +468,8 @@ module.exports = { numero: '30 bis', street: 'Avenue Leclerc', commune: 'Rillieux-la-Pape', - postcode: '69286', + postcode: '69140', + inseeCode: '69286', }, coord: [4.9036773, 45.8142196], accountVerified: true, @@ -590,6 +594,7 @@ module.exports = { street: 'Avenue Franklin Roosevelt', commune: 'Bron', postcode: '69500', + inseeCode: '69029', }, coord: [4.9149121, 45.7311648], accountVerified: true, @@ -693,6 +698,7 @@ module.exports = { street: 'Rue Louis Normand', commune: 'Oullins', postcode: '69600', + inseeCode: '69149', }, coord: [4.8188378, 45.7157896], accountVerified: true, @@ -803,7 +809,8 @@ module.exports = { numero: '7', street: 'Rue Robert et Reynier ', commune: 'Saint-Fons', - postcode: '69199', + postcode: '69190', + inseeCode: '69199', }, coord: [4.8608585, 45.7086482], accountVerified: true, @@ -880,6 +887,7 @@ module.exports = { street: 'Avenue Général Frère', commune: 'Lyon 8e', postcode: '69008', + inseeCode: '69388', }, createdAt: '2020-11-13T14:13:00.000Z', updatedAt: '2022-04-13T14:13:00.000Z', @@ -948,6 +956,7 @@ module.exports = { street: 'Rue Neuve', commune: 'Fleurieu-sur-Saône', postcode: '69250', + inseeCode: '69085', }, createdAt: '2022-01-13T14:13:00.000Z', updatedAt: '2022-02-13T14:13:00.000Z', diff --git a/scripts/init-ctm-territories.js b/scripts/init-ctm-territories.js new file mode 100644 index 0000000000000000000000000000000000000000..3f5fabf6194f15b9e71b5740602b03339574d22f --- /dev/null +++ b/scripts/init-ctm-territories.js @@ -0,0 +1,30 @@ +// eslint-disable-next-line @typescript-eslint/no-var-requires +const axios = require('axios'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +const path = require('path'); +// eslint-disable-next-line @typescript-eslint/no-var-requires +require('dotenv').config({ path: path.resolve(__dirname, '../.env') }); + +axios + .post('http://localhost:3000/api/auth/login', { + email: 'admin@admin.com', + password: process.env.USER_PWD, + }) + .then((res) => { + const config = { + headers: { Authorization: `Bearer ${res.data.accessToken}` }, + }; + axios + .get('http://localhost:3000/api/structures/ctm/update', config) + .then((res) => { + console.log(`CTM territories | statusCode: ${res.status}`); + }) + .catch((error) => { + console.error('Error in fetching CTM territories'); + console.error(error); + }); + }) + .catch((error) => { + console.error('Error in auth'); + console.error(error); + }); diff --git a/src/categories/categories.module.ts b/src/categories/categories.module.ts index 3a02d98d3d166462090080fd68034a4aa36e5d18..63bb708ba5ef0a0b8b831ef46c6133d2ce1e2e26 100644 --- a/src/categories/categories.module.ts +++ b/src/categories/categories.module.ts @@ -3,9 +3,10 @@ import { MongooseModule } from '@nestjs/mongoose'; import { CategoriesService } from './services/categories.service'; import { Categories, CategoriesSchema } from './schemas/categories.schema'; import { CategoriesController } from './controllers/categories.controller'; +import { HttpModule } from '@nestjs/axios'; @Module({ - imports: [MongooseModule.forFeature([{ name: Categories.name, schema: CategoriesSchema }])], + imports: [MongooseModule.forFeature([{ name: Categories.name, schema: CategoriesSchema }]), HttpModule], controllers: [CategoriesController], exports: [CategoriesService], providers: [CategoriesService], diff --git a/src/categories/schemas/module.class.ts b/src/categories/schemas/module.class.ts index 15c6b2a4d3695edc8a0dff69346668a03532ff7b..66dc8264caebd2dea200891087f2251d64cbc947 100644 --- a/src/categories/schemas/module.class.ts +++ b/src/categories/schemas/module.class.ts @@ -2,4 +2,5 @@ export class Module { id: string; name: string; apticIds?: string[]; + communes?: string[]; } diff --git a/src/categories/services/categories.service.spec.ts b/src/categories/services/categories.service.spec.ts index 227d0a000ae911b91054fc5cad65dff2203e13c3..ee2726837903a0a80738dc1120834022e032fa51 100644 --- a/src/categories/services/categories.service.spec.ts +++ b/src/categories/services/categories.service.spec.ts @@ -1,3 +1,4 @@ +import { HttpModule } from '@nestjs/axios'; import { getModelToken } from '@nestjs/mongoose'; import { Test, TestingModule } from '@nestjs/testing'; import { CreateCategories } from '../dto/create-categories.dto'; @@ -16,7 +17,7 @@ describe('CategoriesService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ - imports: [], + imports: [HttpModule], providers: [ CategoriesService, { diff --git a/src/categories/services/categories.service.ts b/src/categories/services/categories.service.ts index 6e331f8cb5a0188d3d9d773c0b8fbdd411445405..8b63e7cec2b42273139ae5c1b02e6dfa4df35c00 100644 --- a/src/categories/services/categories.service.ts +++ b/src/categories/services/categories.service.ts @@ -1,12 +1,19 @@ +import { HttpService } from '@nestjs/axios'; import { Injectable, Logger } from '@nestjs/common'; import { InjectModel } from '@nestjs/mongoose'; import { Model } from 'mongoose'; +import { lastValueFrom } from 'rxjs'; +import slugify from 'slugify'; import { Categories, CategoriesDocument } from '../schemas/categories.schema'; +import { Module } from '../schemas/module.class'; @Injectable() export class CategoriesService { private readonly logger = new Logger(CategoriesService.name); - constructor(@InjectModel(Categories.name) private categoriesModel: Model<CategoriesDocument>) {} + constructor( + @InjectModel(Categories.name) private categoriesModel: Model<CategoriesDocument>, + private httpService: HttpService + ) {} public async findAll(): Promise<Categories[]> { this.logger.debug(`findAll`); @@ -22,4 +29,40 @@ export class CategoriesService { this.logger.debug(`findOneComplete`); return this.categoriesModel.findOne({ id: categoryId }).exec(); } + + public async findCtmByInseeCode(inseeCode: string): Promise<Module> { + const ctm = await this.findOneComplete('ctm'); + return ctm.modules.find((ctm) => ctm.communes.includes(inseeCode)); + } + + /** + * Clear 'categories.ctm' and fill it with data from Data Grand Lyon + */ + public async updateCTM(): Promise<Categories> { + this.logger.debug('updateCTM'); + const dataCTMs = await lastValueFrom( + this.httpService.get( + `https://download.data.grandlyon.com/ws/grandlyon/ter_territoire.direction_territoriale/all.json?maxfeatures=-1` + ) + ).then(async (res) => res.data.values); + dataCTMs.sort((a, b) => a.nom.localeCompare(b.nom)); + + const newDocCTM: Categories = { + modules: [], + name: 'Territoires métropolitains', + theme: 'Territoires métropolitains', + id: 'ctm', + }; + + for (const ctm of dataCTMs) { + newDocCTM.modules.push({ + id: slugify(ctm.nom, { lower: true, strict: true, locale: 'fr' }), + name: ctm.nom, + communes: ctm.communes, + }); + } + + await this.categoriesModel.updateOne({ id: 'ctm' }, [{ $set: newDocCTM }], { upsert: true }); + return newDocCTM; + } } diff --git a/src/structures/schemas/address.schema.ts b/src/structures/schemas/address.schema.ts index 7e822db4c5eb894d595701ec1ab6afb998bcce29..633146267703638c4b51d6dee77d13fcf24938ae 100644 --- a/src/structures/schemas/address.schema.ts +++ b/src/structures/schemas/address.schema.ts @@ -13,6 +13,17 @@ export class Address { commune: string; postcode: string; + inseeCode: string; + + static equals = function (a: Address, b: Address) { + // ignore inseeCode if not both set + return ( + a.numero === b.numero && + a.street === b.street && + a.commune === b.commune && + (a.inseeCode && b.inseeCode ? a.inseeCode === b.inseeCode : true) + ); + }; } export const AddressSchema = SchemaFactory.createForClass(Address); diff --git a/src/structures/services/structures.service.spec.ts b/src/structures/services/structures.service.spec.ts index e638eca20b335edd8cf9e36189c27d1e8d527065..45e18e3a84f2d00b6b7ec9b62a440f4e990937f6 100644 --- a/src/structures/services/structures.service.spec.ts +++ b/src/structures/services/structures.service.spec.ts @@ -159,7 +159,8 @@ const structuresSearchServiceMock = { numero: '30 bis', street: 'Avenue Leclerc', commune: 'Rillieux-la-Pape', - postcode: '69250', + postcode: '69140', + inseeCode: '69286', }, coord: [4.9036773, 45.8142196], accountVerified: true, @@ -350,7 +351,8 @@ describe('StructuresService', () => { numero: '30 bis', street: 'Avenue Leclerc', commune: 'Rillieux-la-Pape', - postcode: '69250', + postcode: '69140', + inseeCode: '69286', }, coord: [4.9036773, 45.8142196], accountVerified: true, diff --git a/src/structures/services/structures.service.ts b/src/structures/services/structures.service.ts index 28b80fd52d6bdd261a48166fa6b53d7e6e8ee82f..ae063bcabc9127bf5f2dfd433ccac148b133e7fc 100644 --- a/src/structures/services/structures.service.ts +++ b/src/structures/services/structures.service.ts @@ -7,7 +7,7 @@ import * as ejs from 'ejs'; import * as _ from 'lodash'; import { DateTime } from 'luxon'; import { DocumentDefinition, FilterQuery, Model, Types } from 'mongoose'; -import { map, Observable, tap } from 'rxjs'; +import { lastValueFrom, map, Observable, tap } from 'rxjs'; import { PendingStructureDto } from '../../admin/dto/pending-structure.dto'; import { UnclaimedStructureDto } from '../../admin/dto/unclaimed-structure-dto'; import { Categories } from '../../categories/schemas/categories.schema'; @@ -24,6 +24,7 @@ import { StructureDto } from '../dto/structure.dto'; import { UpdateStructureDto } from '../dto/update-structure.dto'; import { EquipmentsServicesEnum } from '../enum/equipmentsServices'; import { CNFSStructure } from '../interfaces/cnfs-structure.interface'; +import { Address } from '../schemas/address.schema'; import { Structure, StructureDocument } from '../schemas/structure.schema'; import { StructuresSearchService } from './structures-search.service'; @@ -46,17 +47,6 @@ export class StructuresService { return this.populateES(); } - //TODO: refactor - public fillFilters(filters: Array<any>): Promise<any[]>[] { - return filters?.map(async (elem) => { - const key = Object.keys(elem)[0]; - const modules = (await this.categoriesService.findOneComplete(elem[key])).modules; - return modules.map((module) => { - return { [elem[key]]: module.id }; - }); - }); - } - async searchForStructures( text: string, filters?: Array<any>, @@ -69,47 +59,41 @@ export class StructuresService { const results = await this.structuresSearchService.search(text, fields); const ids = results.map((result) => result.structureId); let structures; - let multipleFilters = filters ? filters.filter((elem) => Object.keys(elem)[0]?.length == 0) : null; - filters = filters?.filter((elem) => { - return Object.keys(elem)[0]?.length != 0; - }); + const andFilters = filters ? filters[0] : null; + const orFilters = filters ? filters[1] : null; - //TODO: remove useless code ? - if (multipleFilters) { - const filtersArrays = await Promise.all(this.fillFilters(multipleFilters)); - multipleFilters = [...filtersArrays]; - } if (!ids.length) { return []; } // we match ids from Elasticsearch with ids from mongoDB (and filters) and sort the result according to ElasticSearch order. - if (filters?.length > 0 && multipleFilters?.length == 0) { + if (andFilters?.length > 0 && orFilters?.length == 0) { structures = await this.structureModel .find({ _id: { $in: ids }, - $and: [...this.parseFilter(filters), { deletedAt: { $exists: false }, accountVerified: true }], + $and: [...this.parseFilter(andFilters), { deletedAt: { $exists: false }, accountVerified: true }], }) .populate('personalOffers') .populate('structureType') .limit(limit) .exec(); - } else if (filters?.length > 0 && multipleFilters?.length > 0) { + } else if (andFilters?.length > 0 && orFilters?.length > 0) { structures = await this.structureModel .find({ _id: { $in: ids }, - $or: [...this.parseFilter(multipleFilters)], - $and: [...this.parseFilter(filters), { deletedAt: { $exists: false }, accountVerified: true }], + $or: [...this.parseFilter(orFilters)], + $and: [...this.parseFilter(andFilters), { deletedAt: { $exists: false }, accountVerified: true }], }) .populate('personalOffers') .populate('structureType') .limit(limit) .exec(); - } else if (filters?.length == 0 && multipleFilters?.length > 0) { + } else if (andFilters?.length == 0 && orFilters?.length > 0) { structures = await this.structureModel .find({ _id: { $in: ids }, - $or: [...this.parseFilter(multipleFilters), { deletedAt: { $exists: false }, accountVerified: true }], + $or: [...this.parseFilter(orFilters)], + $and: [{ deletedAt: { $exists: false }, accountVerified: true }], }) .populate('personalOffers') .populate('structureType') @@ -140,19 +124,13 @@ export class StructuresService { structure.accountVerified = true; const createdStructure = new this.structureModel(structure); createdStructure._id = new Types.ObjectId(); + createdStructure.structureName = createdStructure.structureName.trim(); createdStructure.categories.selfServiceMaterial = this.getSelfServiceMaterial(createdStructure); await createdStructure.save(); - await this.getStructurePosition(createdStructure).then(async (position: StructureDocument) => { + await this.getStructurePosition(createdStructure).then(async (structureWithPosition: StructureDocument) => { return this.structuresSearchService.indexStructure( await this.structureModel - .findByIdAndUpdate( - new Types.ObjectId(createdStructure._id), - { - address: position.address, - coord: position.coord, - }, - { new: true } - ) + .findByIdAndUpdate(new Types.ObjectId(createdStructure._id), structureWithPosition, { new: true }) .populate('personalOffers') .populate('structureType') .exec() @@ -334,8 +312,15 @@ export class StructuresService { public async update(idStructure: string, updatedFields: UpdateStructureDto): Promise<Structure> { this.logger.debug(`Updating structure ${idStructure}`); const oldStructure = await this.findOne(idStructure); - const oldCategories = { ...oldStructure.categories }; + + // Update position only if address has changed (to not erase coord which may have been set manually in database) + if (updatedFields.address && !Address.equals(updatedFields.address, oldStructure.address)) { + oldStructure.address = updatedFields.address; + await this.getStructurePosition(oldStructure); + } + // Store updated categories because it will be erased by Object.assign which is a shallow copy + const oldCategories = { ...oldStructure.categories }; const updatedCategories = { ...updatedFields.categories }; const deepClone: StructureDocument = Object.assign(oldStructure, updatedFields); // Update structure categories in order to not override it @@ -346,8 +331,8 @@ export class StructuresService { }); } - if (updatedFields.address) { - await this.getStructurePosition(deepClone); + if (updatedFields.structureName) { + deepClone.structureName = deepClone.structureName.trim(); } const parsedStructure = deepClone; parsedStructure.categories.selfServiceMaterial = this.getSelfServiceMaterial(deepClone); @@ -379,65 +364,101 @@ export class StructuresService { /** * Get structures positions and add marker corresponding to those positons on the map */ - private getStructurePosition(structure: Structure): Promise<Structure> { + private async getStructurePosition(structure: Structure): Promise<Structure> { this.logger.debug('getStructurePosition'); - return new Promise((resolve, reject) => { - this.getCoord(structure.address.numero, structure.address.street, structure.address.commune, 'photon').subscribe( - (res) => { - this.getCoord( - structure.address.numero, - structure.address.street, - structure.address.commune, - 'photon-bal' - ).subscribe((resbal) => { - // check if photon-bal is more precise than photon ban - if (resbal.data.features.length > 0) { - resbal.data.features = resbal.data.features.sort((_a, b) => { - return b.properties.housenumber ? 1 : -1; - }); - const address = resbal.data.features[0]; - if (address && address.geometry) { - structure.coord = address.geometry.coordinates; - structure.address.postcode = address.properties.postcode; - } else { - this.logger.error( - `No coord found for: ${structure.address.numero} ${structure.address.street} ${structure.address.commune}`, - StructuresService.name - ); - structure.coord = []; - } - resolve(structure); - // else pick structure from photonban if it exists or throw error - } else { - if (res.data.features.length > 0) { - const address = res.data.features[0]; - if (address && address.geometry) { - structure.coord = address.geometry.coordinates; - structure.address.postcode = address.properties.postcode; - } else { - this.logger.error( - `No coord found for: ${structure.address.numero} ${structure.address.street} ${structure.address.commune}`, - StructuresService.name - ); - structure.coord = []; - } - resolve(structure); - } else { - this.logger.error( - `No structure found for: ${structure.address.numero} ${structure.address.street} ${structure.address.commune}`, - StructuresService.name - ); - } - } - }); - }, - (err) => { - this.logger.error(`Request error: ${err.config.url}`, 'StructureService'); - this.logger.error(err); - reject(err); - } + try { + let address: { + geometry: { + coordinates: [number, number]; + }; + properties: { + [key: string]: string; + }; + }; + // check if photon-bal is more precise than photon ban + const photonBalCoord = await this.getCoord( + structure.address.numero || '', + structure.address.street, + structure.address.commune, + 'photon-bal' ); - }); + if (photonBalCoord.data.features.length > 0) { + address = photonBalCoord.data.features.find((feature) => feature.properties.hasOwnProperty('housenumber')); + if (!address) { + address = photonBalCoord.data.features[0]; + } + } else { + // else pick structure from photonban if it exists or log error + const photonCoord = await this.getCoord( + structure.address.numero || '', + structure.address.street, + structure.address.commune, + 'photon' + ); + address = photonCoord.data.features.find((feature) => feature.properties.hasOwnProperty('housenumber')); + } + + if (!address || !address.geometry) { + this.logger.error( + `No coord found for: ${structure.address.numero || ''} ${structure.address.street} ${ + structure.address.commune + }`, + StructuresService.name + ); + structure.coord = []; + } else { + structure.coord = address.geometry.coordinates; + structure.address.postcode = address.properties.postcode; + } + + // set territory for CTM filter + await this.setCtmTerritory(structure); + } catch (err) { + this.logger.error(err); + } + + return structure; + } + + /** + * Get structure territory + */ + private async setCtmTerritory(structure: Structure): Promise<Structure> { + structure.categories['ctm'] = []; + if (structure.address?.postcode) { + let inseeCode: string = null; + const req = `https://apicarto.ign.fr/api/codes-postaux/communes/${structure.address.postcode}`; + this.logger.debug(`Request : ${req}`); + + const res = await lastValueFrom(this.httpService.get(encodeURI(req))); + if (res.data?.length) { + if (res.data.length == 1) { + this.logger.debug(`Insee code ${res.data[0].codeCommune} for ${structure.address.commune}`); + inseeCode = res.data[0].codeCommune; + } else { + // if several commune for this postcode, find which commune we are looking for the insee code + const communes = res.data.filter((commune) => commune.nomCommune == structure.address.commune); + if (communes.length) { + this.logger.debug(`Insee code ${communes[0].codeCommune} for ${structure.address.commune}`); + inseeCode = res.data[0].codeCommune; + } else { + this.logger.warn( + `Commune not found for ${structure.address.commune} in ${JSON.stringify(res.data)} (${req})` + ); + } + } + } + + if (inseeCode) { + structure.address.inseeCode = inseeCode; + const ctm: Module = await this.categoriesService.findCtmByInseeCode(inseeCode); + if (ctm) { + this.logger.debug(`${structure.structureName} : CTM ${ctm.id}`); + structure.categories['ctm'] = [ctm.id]; + } + } + } + return structure; } public async isClaimed(structureId: string, user: User): Promise<boolean> { @@ -564,12 +585,12 @@ export class StructuresService { ); } - public getCoord( + public async getCoord( numero: string, address: string, commune: string, scope: string - ): Observable<AxiosResponse<PhotonResponse>> { + ): Promise<AxiosResponse<PhotonResponse>> { const req = `https://download.data.grandlyon.com/geocoding/${scope}/api?q=` + (numero == null ? '' : numero + ' ') + @@ -578,7 +599,7 @@ export class StructuresService { commune; this.logger.debug('Print getCoord ' + req); - return this.httpService.get(encodeURI(req)); + return await lastValueFrom(this.httpService.get(encodeURI(req))); } /** @@ -1112,4 +1133,18 @@ export class StructuresService { .populate('personalOffers') .exec(); } + + /** + * Set structure CTM Territory with data from Data Grand Lyon + */ + public async setCTMs(): Promise<void> { + this.logger.debug('setCTMs'); + const structures: StructureDocument[] = await this.findAll(); + for (const structure of structures) { + this.setCtmTerritory(structure).then(async (updatedStructure) => { + await this.structureModel.findByIdAndUpdate(new Types.ObjectId(structure._id), updatedStructure).exec(); + }); + } + return null; + } } diff --git a/src/structures/structures.controller.ts b/src/structures/structures.controller.ts index 19f13b6fb8deac03871d8bed25557588f544a56c..6b7e1304339022e7747bf5e557def4deccd487e3 100644 --- a/src/structures/structures.controller.ts +++ b/src/structures/structures.controller.ts @@ -15,7 +15,7 @@ import { Request, UseGuards, } from '@nestjs/common'; -import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger'; +import { ApiOperation, ApiParam, ApiResponse, ApiTags } from '@nestjs/swagger'; import { Types } from 'mongoose'; import { JwtAuthGuard } from '../auth/guards/jwt-auth.guard'; import { CategoriesService } from '../categories/services/categories.service'; @@ -177,6 +177,22 @@ export class StructuresController { return this.tempUserService.getStructureTempUsers(id); } + @ApiOperation({ + description: `Mettre à jour les territoires CTM des structures à partir de Data Grand Lyon`, + }) + @ApiResponse({ + status: 204, + description: 'The CTM territories have been updated successfully.', + }) + @Get('/ctm/update') + @UseGuards(JwtAuthGuard, RolesGuard) + @Roles('admin') + public async updateCTM(): Promise<void> { + this.logger.debug('updateCTM'); + await this.categoriesService.updateCTM(); + return this.structureService.setCTMs(); + } + @Delete(':id') @UseGuards(JwtAuthGuard, IsStructureOwnerGuard) @ApiParam({ name: 'id', type: String, required: true }) diff --git a/test/mock/data/structures.mock.data.ts b/test/mock/data/structures.mock.data.ts index bbd943cacbdfbf9954367c9c4bb28b70f6ffc119..6c15cd6dec556e8ddec133e828946f82b91f6bd9 100644 --- a/test/mock/data/structures.mock.data.ts +++ b/test/mock/data/structures.mock.data.ts @@ -19,6 +19,7 @@ export const structuresDocumentDataMock: StructureDocument[] = [ street: 'Rue Alphonse Daudet', commune: 'Lyon 7ème Arrondissement', postcode: '69007', + inseeCode: '69387', }, contactMail: '', contactPhone: '', @@ -95,6 +96,7 @@ export const structuresDocumentDataMock: StructureDocument[] = [ street: 'Rue Alphonse Daudet', commune: 'Lyon 7ème Arrondissement', postcode: '69007', + inseeCode: '69387', }, contactMail: '', contactPhone: '', @@ -179,6 +181,7 @@ export const structureMockDto: StructureDto = { street: 'Rue Alphonse Daudet', commune: 'Lyon 7ème Arrondissement', postcode: '69007', + inseeCode: '69387', }, contactMail: '', contactPhone: '', diff --git a/test/mock/services/structures-for-search.mock.service.ts b/test/mock/services/structures-for-search.mock.service.ts index 816f18da0fa1e046995b76ee610ed364ed5f99af..6b0b5de417f25a08b92fc195e58ba9113287627b 100644 --- a/test/mock/services/structures-for-search.mock.service.ts +++ b/test/mock/services/structures-for-search.mock.service.ts @@ -10,6 +10,7 @@ export class StructuresForSearchServiceMock { street: 'Avenue Edouard Aynard', commune: 'Écully', postcode: '69130', + inseeCode: '69081', }, nbPrinters: 1, description: @@ -24,6 +25,7 @@ export class StructuresForSearchServiceMock { street: " Place de l'Abbe Launay", commune: 'Grézieu-la-Varenne', postcode: '69290', + inseeCode: '69094', }, nbPrinters: 1, description: null, @@ -37,6 +39,7 @@ export class StructuresForSearchServiceMock { street: 'Place de la Mairie', commune: 'La Tour-de-Salvagny', postcode: '69890', + inseeCode: '69250', }, nbPrinters: 1, description: null, @@ -50,6 +53,7 @@ export class StructuresForSearchServiceMock { street: 'Chemin Jean-Marie Vianney', commune: 'Écully', postcode: '69130', + inseeCode: '69081', }, nbPrinters: 1, description: null, @@ -63,6 +67,7 @@ export class StructuresForSearchServiceMock { street: 'Rue Tupin', commune: 'Oullins', postcode: '69600', + inseeCode: '69149', }, description: null, nbPrinters: 1, @@ -76,7 +81,8 @@ export class StructuresForSearchServiceMock { numero: '7', street: 'Rue Robert et Reynier ', commune: 'Saint-Fons', - postcode: '69199', + postcode: '69190', + inseeCode: '69199', }, nbScanners: 1, description: @@ -91,6 +97,7 @@ export class StructuresForSearchServiceMock { street: 'a Rue du Mai 1945', commune: 'Villeurbanne', postcode: '69100', + inseeCode: '69266', }, nbScanners: 1, description: "Notre rôle est de faciliter l'accès des personnes aux services nécessaires à la vie quotidienne", diff --git a/test/mock/services/structures.mock.service.ts b/test/mock/services/structures.mock.service.ts index 9dd4e40a76a99096d4aa89ea11725e4fd1066a08..421f379d32e37b4362343ed3bbd59a906d2d2c30 100644 --- a/test/mock/services/structures.mock.service.ts +++ b/test/mock/services/structures.mock.service.ts @@ -58,6 +58,7 @@ export class StructuresServiceMock { street: 'Rue Alphonse Daudet', commune: 'Lyon 7ème Arrondissement', postcode: '69007', + inseeCode: '69387', }, contactMail: '', contactPhone: '', @@ -249,7 +250,8 @@ export class StructuresServiceMock { numero: '30 bis', street: 'Avenue Leclerc', commune: 'Rillieux-la-Pape', - postcode: '69286', + postcode: '69140', + inseeCode: '69286', }, coord: [4.9036773, 45.8142196], accountVerified: true, @@ -403,7 +405,8 @@ export class StructuresServiceMock { numero: '30 bis', street: 'Avenue Leclerc', commune: 'Rillieux-la-Pape', - postcode: '69286', + postcode: '69140', + inseeCode: '69286', }, coord: [4.9036773, 45.8142196], accountVerified: true, @@ -470,6 +473,7 @@ export class StructuresServiceMock { street: 'Rue Alphonse Daudet', commune: 'Lyon 7ème Arrondissement', postcode: '69007', + inseeCode: '69387', }, contactMail: '', contactPhone: '', @@ -571,6 +575,7 @@ export class StructuresServiceMock { street: 'Rue Alphonse Daudet', commune: 'Lyon 7ème Arrondissement', postcode: '69007', + inseeCode: '69387', }, contactMail: '', contactPhone: '', @@ -677,6 +682,7 @@ export class StructuresServiceMock { street: 'Rue Alphonse Daudet', commune: 'Lyon 7ème Arrondissement', postcode: '69007', + inseeCode: '69387', }, contactMail: '', contactPhone: '', @@ -778,6 +784,7 @@ export class StructuresServiceMock { street: 'Rue Alphonse Daudet', commune: 'Lyon 7ème Arrondissement', postcode: '69007', + inseeCode: '69387', }, contactMail: '', contactPhone: '', @@ -884,6 +891,7 @@ export class StructuresServiceMock { street: 'Rue Alphonse Daudet', commune: 'Lyon 7ème Arrondissement', postcode: '69007', + inseeCode: '69387', }, contactMail: '', contactPhone: '', @@ -986,8 +994,9 @@ export class StructuresServiceMock { numero: null, street: 'Rue Alphonse Daudet', commune: 'Lyon 7ème Arrondissement', + postcode: '69007', + inseeCode: '69387', }, - postcode: '69007', contactMail: '', contactPhone: '', website: '', @@ -1065,6 +1074,7 @@ export class StructuresServiceMock { street: 'Rue Alphonse Daudet', commune: 'Lyon 7ème Arrondissement', postcode: '69007', + inseeCode: '69387', }, contactMail: '', contactPhone: '', @@ -1178,6 +1188,7 @@ export class StructuresServiceMock { street: 'Rue Alphonse Daudet', commune: 'Lyon 7ème Arrondissement', postcode: '69007', + inseeCode: '69387', }, contactMail: '', contactPhone: '', @@ -1386,6 +1397,7 @@ export const mockResinStructures: Array<Structure> = [ street: 'Rue Alphonse Daudet', commune: 'Lyon 7ème Arrondissement', postcode: '69007', + inseeCode: '69387', }, website: '', facebook: null, @@ -1490,6 +1502,7 @@ export const mockResinStructures: Array<Structure> = [ street: 'Rue Alphonse Daudet', commune: 'Lyon 7ème Arrondissement', postcode: '69007', + inseeCode: '69387', }, website: '', facebook: null, @@ -1594,6 +1607,7 @@ export const mockResinStructures: Array<Structure> = [ street: 'Rue Alphonse Daudet', commune: 'Lyon 7ème Arrondissement', postcode: '69007', + inseeCode: '69387', }, website: '', facebook: null,