From 846254a72991c38be4827d6fb126ea635ca98552 Mon Sep 17 00:00:00 2001
From: Etienne LOUPIAS <eloupias@grandlyon.com>
Date: Tue, 25 Oct 2022 08:34:35 +0000
Subject: [PATCH] feat(structure): add soft delete

---
 scripts/init-db.js                            |   1 +
 src/admin/admin.controller.ts                 |   5 +-
 src/configuration/config.ts                   |   8 ++
 .../mail-templates/adminStructureClaim.ejs    |   2 +-
 .../mail-templates/structureCancelDelete.ejs  |   4 +
 .../mail-templates/structureCancelDelete.json |   3 +
 .../structureDeletionNotification.json        |   2 +-
 .../mail-templates/structureToBeDeleted.ejs   |  15 ++
 .../mail-templates/structureToBeDeleted.json  |   3 +
 src/structures/dto/structure.dto.ts           |   1 +
 src/structures/schemas/structure.schema.ts    |   3 +
 .../services/structure.service.spec.ts        |  12 +-
 src/structures/services/structures.service.ts | 129 ++++++++++++++++--
 src/structures/structures.controller.spec.ts  |   9 +-
 src/structures/structures.controller.ts       |  29 +++-
 .../controllers/users.controller.spec.ts      |   4 +-
 src/users/controllers/users.controller.ts     |   5 +-
 src/users/services/users.service.ts           |   6 +-
 test/mock/data/structures.mock.data.ts        |   1 +
 19 files changed, 215 insertions(+), 27 deletions(-)
 create mode 100644 src/mailer/mail-templates/structureCancelDelete.ejs
 create mode 100644 src/mailer/mail-templates/structureCancelDelete.json
 create mode 100644 src/mailer/mail-templates/structureToBeDeleted.ejs
 create mode 100644 src/mailer/mail-templates/structureToBeDeleted.json

diff --git a/scripts/init-db.js b/scripts/init-db.js
index 910179178..8b54dae60 100644
--- a/scripts/init-db.js
+++ b/scripts/init-db.js
@@ -143,6 +143,7 @@ const structuresSchema = mongoose.Schema({
   nbScanners: Number,
   hours: Object,
   coord: [],
+  toBeDeletedAt: Date,
   deletedAt: Date,
   accountVerified: Boolean,
 });
diff --git a/src/admin/admin.controller.ts b/src/admin/admin.controller.ts
index 0369680c3..8a09736c0 100644
--- a/src/admin/admin.controller.ts
+++ b/src/admin/admin.controller.ts
@@ -190,9 +190,10 @@ export class AdminController {
   public async deleteUser(@Param() params) {
     const user = await this.usersService.deleteOneId(params.id);
     user.structuresLink.forEach((structureId) => {
-      this.usersService.isStructureClaimed(structureId.toString()).then((userFound) => {
+      this.usersService.isStructureClaimed(structureId.toString()).then(async (userFound) => {
         if (!userFound) {
-          this.structuresService.deleteOne(structureId.toString());
+          const structure = await this.structuresService.findOne(structureId.toString());
+          this.structuresService.deleteOne(structure);
         }
       });
     });
diff --git a/src/configuration/config.ts b/src/configuration/config.ts
index 67e56cc1e..f53310aed 100644
--- a/src/configuration/config.ts
+++ b/src/configuration/config.ts
@@ -61,6 +61,14 @@ export const config = {
       ejs: 'structureDeletionNotification.ejs',
       json: 'structureDeletionNotification.json',
     },
+    structureToBeDeleted: {
+      ejs: 'structureToBeDeleted.ejs',
+      json: 'structureToBeDeleted.json',
+    },
+    structureCancelDelete: {
+      ejs: 'structureCancelDelete.ejs',
+      json: 'structureCancelDelete.json',
+    },
     adminJobCreate: {
       ejs: 'adminJobCreate.ejs',
       json: 'adminJobCreate.json',
diff --git a/src/mailer/mail-templates/adminStructureClaim.ejs b/src/mailer/mail-templates/adminStructureClaim.ejs
index 91f347bac..cf9935dfb 100644
--- a/src/mailer/mail-templates/adminStructureClaim.ejs
+++ b/src/mailer/mail-templates/adminStructureClaim.ejs
@@ -3,7 +3,7 @@ Bonjour,<br />
 La structure <%= structureName %> a été revendiquée par <%= user.name %> <%= user.surname %>.
 <br />
 Voici les informations de la structure : <br />
-<%= structureAdress %><br />
+<%= structureAddress %><br />
 <%= structureDescription %><br />
 Et du demandeur : <br />
 <%= user.email %><br />
diff --git a/src/mailer/mail-templates/structureCancelDelete.ejs b/src/mailer/mail-templates/structureCancelDelete.ejs
new file mode 100644
index 000000000..972812db9
--- /dev/null
+++ b/src/mailer/mail-templates/structureCancelDelete.ejs
@@ -0,0 +1,4 @@
+Bonjour,<br />
+<br />
+<%= name %> <%= surname %> a annulé la suppression de votre structure <%= structureName %>, celle-ci restera visible sur
+la cartographie de Res'in.
diff --git a/src/mailer/mail-templates/structureCancelDelete.json b/src/mailer/mail-templates/structureCancelDelete.json
new file mode 100644
index 000000000..5e56bc8c4
--- /dev/null
+++ b/src/mailer/mail-templates/structureCancelDelete.json
@@ -0,0 +1,3 @@
+{
+  "subject": "La suppression d'une structure a été annulée, Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon"
+}
diff --git a/src/mailer/mail-templates/structureDeletionNotification.json b/src/mailer/mail-templates/structureDeletionNotification.json
index 3fabb0edb..ac9e6bdfe 100644
--- a/src/mailer/mail-templates/structureDeletionNotification.json
+++ b/src/mailer/mail-templates/structureDeletionNotification.json
@@ -1,3 +1,3 @@
 {
-  "subject": "Une structure à été supprimé de Res'in, Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon"
+  "subject": "Une structure a été supprimée de Res'in, Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon"
 }
diff --git a/src/mailer/mail-templates/structureToBeDeleted.ejs b/src/mailer/mail-templates/structureToBeDeleted.ejs
new file mode 100644
index 000000000..05e4659ac
--- /dev/null
+++ b/src/mailer/mail-templates/structureToBeDeleted.ejs
@@ -0,0 +1,15 @@
+Bonjour,<br />
+<br />
+<%= name %> <%= surname %> a demandé la suppression de votre structure <%= structureName %> dans Res'in.<br />
+Cette suppression sera effective le <%= toBeDeletedAt %>. Jusqu'à cette date, vous pouvez annuler cette demande de
+suppression en cliquant sur le lien ci-dessous.
+<br />
+<br />
+<br />
+
+<div style="text-align: center">
+  <a
+    href="<%= config.protocol %>://<%= config.host %><%= config.port ? ':' + config.port : '' %>/profile/structures-management"
+    >Gérer mes structures</a
+  >
+</div>
diff --git a/src/mailer/mail-templates/structureToBeDeleted.json b/src/mailer/mail-templates/structureToBeDeleted.json
new file mode 100644
index 000000000..7909fbb34
--- /dev/null
+++ b/src/mailer/mail-templates/structureToBeDeleted.json
@@ -0,0 +1,3 @@
+{
+  "subject": "Une structure va être supprimée de Res'in, Réseau des Acteurs de la Médiation Numérique de la Métropole de Lyon"
+}
diff --git a/src/structures/dto/structure.dto.ts b/src/structures/dto/structure.dto.ts
index cf647c26a..d72c0413d 100644
--- a/src/structures/dto/structure.dto.ts
+++ b/src/structures/dto/structure.dto.ts
@@ -9,6 +9,7 @@ export class StructureDto {
   numero: string;
   createdAt: Date;
   updatedAt: Date;
+  toBeDeletedAt: Date;
   deletedAt: Date;
 
   @IsNotEmpty()
diff --git a/src/structures/schemas/structure.schema.ts b/src/structures/schemas/structure.schema.ts
index f2f807dac..d708615ad 100644
--- a/src/structures/schemas/structure.schema.ts
+++ b/src/structures/schemas/structure.schema.ts
@@ -177,6 +177,9 @@ export class Structure {
   @Prop()
   coord: number[];
 
+  @Prop()
+  toBeDeletedAt: Date;
+
   @Prop()
   deletedAt: Date;
 
diff --git a/src/structures/services/structure.service.spec.ts b/src/structures/services/structure.service.spec.ts
index d71dfb428..7bb4585e3 100644
--- a/src/structures/services/structure.service.spec.ts
+++ b/src/structures/services/structure.service.spec.ts
@@ -7,6 +7,7 @@ import * as bcrypt from 'bcrypt';
 import { Types } from 'mongoose';
 import { personalOffersDataMock } from '../../../test/mock/data/personalOffers.mock.data';
 import { structureMockDto, structuresDocumentDataMock } from '../../../test/mock/data/structures.mock.data';
+import { userDetails } from '../../../test/mock/data/users.mock.data';
 import { mockParametersModel } from '../../../test/mock/services/parameters.mock.service';
 import { UsersServiceMock } from '../../../test/mock/services/user.mock.service';
 import { CategoriesFormationsService } from '../../categories/services/categories-formations.service';
@@ -18,7 +19,6 @@ import { PersonalOfferDocument } from '../../personal-offers/schemas/personal-of
 import { SearchModule } from '../../search/search.module';
 import { IUser } from '../../users/interfaces/user.interface';
 import { UsersService } from '../../users/services/users.service';
-import { StructureDto } from '../dto/structure.dto';
 import { Structure, StructureDocument } from '../schemas/structure.schema';
 import { StructuresSearchService } from './structures-search.service';
 import { StructuresService } from './structures.service';
@@ -389,6 +389,16 @@ describe('StructuresService', () => {
     // });
   });
 
+  it('should set structure to be deleted', async () => {
+    const res = await service.setToBeDeleted(userDetails[0], structuresDocumentDataMock[0]);
+    expect(res.toBeDeletedAt).toBeTruthy();
+  });
+
+  it('should cancel structure delete', async () => {
+    const res = await service.cancelDelete(userDetails[0], structuresDocumentDataMock[0]);
+    expect(res.toBeDeletedAt).toBeNull();
+  });
+
   it('should search structure', () => {
     const filters = [{ nbPrinters: '1' }];
     let res = service.search('', filters);
diff --git a/src/structures/services/structures.service.ts b/src/structures/services/structures.service.ts
index f638e0edf..a7977222e 100644
--- a/src/structures/services/structures.service.ts
+++ b/src/structures/services/structures.service.ts
@@ -574,13 +574,96 @@ export class StructuresService {
     return this.httpService.get(encodeURI(req));
   }
 
-  public async deleteOne(id: string): Promise<Structure> {
-    const structure = await this.structureModel.findById(Types.ObjectId(id)).exec();
+  /**
+   * Set the structure to be deleted in 5 weeks
+   * @param user IUser
+   * @param structure StructureDocument
+   */
+  public async setToBeDeleted(user: IUser, structure: StructureDocument): Promise<Structure> {
+    if (!structure) {
+      throw new HttpException('Invalid structure id', HttpStatus.BAD_REQUEST);
+    }
+    structure.toBeDeletedAt = DateTime.local().plus({ weeks: 5 }).setZone('Europe/Paris').toString();
+    structure.save();
+
+    this.sendToBeDeletedNotification(user, structure);
+    return structure;
+  }
+
+  /**
+   * Send an email to structure owners (except the one who asked for the deletion) to inform the structure is will be deleted in 5 weeks
+   * @param user User
+   */
+  public async sendToBeDeletedNotification(user: IUser, structure: StructureDocument): Promise<void> {
+    const config = this.mailerService.config;
+    const ejsPath = this.mailerService.getTemplateLocation(config.templates.structureToBeDeleted.ejs);
+    const jsonConfig = this.mailerService.loadJsonConfig(config.templates.structureToBeDeleted.json);
+
+    const html = await ejs.renderFile(ejsPath, {
+      config,
+      structureName: structure.structureName,
+      name: user.name,
+      surname: user.surname,
+      toBeDeletedAt: DateTime.fromISO(structure.toBeDeletedAt.toISOString())
+        .plus({ days: 1 })
+        .toLocaleString(DateTime.DATE_SHORT),
+    });
+    const owners = await this.getOwners(structure._id);
+    owners.forEach((owner) => {
+      if (!owner._id.equals(user._id)) {
+        this.mailerService.send(owner.email, jsonConfig.subject, html);
+      }
+    });
+  }
+
+  /**
+   * Cancel the structure deletion when one of the structure owners had asked the structure for deletion
+   * @param user IUser
+   * @param structure StructureDocument
+   */
+  public async cancelDelete(user: IUser, structure: StructureDocument): Promise<Structure> {
+    if (!structure) {
+      throw new HttpException('Invalid structure id', HttpStatus.BAD_REQUEST);
+    }
+    if (!structure.deletedAt) {
+      structure.toBeDeletedAt = null;
+      structure.save();
+
+      this.sendCancelDeleteNotification(user, structure);
+    }
+    return structure;
+  }
+
+  /**
+   * Send an email to other structure owners to inform an owner cancelled the structure deletion
+   * @param user User
+   */
+  public async sendCancelDeleteNotification(user: IUser, structure: StructureDocument): Promise<void> {
+    const config = this.mailerService.config;
+    const ejsPath = this.mailerService.getTemplateLocation(config.templates.structureCancelDelete.ejs);
+    const jsonConfig = this.mailerService.loadJsonConfig(config.templates.structureCancelDelete.json);
+
+    const html = await ejs.renderFile(ejsPath, {
+      config,
+      structureName: structure.structureName,
+      name: user.name,
+      surname: user.surname,
+    });
+    const owners = await this.getOwners(structure._id);
+    owners.forEach((owner) => {
+      if (!owner._id.equals(user._id)) {
+        this.mailerService.send(owner.email, jsonConfig.subject, html);
+      }
+    });
+  }
+
+  public async deleteOne(structure: StructureDocument): Promise<Structure> {
     if (!structure) {
       throw new HttpException('Invalid structure id', HttpStatus.BAD_REQUEST);
     }
     this.structuresSearchService.deleteIndexStructure(structure);
     structure.structureType = null;
+    if (structure.toBeDeletedAt) structure.toBeDeletedAt = null;
     structure.deletedAt = DateTime.local().setZone('Europe/Paris').toString();
     this.anonymizeStructure(structure).save();
     // Remove structure from owners (and check if there is a newly unattached user)
@@ -608,7 +691,6 @@ export class StructuresService {
       }
     );
 
-    //ici récupérer le user Actuel avec le service afin de remplir le mail.
     const config = this.mailerService.config;
     const ejsPath = this.mailerService.getTemplateLocation(templateLocation);
     const jsonConfig = this.mailerService.loadJsonConfig(jsonConfigLocation);
@@ -616,10 +698,8 @@ export class StructuresService {
       config,
       id: structure ? structure._id : 0,
       structureName: structure ? structure.structureName : '',
-      structureAdress: structure
-        ? structure.address.numero
-          ? `${structure.address.numero} ${structure.address.street} ${structure.address.commune}`
-          : `${structure.address.street} ${structure.address.commune}`
+      structureAddress: structure
+        ? `${structure.address.numero || ''} ${structure.address.street} ${structure.address.commune}`
         : '',
       structureDescription: structure ? structure.otherDescription : '',
       user: user,
@@ -638,8 +718,37 @@ export class StructuresService {
   }
 
   @Cron(CronExpression.EVERY_DAY_AT_4AM)
-  public async checkOutdatedStructuresInfo(): Promise<void> {
-    this.logger.debug('checkOutdatedStructuresInfo');
+  public async structuresTasksProcess(): Promise<void> {
+    this.logger.debug('structuresTasksProcess');
+
+    await this.processToBeDeletedStructures().catch((error) => {
+      this.logger.error(error);
+    });
+
+    await this.processOutdatedStructures().catch((error) => {
+      this.logger.error(error);
+    });
+  }
+
+  private async processToBeDeletedStructures(): Promise<void> {
+    this.logger.debug('processToBeDeletedStructures');
+
+    const structures = await this.structureModel
+      .find()
+      .where('toBeDeletedAt')
+      .lte(DateTime.local())
+      .where('deletedAt')
+      .exists(false)
+      .exec();
+
+    structures.forEach((structure) => {
+      this.logger.debug(`delete structure : ${structure.structureName} (${structure._id})`);
+      this.deleteOne(structure);
+    });
+  }
+
+  private async processOutdatedStructures(): Promise<void> {
+    this.logger.debug('processOutdatedStructures');
     const OUTDATED_MONTH_TO_CHECK = 6;
     const structureList = await this.findAll();
     // Get outdated structures
@@ -696,7 +805,7 @@ export class StructuresService {
   }
 
   /**
-   * Send an email to structure owner's in order to accept or decline a join request
+   * Send an email to structure owners in order to accept or decline a join request
    * @param user User
    */
   public async sendStructureJoinRequest(user: IUser, structure: StructureDocument): Promise<void> {
diff --git a/src/structures/structures.controller.spec.ts b/src/structures/structures.controller.spec.ts
index 8338823da..426f50984 100644
--- a/src/structures/structures.controller.spec.ts
+++ b/src/structures/structures.controller.spec.ts
@@ -174,7 +174,14 @@ describe('AuthController', () => {
   });
 
   it('should delete struct', async () => {
-    const res = controller.delete('6093ba0e2ab5775cfc01ed3e');
+    const req = { user: { _id: '6036721022462b001334c4bb' } };
+    const res = controller.delete(req, '6093ba0e2ab5775cfc01ed3e');
+    expect(res).toBeTruthy();
+  });
+
+  it('should cancel struct delete', async () => {
+    const req = { user: { _id: '6036721022462b001334c4bb' } };
+    const res = controller.cancelDelete(req, '6093ba0e2ab5775cfc01ed3e');
     expect(res).toBeTruthy();
   });
 
diff --git a/src/structures/structures.controller.ts b/src/structures/structures.controller.ts
index 73ddd47df..b838b55f5 100644
--- a/src/structures/structures.controller.ts
+++ b/src/structures/structures.controller.ts
@@ -12,6 +12,7 @@ import {
   Post,
   Put,
   Query,
+  Request,
   UseGuards,
 } from '@nestjs/common';
 import { ApiOperation, ApiParam, ApiTags } from '@nestjs/swagger';
@@ -26,6 +27,7 @@ import { Roles } from '../users/decorators/roles.decorator';
 import { IsStructureOwnerGuard } from '../users/guards/isStructureOwner.guard';
 import { RolesGuard } from '../users/guards/roles.guard';
 import { User } from '../users/schemas/user.schema';
+import { IUser } from '../users/interfaces/user.interface';
 import { UsersService } from '../users/services/users.service';
 import { depRegex } from './common/regex';
 import { CreateStructureDto } from './dto/create-structure.dto';
@@ -159,10 +161,31 @@ export class StructuresController {
 
   @Delete(':id')
   @UseGuards(JwtAuthGuard, IsStructureOwnerGuard)
-  @Roles('admin')
   @ApiParam({ name: 'id', type: String, required: true })
-  public async delete(@Param('id') id: string) {
-    return this.structureService.deleteOne(id);
+  public async delete(@Request() req, @Param('id') id: string) {
+    const structure = await this.structureService.findOne(id);
+    if (!structure) {
+      throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND);
+    }
+    const otherOwners: IUser[] = (await this.userService.getStructureOwners(id)).filter((owner) => {
+      return !owner._id.equals(req.user._id);
+    });
+    if (otherOwners.length) {
+      return this.structureService.setToBeDeleted(req.user, structure);
+    } else {
+      return this.structureService.deleteOne(structure);
+    }
+  }
+
+  @Post(':id/cancelDelete')
+  @UseGuards(JwtAuthGuard, IsStructureOwnerGuard)
+  @ApiParam({ name: 'id', type: String, required: true })
+  public async cancelDelete(@Request() req, @Param('id') id: string): Promise<Structure> {
+    const structure = await this.structureService.findOne(id);
+    if (!structure) {
+      throw new HttpException('Invalid Structure', HttpStatus.NOT_FOUND);
+    }
+    return this.structureService.cancelDelete(req.user, structure);
   }
 
   @Post(':id/addOwner')
diff --git a/src/users/controllers/users.controller.spec.ts b/src/users/controllers/users.controller.spec.ts
index 71d9c2ece..c976f486c 100644
--- a/src/users/controllers/users.controller.spec.ts
+++ b/src/users/controllers/users.controller.spec.ts
@@ -388,7 +388,7 @@ describe('UsersController', () => {
     it('should call isStructureClaimed for each structure linked', async () => {
       const userDeleteOneSpyer = jest.spyOn(userServiceMock, 'deleteOne');
       const isStructureClaimedSpyer = jest.spyOn(userServiceMock, 'isStructureClaimed');
-      const structureDeleteOneSpyer = jest.spyOn(structureServiceMock, 'deleteOne');
+      const structureFindOne = jest.spyOn(structureServiceMock, 'findOne');
       const userWithThreeStructures = usersMockData[3];
       userServiceMock.deleteOne.mockResolvedValueOnce(userWithThreeStructures);
       userServiceMock.isStructureClaimed.mockResolvedValue(null);
@@ -396,7 +396,7 @@ describe('UsersController', () => {
       await controller.delete({ user: { _id: '36', email: 'a@a.com' } });
       expect(userDeleteOneSpyer).toBeCalledTimes(1);
       expect(isStructureClaimedSpyer).toBeCalledTimes(3);
-      expect(structureDeleteOneSpyer).toBeCalledTimes(2);
+      expect(structureFindOne).toBeCalledTimes(2);
     });
   });
 
diff --git a/src/users/controllers/users.controller.ts b/src/users/controllers/users.controller.ts
index ac7859f29..666ce7ddc 100644
--- a/src/users/controllers/users.controller.ts
+++ b/src/users/controllers/users.controller.ts
@@ -179,9 +179,10 @@ export class UsersController {
   public async delete(@Req() req) {
     const user = await this.usersService.deleteOne(req.user.email);
     user.structuresLink.forEach((structureId) => {
-      this.usersService.isStructureClaimed(structureId.toString()).then((userFound) => {
+      this.usersService.isStructureClaimed(structureId.toString()).then(async (userFound) => {
         if (!userFound) {
-          this.structureService.deleteOne(structureId.toString());
+          const structure = await this.structureService.findOne(structureId.toString());
+          this.structureService.deleteOne(structure);
         }
       });
     });
diff --git a/src/users/services/users.service.ts b/src/users/services/users.service.ts
index d2d5b80b5..c5a934cac 100644
--- a/src/users/services/users.service.ts
+++ b/src/users/services/users.service.ts
@@ -480,10 +480,8 @@ export class UsersService {
       config,
       id: structure ? structure._id : 0,
       structureName: structure ? structure.structureName : '',
-      structureAdress: structure
-        ? structure.address.numero
-          ? `${structure.address.numero} ${structure.address.street} ${structure.address.commune}`
-          : `${structure.address.street} ${structure.address.commune}`
+      structureAddress: structure
+        ? `${structure.address.numero || ''} ${structure.address.street} ${structure.address.commune}`
         : '',
       structureDescription: structure ? structure.otherDescription : '',
       user: user,
diff --git a/test/mock/data/structures.mock.data.ts b/test/mock/data/structures.mock.data.ts
index e38728842..ddc26b41a 100644
--- a/test/mock/data/structures.mock.data.ts
+++ b/test/mock/data/structures.mock.data.ts
@@ -240,6 +240,7 @@ export const structureMockDto: StructureDto = {
   personalOffers: [],
   createdAt: new Date(),
   updatedAt: new Date(),
+  toBeDeletedAt: new Date(),
   deletedAt: new Date(),
   remoteAccompaniment: true,
   dataShareConsentDate: new Date(),
-- 
GitLab