diff --git a/package.json b/package.json index ba8a4fdc..0483be4f 100644 --- a/package.json +++ b/package.json @@ -59,7 +59,7 @@ "file-type-checker": "^1.0.8", "fp-ts": "^2.16.1", "jsonwebtoken": "^9.0.0", - "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.151", + "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.160", "module-alias": "^2.2.2", "monocle-ts": "^2.3.13", "multer": "^1.4.5-lts.1", diff --git a/src/app/api/notary/CustomersController.ts b/src/app/api/notary/CustomersController.ts index 83d3d1c9..2df0e275 100644 --- a/src/app/api/notary/CustomersController.ts +++ b/src/app/api/notary/CustomersController.ts @@ -7,13 +7,14 @@ import { Customer } from "le-coffre-resources/dist/Notary"; import { validateOrReject } from "class-validator"; import authHandler from "@App/middlewares/AuthHandler"; import ruleHandler from "@App/middlewares/RulesHandler"; -import { Prisma } from "@prisma/client"; +import { Documents, Prisma } from "@prisma/client"; import customerHandler from "@App/middlewares/OfficeMembershipHandlers/CustomerHandler"; +import DocumentsService from "@Services/notary/DocumentsService/DocumentsService"; @Controller() @Service() export default class CustomersController extends ApiController { - constructor(private customersService: CustomersService) { + constructor(private customersService: CustomersService, private documentsService: DocumentsService) { super(); } @@ -215,4 +216,64 @@ export default class CustomersController extends ApiController { return; } } + + /** + * @description Send a reminder to a specific customer by uid to signe specific documents + */ + @Post("/api/v1/notary/customers/:uid/send_reminder", [authHandler, ruleHandler, customerHandler]) + protected async sendReminder(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + + const documentsUid = req.body.documentsUid; + if (!documentsUid || !Array.isArray(documentsUid)) { + this.httpBadRequest(response, "Invalid or missing documents"); + return; + } + + const documentEntities : Documents[] = []; + //For each document uid, use DocumentsService.getByUid to get the document entity and add it to the documents array + for (const documentUid of documentsUid) { + console.log("documentUid", documentUid); + const documentEntity = await this.documentsService.getByUid(documentUid, { document_type : true, folder : true }); + console.log("documentEntity", documentEntity); + + if (!documentEntity) { + this.httpBadRequest(response, "Document not found"); + return; + } + documentEntities.push(documentEntity); + } + + const customerEntity = await this.customersService.getByUid(uid, { contact: true, office : true }); + console.log("customerEntity", customerEntity); + + + if (!customerEntity) { + this.httpNotFoundRequest(response, "customer not found"); + return; + } + + //Hydrate ressource with prisma entity + const customer = Customer.hydrate(customerEntity, { strategy: "excludeAll" }); + console.log("customer", customer); + + + // Call service to send reminder with documents + const reminder = await this.customersService.sendDocumentsReminder(customer, documentEntities); + + console.log("Reminder sent", reminder); + + + // Success + this.httpSuccess(response, customer); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } } diff --git a/src/app/api/notary/DocumentsNotaryController.ts b/src/app/api/notary/DocumentsNotaryController.ts new file mode 100644 index 00000000..d6cf82e2 --- /dev/null +++ b/src/app/api/notary/DocumentsNotaryController.ts @@ -0,0 +1,207 @@ +import { Response, Request } from "express"; +import { Controller, Delete, Get, Post } from "@ControllerPattern/index"; +import ApiController from "@Common/system/controller-pattern/ApiController"; +import { Service } from "typedi"; +import DocumentsNotaryService from "@Services/notary/DocumentsNotaryService/DocumentsNotaryService"; +import { DocumentsNotary, Prisma } from "@prisma/client"; +import { Document, DocumentNotary, FileNotary } from "le-coffre-resources/dist/Notary"; +import authHandler from "@App/middlewares/AuthHandler"; +import ruleHandler from "@App/middlewares/RulesHandler"; +import documentHandler from "@App/middlewares/OfficeMembershipHandlers/DocumentHandler"; +import OfficeFoldersService from "@Services/notary/OfficeFoldersService/OfficeFoldersService"; +import CustomersService from "@Services/admin/CustomersService/CustomersService"; +import UsersService from "@Services/notary/UsersService/UsersService"; +import FilesService from "@Services/common/FilesService/FilesService"; +// import NotificationBuilder from "@Common/notifications/NotificationBuilder"; + +@Controller() +@Service() +export default class DocumentsNotaryController extends ApiController { + constructor( + private documentsNotaryService: DocumentsNotaryService, + private officeFoldersService: OfficeFoldersService, + private customerService: CustomersService, + private userService: UsersService, + private filesService: FilesService, + ) { + super(); + } + + /** + * @description Get all documents + * @returns IDocument[] list of documents + */ + @Get("/api/v1/notary/documents_notary", [authHandler, ruleHandler]) + protected async get(req: Request, response: Response) { + try { + //get query + let query: Prisma.DocumentsNotaryFindManyArgs = {}; + if (req.query["q"]) { + query = JSON.parse(req.query["q"] as string); + if (query.where?.uid) { + this.httpBadRequest(response, "You can't filter by uid"); + return; + } + } + const officeId: string = req.body.user.office_Id; + const officeWhereInput: Prisma.OfficesWhereInput = { uid: officeId }; + if (!query.where) query.where = { folder: { office: officeWhereInput } }; + query.where.folder!.office = officeWhereInput; + + //call service to get prisma entity + const documentEntities = await this.documentsNotaryService.get(query); + + //Hydrate ressource with prisma entity + const documents = Document.hydrateArray(documentEntities, { strategy: "excludeAll" }); + + //success + this.httpSuccess(response, documents); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + /** + * @description Create a new document + * @returns IDocument created + */ + @Post("/api/v1/notary/documents_notary", [authHandler]) + protected async post(req: Request, response: Response) { + try { + if(!req.file) return; + + const customer = await this.customerService.getByUid(req.body.customerUid); + if (!customer) return; + console.log("customer", customer); + + const folder = await this.officeFoldersService.getByUid(req.body.folderUid); + if (!folder) return; + console.log("folder", folder); + + const user = await this.userService.getByUid(req.body.user.userId); + if (!user) return; + + const documentNotaryEntity = DocumentNotary.hydrate({ + customer: customer, + folder: folder, + depositor: user, + }); + + const documentNotaryEntityCreated = await this.documentsNotaryService.create(documentNotaryEntity); + console.log("documentNotaryEntityCreated", documentNotaryEntityCreated); + + const query = JSON.stringify({ document: { uid: documentNotaryEntityCreated.uid } }); + + const fileEntity = FileNotary.hydrate(JSON.parse(query)); + + const fileEntityCreated = await this.filesService.createFileNotary(fileEntity, req.file); + console.log("fileEntityCreated", fileEntityCreated); + + + + // //init Document resource with request body values + // const documentNotaryEntity = DocumentNotary.hydrate(req.body); + // console.log(documentNotaryEntity); + + // const folder = await this.officeFoldersService.getByUid(documentNotaryEntity.folder?.uid!, { + // folder_anchor: true, + // }); + // if (!folder) { + // this.httpBadRequest(response, "Folder not found"); + // return; + // } + + // const folderRessource = OfficeFolder.hydrate(folder); + // if (folderRessource.folder_anchor) { + // this.httpBadRequest(response, "Cannot add document on an anchored or anchoring folder"); + // return; + // } + + // //validate document + // await validateOrReject(documentNotaryEntity, { groups: ["createDocument"], forbidUnknownValues: false }); + + // //call service to get prisma entity + // const documentNotaryEntityCreated = await this.documentsNotaryService.create(documentNotaryEntity); + + // //Hydrate ressource with prisma entity + // const documentNotary = DocumentNotary.hydrate(documentNotaryEntityCreated, { + // strategy: "excludeAll", + // }); + + // //success + // this.httpCreated(response, documentNotary); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + /** + * @description Delete a specific document + */ + @Delete("/api/v1/notary/documents_notary/:uid", [authHandler, ruleHandler, documentHandler]) + protected async delete(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + + const documentFound = await this.documentsNotaryService.getByUid(uid); + + if (!documentFound) { + this.httpNotFoundRequest(response, "document not found"); + return; + } + + //call service to get prisma entity + const documentEntity: DocumentsNotary = await this.documentsNotaryService.delete(uid); + + //Hydrate ressource with prisma entity + const document = Document.hydrate(documentEntity, { strategy: "excludeAll" }); + + //success + this.httpSuccess(response, document); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + /** + * @description Get a specific document by uid + */ + @Get("/api/v1/notary/documents_notary/:uid", [authHandler, ruleHandler, documentHandler]) + protected async getOneByUid(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + //get query + let query; + if (req.query["q"]) { + query = JSON.parse(req.query["q"] as string); + } + + const documentEntity = await this.documentsNotaryService.getByUid(uid, query); + + if (!documentEntity) { + this.httpNotFoundRequest(response, "document not found"); + return; + } + + //Hydrate ressource with prisma entity + const document = Document.hydrate(documentEntity, { strategy: "excludeAll" }); + + //success + this.httpSuccess(response, document); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } +} diff --git a/src/app/api/notary/DocumentsReminderController.ts b/src/app/api/notary/DocumentsReminderController.ts new file mode 100644 index 00000000..27829d6d --- /dev/null +++ b/src/app/api/notary/DocumentsReminderController.ts @@ -0,0 +1,87 @@ +import { Response, Request } from "express"; +import { Controller, Get } from "@ControllerPattern/index"; +import ApiController from "@Common/system/controller-pattern/ApiController"; +import { Service } from "typedi"; +import { Prisma } from "@prisma/client"; +import { DocumentReminder } from "le-coffre-resources/dist/Notary"; + +import authHandler from "@App/middlewares/AuthHandler"; + +import DocumentsReminderService from "@Services/notary/DocumentsReminder/DocumentsReminder"; +// import NotificationBuilder from "@Common/notifications/NotificationBuilder"; + +@Controller() +@Service() +export default class DocumentsReminderController extends ApiController { + constructor( + private documentsReminderService: DocumentsReminderService, + ) { + super(); + } + + /** + * @description Get all documents + * @returns IDocument[] list of documents + */ + @Get("/api/v1/notary/document_reminders", [authHandler]) + protected async get(req: Request, response: Response) { + try { + //get query + let query: Prisma.DocumentsReminderFindManyArgs = {}; + if (req.query["q"]) { + query = JSON.parse(req.query["q"] as string); + if(query.where?.uid) { + this.httpBadRequest(response, "You can't filter by uid"); + return; + } + } + + //call service to get prisma entity + const documentReminderEntities = await this.documentsReminderService.get(query); + + //Hydrate ressource with prisma entity + const documentReminders = DocumentReminder.hydrateArray(documentReminderEntities, { strategy: "excludeAll" }); + + //success + this.httpSuccess(response, documentReminders); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + // /** + // * @description Get a specific document by uid + // */ + // @Get("/api/v1/notary/document_reminders/:uid", [authHandler, ruleHandler, documentHandler]) + // protected async getOneByUid(req: Request, response: Response) { + // try { + // const uid = req.params["uid"]; + // if (!uid) { + // this.httpBadRequest(response, "No uid provided"); + // return; + // } + // //get query + // let query; + // if (req.query["q"]) { + // query = JSON.parse(req.query["q"] as string); + // } + + // const documentEntity = await this.documentsNotaryService.getByUid(uid, query); + + // if (!documentEntity) { + // this.httpNotFoundRequest(response, "document not found"); + // return; + // } + + // //Hydrate ressource with prisma entity + // const document = Document.hydrate(documentEntity, { strategy: "excludeAll" }); + + // //success + // this.httpSuccess(response, document); + // } catch (error) { + // this.httpInternalError(response, error); + // return; + // } + // } +} diff --git a/src/app/index.ts b/src/app/index.ts index 3b747d8a..56b47310 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -55,6 +55,8 @@ import StripeWebhooks from "@Common/webhooks/stripeWebhooks"; import RulesGroupsController from "./api/admin/RulesGroupsController"; import NotesController from "./api/customer/NotesController"; import MailchimpController from "./api/notary/MailchimpController"; +import DocumentsReminderController from "./api/notary/DocumentsReminderController"; +import DocumentsNotaryController from "./api/notary/DocumentsNotaryController"; /** * @description This allow to declare all controllers used in the application @@ -118,5 +120,7 @@ export default { Container.get(RulesGroupsController); Container.get(NotesController); Container.get(MailchimpController); + Container.get(DocumentsReminderController) + Container.get(DocumentsNotaryController) }, }; diff --git a/src/common/databases/migrations/20240830075809_notary_documents/migration.sql b/src/common/databases/migrations/20240830075809_notary_documents/migration.sql new file mode 100644 index 00000000..c0136620 --- /dev/null +++ b/src/common/databases/migrations/20240830075809_notary_documents/migration.sql @@ -0,0 +1,45 @@ +-- CreateTable +CREATE TABLE "documents_notary" ( + "uid" TEXT NOT NULL, + "folder_uid" VARCHAR(255) NOT NULL, + "depositor_uid" VARCHAR(255) NOT NULL, + "created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3), + + CONSTRAINT "documents_notary_pkey" PRIMARY KEY ("uid") +); + +-- CreateTable +CREATE TABLE "files_notary" ( + "uid" TEXT NOT NULL, + "document_uid" VARCHAR(255) NOT NULL, + "file_path" VARCHAR(255) NOT NULL, + "file_name" VARCHAR(255) NOT NULL, + "mimetype" VARCHAR(255) NOT NULL, + "hash" VARCHAR(255) NOT NULL, + "size" INTEGER NOT NULL, + "archived_at" TIMESTAMP(3), + "key" VARCHAR(255), + "created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3), + + CONSTRAINT "files_notary_pkey" PRIMARY KEY ("uid") +); + +-- CreateIndex +CREATE UNIQUE INDEX "documents_notary_uid_key" ON "documents_notary"("uid"); + +-- CreateIndex +CREATE UNIQUE INDEX "files_notary_uid_key" ON "files_notary"("uid"); + +-- CreateIndex +CREATE UNIQUE INDEX "files_notary_file_path_key" ON "files_notary"("file_path"); + +-- AddForeignKey +ALTER TABLE "documents_notary" ADD CONSTRAINT "documents_notary_folder_uid_fkey" FOREIGN KEY ("folder_uid") REFERENCES "office_folders"("uid") ON DELETE RESTRICT ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "documents_notary" ADD CONSTRAINT "documents_notary_depositor_uid_fkey" FOREIGN KEY ("depositor_uid") REFERENCES "users"("uid") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "files_notary" ADD CONSTRAINT "files_notary_document_uid_fkey" FOREIGN KEY ("document_uid") REFERENCES "documents_notary"("uid") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/common/databases/migrations/20240906073736_email_reminder/migration.sql b/src/common/databases/migrations/20240906073736_email_reminder/migration.sql new file mode 100644 index 00000000..04034ca4 --- /dev/null +++ b/src/common/databases/migrations/20240906073736_email_reminder/migration.sql @@ -0,0 +1,16 @@ +-- CreateTable +CREATE TABLE "documents_reminder" ( + "uid" TEXT NOT NULL, + "document_uid" VARCHAR(255) NOT NULL, + "reminder_date" TIMESTAMP(3) NOT NULL, + "created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3), + + CONSTRAINT "documents_reminder_pkey" PRIMARY KEY ("uid") +); + +-- CreateIndex +CREATE UNIQUE INDEX "documents_reminder_uid_key" ON "documents_reminder"("uid"); + +-- AddForeignKey +ALTER TABLE "documents_reminder" ADD CONSTRAINT "documents_reminder_document_uid_fkey" FOREIGN KEY ("document_uid") REFERENCES "documents"("uid") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/common/databases/migrations/20240909032319_document_notary_customer/migration.sql b/src/common/databases/migrations/20240909032319_document_notary_customer/migration.sql new file mode 100644 index 00000000..9b636a65 --- /dev/null +++ b/src/common/databases/migrations/20240909032319_document_notary_customer/migration.sql @@ -0,0 +1,17 @@ +/* + Warnings: + + - You are about to drop the column `archived_at` on the `files_notary` table. All the data in the column will be lost. + - You are about to drop the column `key` on the `files_notary` table. All the data in the column will be lost. + - Added the required column `customer_uid` to the `documents_notary` table without a default value. This is not possible if the table is not empty. + +*/ +-- AlterTable +ALTER TABLE "documents_notary" ADD COLUMN "customer_uid" VARCHAR(255) NOT NULL; + +-- AlterTable +ALTER TABLE "files_notary" DROP COLUMN "archived_at", +DROP COLUMN "key"; + +-- AddForeignKey +ALTER TABLE "documents_notary" ADD CONSTRAINT "documents_notary_customer_uid_fkey" FOREIGN KEY ("customer_uid") REFERENCES "customers"("uid") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/common/databases/schema.prisma b/src/common/databases/schema.prisma index 259c59f1..a88f4839 100644 --- a/src/common/databases/schema.prisma +++ b/src/common/databases/schema.prisma @@ -69,6 +69,7 @@ model Users { votes Votes[] user_notifications UserNotifications[] seats Seats[] + documents_notary DocumentsNotary[] @@map("users") } @@ -129,6 +130,7 @@ model Customers { office Offices @relation(fields: [office_uid], references: [uid], onDelete: Cascade) office_uid String @db.VarChar(255) notes Notes[] + documents_notary DocumentsNotary[] @@map("customers") } @@ -170,6 +172,7 @@ model OfficeFolders { stakeholders Users[] @relation("OfficeFolderHasStakeholders") customers Customers[] @relation("OfficeFolderHasCustomers") documents Documents[] + documents_notary DocumentsNotary[] folder_anchor OfficeFolderAnchors? @relation(fields: [folder_anchor_uid], references: [uid]) folder_anchor_uid String? @unique @db.VarChar(255) @@ -215,10 +218,37 @@ model Documents { updated_at DateTime? @updatedAt files Files[] document_history DocumentHistory[] + reminders DocumentsReminder[] @@map("documents") } +model DocumentsNotary { + uid String @id @unique @default(uuid()) + folder OfficeFolders @relation(fields: [folder_uid], references: [uid]) + folder_uid String @db.VarChar(255) + depositor Users @relation(fields: [depositor_uid], references: [uid], onDelete: Cascade) + depositor_uid String @db.VarChar(255) + created_at DateTime? @default(now()) + updated_at DateTime? @updatedAt + files FilesNotary[] + customer Customers @relation(fields: [customer_uid], references: [uid], onDelete: Cascade) + customer_uid String @db.VarChar(255) + + @@map("documents_notary") +} + +model DocumentsReminder { + uid String @id @unique @default(uuid()) + document Documents @relation(fields: [document_uid], references: [uid], onDelete: Cascade) + document_uid String @db.VarChar(255) + reminder_date DateTime + created_at DateTime? @default(now()) + updated_at DateTime? @updatedAt + + @@map("documents_reminder") +} + model DocumentHistory { uid String @id @unique @default(uuid()) document_status EDocumentStatus @default(ASKED) @@ -248,6 +278,21 @@ model Files { @@map("files") } +model FilesNotary { + uid String @id @unique @default(uuid()) + document_notary DocumentsNotary @relation(fields: [document_uid], references: [uid], onDelete: Cascade) + document_uid String @db.VarChar(255) + file_path String @unique @db.VarChar(255) + file_name String @db.VarChar(255) + mimetype String @db.VarChar(255) + hash String @db.VarChar(255) + size Int + created_at DateTime? @default(now()) + updated_at DateTime? @updatedAt + + @@map("files_notary") +} + model DocumentTypes { uid String @id @unique @default(uuid()) name String @db.VarChar(255) diff --git a/src/common/emails/EmailBuilder.ts b/src/common/emails/EmailBuilder.ts index f3296d13..7d795ba9 100644 --- a/src/common/emails/EmailBuilder.ts +++ b/src/common/emails/EmailBuilder.ts @@ -7,6 +7,7 @@ import MailchimpService from "@Services/common/MailchimpService/MailchimpService import { BackendVariables } from "@Common/config/variables/Variables"; import UsersService from "@Services/super-admin/UsersService/UsersService"; import User from "le-coffre-resources/dist/SuperAdmin"; +import { Customer } from "le-coffre-resources/dist/Notary"; @Service() export default class EmailBuilder { @@ -138,4 +139,31 @@ export default class EmailBuilder { } + + public async sendReminder(customer: Customer, documents: Documents[]) { + const to = customer.contact!.email; + const templateVariables = { + office_name: customer.office?.name, + last_name: customer.contact!.last_name, + first_name: customer.contact!.first_name, + link: this.variables.APP_HOST, + }; + + const templateName = ETemplates.DOCUMENT_REMINDER; + const subject = "Vous avez des documents à déposer pour votre dossier."; + + this.mailchimpService.create({ + templateName, + to, + subject, + templateVariables, + uid: "", + from: null, + cc: [], + cci: [], + sentAt: null, + nbTrySend: null, + lastTrySendDate: null, + }); + } } diff --git a/src/common/emails/Templates/EmailTemplates.ts b/src/common/emails/Templates/EmailTemplates.ts index 520098e2..4943b885 100644 --- a/src/common/emails/Templates/EmailTemplates.ts +++ b/src/common/emails/Templates/EmailTemplates.ts @@ -3,4 +3,5 @@ export const ETemplates = { DOCUMENT_REFUSED: "DOCUMENT_REFUSED", DOCUMENT_RECAP: "DOCUMENT_RECAP", SUBSCRIPTION_INVITATION: "SUBSCRIPTION_INVITATION", + DOCUMENT_REMINDER: "DOCUMENT_REMINDER", }; \ No newline at end of file diff --git a/src/common/repositories/DocumentsNotaryRepository.ts b/src/common/repositories/DocumentsNotaryRepository.ts new file mode 100644 index 00000000..4d785adb --- /dev/null +++ b/src/common/repositories/DocumentsNotaryRepository.ts @@ -0,0 +1,102 @@ +import Database from "@Common/databases/database"; +import BaseRepository from "@Repositories/BaseRepository"; +import { Service } from "typedi"; +import { DocumentsNotary, Prisma } from "@prisma/client"; +import { DocumentNotary } from "le-coffre-resources/dist/Notary"; + +@Service() +export default class DocumentsNotaryRepository extends BaseRepository { + constructor(private database: Database) { + super(); + } + protected get model() { + return this.database.getClient().documentsNotary; + } + protected get instanceDb() { + return this.database.getClient(); + } + + /** + * @description : Find many documents + */ + public async findMany(query: Prisma.DocumentsNotaryFindManyArgs) { + query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows); + return this.model.findMany(query); + } + + /** + * @description : Create a document + */ + public async create(document: DocumentNotary): Promise { + const createArgs: Prisma.DocumentsNotaryCreateArgs = { + data: { + folder: { + connect: { + uid: document.folder!.uid, + }, + }, + depositor: { + connect: { + uid: document.depositor!.uid, + }, + }, + customer: { + connect: { + uid: document.customer!.uid, + }, + }, + }, + }; + + const documentCreated = await this.model.create({ ...createArgs }); + + return documentCreated; + } + + /** + * @description : Delete a document + */ + public async delete(uid: string): Promise { + return this.model.delete({ + where: { + uid: uid, + }, + }); + } + + /** + * @description : Find unique document + */ + public async findOneByUid(uid: string, query?: Prisma.DocumentsNotaryInclude): Promise { + return this.model.findUnique({ + where: { + uid: uid, + }, + include: query, + }); + } + + /** + * @description : Find unique document with relations + */ + public async findOneByUidWithOffice(uid: string) { + return this.model.findUnique({ + where: { + uid: uid, + }, + include: { folder: { include: { office: true } } }, + }); + } + + /** + * @description : Find unique document with relations + */ + public async findOneByUidWithFiles(uid: string) { + return this.model.findUnique({ + where: { + uid: uid, + }, + include: { files: true }, + }); + } +} diff --git a/src/common/repositories/DocumentsReminderRepository.ts b/src/common/repositories/DocumentsReminderRepository.ts new file mode 100644 index 00000000..1b5b1ac2 --- /dev/null +++ b/src/common/repositories/DocumentsReminderRepository.ts @@ -0,0 +1,46 @@ +import Database from "@Common/databases/database"; +import BaseRepository from "@Repositories/BaseRepository"; +import { Service } from "typedi"; +import { DocumentsReminder, Prisma } from "@prisma/client"; +import { DocumentReminder } from "le-coffre-resources/dist/Notary"; + +@Service() +export default class DocumentsReminderRepository extends BaseRepository { + constructor(private database: Database) { + super(); + } + protected get model() { + return this.database.getClient().documentsReminder; + } + protected get instanceDb() { + return this.database.getClient(); + } + + /** + * @description : Find many documents + */ + public async findMany(query: Prisma.DocumentsReminderFindManyArgs) { + query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows); + return this.model.findMany(query); + } + + /** + * @description : Create a document + */ + public async create(documentReminder: DocumentReminder): Promise { + const createArgs: Prisma.DocumentsReminderCreateArgs = { + data: { + document: { + connect: { + uid: documentReminder.document!.uid, + }, + }, + reminder_date: new Date(), + }, + }; + + const documentReminderCreated = await this.model.create({ ...createArgs }); + + return documentReminderCreated; + } +} diff --git a/src/common/repositories/FilesNotaryRepository.ts b/src/common/repositories/FilesNotaryRepository.ts new file mode 100644 index 00000000..6f05e843 --- /dev/null +++ b/src/common/repositories/FilesNotaryRepository.ts @@ -0,0 +1,83 @@ +import Database from "@Common/databases/database"; +import BaseRepository from "@Repositories/BaseRepository"; +import { Service } from "typedi"; +import { FilesNotary, Prisma } from "@prisma/client"; +import { FileNotary } from "le-coffre-resources/dist/Notary"; + +@Service() +export default class FilesNotaryRepository extends BaseRepository { + constructor(private database: Database) { + super(); + } + protected get model() { + return this.database.getClient().filesNotary; + } + protected get instanceDb() { + return this.database.getClient(); + } + + /** + * @description : Find many files + */ + public async findMany(query: Prisma.FilesNotaryFindManyArgs) { + query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows); + return this.model.findMany(query); + } + + /** + * @description : Create a file linked to a document + */ + public async create(file: FileNotary, key: string): Promise { + const createArgs: Prisma.FilesNotaryCreateArgs = { + data: { + document_notary: { + connect: { + uid: file.document!.uid, + }, + }, + file_name: file.file_name, + file_path: file.file_path, + mimetype: file.mimetype, + hash: file.hash, + size: file.size, + }, + }; + return this.model.create({ ...createArgs, include: { document_notary: true } }); + } + + /** + * @description : Find unique file + */ + public async findOneByUid(uid: string, query?: Prisma.FilesNotaryInclude) { + return this.model.findUnique({ + where: { + uid: uid, + }, + include: query, + }); + } + + /** + * @description : Find unique file with office + */ + public async findOneByUidWithOffice(uid: string) { + return this.model.findUnique({ + where: { + uid: uid, + }, + include: { document_notary: { include: { folder: { include: { office: true } } } } }, + }); + } + + /** + * @description : Find unique file with document + */ + public async findOneByUidWithDocument(uid: string) { + return this.model.findUnique({ + where: { + uid: uid, + }, + include: { document_notary: true }, + }); + } +} diff --git a/src/services/common/FilesService/FilesService.ts b/src/services/common/FilesService/FilesService.ts index ef02f19b..0e41dc52 100644 --- a/src/services/common/FilesService/FilesService.ts +++ b/src/services/common/FilesService/FilesService.ts @@ -8,6 +8,8 @@ import { BackendVariables } from "@Common/config/variables/Variables"; import { Readable } from "stream"; import { v4 } from "uuid"; import { Files, Prisma } from "@prisma/client"; +import { FileNotary } from "le-coffre-resources/dist/Notary"; +import FilesNotaryRepository from "@Repositories/FilesNotaryRepository"; @Service() export default class FilesService extends BaseService { @@ -16,6 +18,7 @@ export default class FilesService extends BaseService { private ipfsService: IpfsService, private variables: BackendVariables, private cryptoService: CryptoService, + private filesNotaryRepository: FilesNotaryRepository ) { super(); } @@ -85,6 +88,27 @@ export default class FilesService extends BaseService { return this.filesRepository.create(fileToCreate, key); } + /** + * @description : Create a new file + * @throws {Error} If file cannot be created + */ + public async createFileNotary(file: FileNotary, fileData: Express.Multer.File) { + const key = v4(); + const encryptedFile = await this.cryptoService.encrypt(fileData.buffer, key); + const hash = await this.cryptoService.getHash(fileData.buffer); + + const upload = await this.ipfsService.pinFile(Readable.from(encryptedFile), fileData.originalname); + let fileToCreate: FileNotary = file; + fileToCreate.file_name = fileData.originalname; + fileToCreate.file_path = this.variables.PINATA_GATEWAY.concat(upload.IpfsHash); + fileToCreate.mimetype = fileData.mimetype; + fileToCreate.size = fileData.size; + fileToCreate.hash = hash; + fileToCreate.archived_at = null; + + return this.filesNotaryRepository.create(fileToCreate, key); + } + /** * @description : Modify a new file * @throws {Error} If file cannot be modified diff --git a/src/services/common/IdNotService/IdNotService.ts b/src/services/common/IdNotService/IdNotService.ts index f18721ad..e9a8cda1 100644 --- a/src/services/common/IdNotService/IdNotService.ts +++ b/src/services/common/IdNotService/IdNotService.ts @@ -113,24 +113,22 @@ export default class IdNotService extends BaseService { super(); } - public async getIdNotToken(code: string) { + public async getIdNotToken(code: string) { const query = new URLSearchParams({ client_id: this.variables.IDNOT_CLIENT_ID, client_secret: this.variables.IDNOT_CLIENT_SECRET, redirect_uri: this.variables.IDNOT_REDIRECT_URL, code: code, grant_type: "authorization_code", - }); - + }); + const token = await fetch(this.variables.IDNOT_BASE_URL + this.variables.IDNOT_CONNEXION_URL + "?" + query, { method: "POST" }); - - if(token.status !== 200) console.error(await token.text()); - + + if (token.status !== 200) console.error(await token.text()); + const decodedToken = (await token.json()) as IIdNotToken; - - - const decodedIdToken = jwt.decode(decodedToken.id_token) as IdNotJwtPayload; - + + const decodedIdToken = jwt.decode(decodedToken.id_token) as IdNotJwtPayload; return decodedIdToken; } @@ -178,7 +176,7 @@ export default class IdNotService extends BaseService { } public async getOfficeMemberships(officeId: string) { - const officeInfos = await this.officeService.getByUid(officeId); + const officeInfos = await this.officeService.getByUid(officeId); const office = Office.hydrate(officeInfos!); const searchParams = new URLSearchParams({ key: this.variables.IDNOT_API_KEY, @@ -187,7 +185,7 @@ export default class IdNotService extends BaseService { await fetch(`${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/entites/${office.idNot}/personnes?` + searchParams, { method: "GET", }) - ).json()) as any; + ).json()) as any; } public getOfficeStatus(statusName: string) { @@ -291,13 +289,14 @@ export default class IdNotService extends BaseService { await this.userService.updateCheckedAt(user.uid!); } - public async updateOffice(officeId: string) { + public async updateOffice(officeId: string) { const officeInfos = await this.officeService.getByUid(officeId); const office = Office.hydrate(officeInfos!); const searchParams = new URLSearchParams({ key: this.variables.IDNOT_API_KEY, }); - const officeRawData = await fetch(`${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/entities/${office.idNot}?` + searchParams, { + + const officeRawData = await fetch(`${this.variables.IDNOT_API_BASE_URL}/api/pp/v2/entites/${office.idNot}?` + searchParams, { method: "GET", }); if (officeRawData.status === 404) { @@ -339,7 +338,7 @@ export default class IdNotService extends BaseService { const officeLocationData = (await ( await fetch(`${this.variables.IDNOT_API_BASE_URL + userData.entite.locationsUrl}?` + searchParams, { method: "GET" }) ).json()) as IOfficeLocation; - + const office = await this.officeService.get({ where: { idNot: decodedToken.entity_idn } }); // if(officeLocationData.result[0]!.adrGeoCodePostal.slice(0,2) !== "35") { @@ -378,17 +377,17 @@ export default class IdNotService extends BaseService { }, }; - if(!userToAdd.contact.email) { + if (!userToAdd.contact.email) { return null; } - + let userHydrated = User.hydrate(userToAdd); const user = await this.userService.create(userHydrated); const userOffice = await this.officeService.getByUid(user.office_uid); userHydrated = User.hydrate(user); const userOfficeHydrated = Office.hydrate(userOffice!); - if(office.length === 0) { + if (office.length === 0) { const officeRoles = await this.officeRolesService.get({ where: { office: { idNot: "0000" } }, include: { office: true, rules: true }, @@ -401,17 +400,16 @@ export default class IdNotService extends BaseService { where: { office: { idNot: "0000" } }, include: { office: true }, }); - + const officeRolesHydrated = OfficeRole.hydrateArray(officeRoles); const deedTypesHydrated = DeedType.hydrateArray(deedTypes); const documentTypesHydrated = DocumentType.hydrateArray(documentTypes); - - + await this.duplicateOfficeRoles(officeRolesHydrated, userOfficeHydrated); const documentTypesCreated = await this.duplicateDocumentTypes(documentTypesHydrated, userOfficeHydrated); await this.duplicateDeedTypes(deedTypesHydrated, documentTypesCreated, userOfficeHydrated); } - + const officeRole = await this.getOfficeRole(userData.typeLien.name, user.office_uid); if (officeRole) { @@ -427,37 +425,35 @@ export default class IdNotService extends BaseService { public async duplicateDocumentTypes(documentTypes: DocumentType[], office: Office): Promise { let newDocumentTypes: DocumentType[] = []; - for(const documentType of documentTypes) { + for (const documentType of documentTypes) { documentType.office = office; const documentTypeCreated = await this.documentTypesService.create(documentType); newDocumentTypes.push(DocumentType.hydrate(documentTypeCreated)); - }; + } return newDocumentTypes; } public async duplicateDeedTypes(deedTypes: DeedType[], documentTypes: DocumentType[], office: Office) { - for (const deedType of deedTypes) { + for (const deedType of deedTypes) { let newDocumentTypes: DocumentType[] = []; - for (const document of deedType.document_types!) { + for (const document of deedType.document_types!) { const newDocumentType = documentTypes.find((documentType) => documentType.name === document.name); - if(!newDocumentType) continue; + if (!newDocumentType) continue; newDocumentTypes.push(newDocumentType!); - }; + } deedType.document_types = newDocumentTypes; deedType.office = office; await this.deedTypesService.create(deedType); - }; + } } - public async duplicateOfficeRoles(officeRoles: OfficeRole[], office: Office){ - for(const officeRole of officeRoles) { + public async duplicateOfficeRoles(officeRoles: OfficeRole[], office: Office) { + for (const officeRole of officeRoles) { officeRole.office = office; await this.officeRolesService.create(officeRole); - }; + } } - - public async updateUsers() { const usersReq = await this.userService.getUsersToBeChecked(); const users = User.hydrateArray(usersReq); diff --git a/src/services/notary/CustomersService/CustomersService.ts b/src/services/notary/CustomersService/CustomersService.ts index 72e6923d..880d7c8d 100644 --- a/src/services/notary/CustomersService/CustomersService.ts +++ b/src/services/notary/CustomersService/CustomersService.ts @@ -1,12 +1,14 @@ -import { Customers, Prisma } from "@prisma/client"; +import EmailBuilder from "@Common/emails/EmailBuilder"; +import { Customers, Documents, Prisma } from "@prisma/client"; import CustomersRepository from "@Repositories/CustomersRepository"; import BaseService from "@Services/BaseService"; -import { Customer } from "le-coffre-resources/dist/Notary"; +import { Customer, DocumentReminder } from "le-coffre-resources/dist/Notary"; import { Service } from "typedi"; +import DocumentsReminderService from "../DocumentsReminder/DocumentsReminder"; @Service() export default class CustomersService extends BaseService { - constructor(private customerRepository: CustomersRepository) { + constructor(private customerRepository: CustomersRepository, private emailBuilder: EmailBuilder, private documentsReminderService : DocumentsReminderService) { super(); } @@ -49,4 +51,17 @@ export default class CustomersService extends BaseService { public async getByContact(contactUid: string): Promise { return this.customerRepository.findOneByContact(contactUid); } + + public async sendDocumentsReminder(customer: Customer, documents: Documents[]): Promise { + //Call email builder to send mail + const email = this.emailBuilder.sendReminder(customer, documents); + //Call DocumentsReminder service to create add the reminder in database + if (!email) return; + for (const document of documents) { + //Create document reminder + const documentReminder = new DocumentReminder(); + documentReminder.document = document; + await this.documentsReminderService.create(documentReminder); + } + } } diff --git a/src/services/notary/DocumentsNotaryService/DocumentsNotaryService.ts b/src/services/notary/DocumentsNotaryService/DocumentsNotaryService.ts new file mode 100644 index 00000000..865752ca --- /dev/null +++ b/src/services/notary/DocumentsNotaryService/DocumentsNotaryService.ts @@ -0,0 +1,61 @@ +import { DocumentsNotary, Prisma } from "@prisma/client"; +import { Document, DocumentNotary } from "le-coffre-resources/dist/Notary"; +import DocumentsNotaryRepository from "@Repositories/DocumentsNotaryRepository"; +import BaseService from "@Services/BaseService"; +import { Service } from "typedi"; + +@Service() +export default class DocumentsService extends BaseService { + constructor(private documentsNotaryRepository: DocumentsNotaryRepository) { + super(); + } + + /** + * @description : Get all documents + * @throws {Error} If documents cannot be get + */ + public async get(query: Prisma.DocumentsNotaryFindManyArgs) { + return this.documentsNotaryRepository.findMany(query); + } + + /** + * @description : Create a new document + * @throws {Error} If document cannot be created + */ + public async create(document: DocumentNotary): Promise { + return this.documentsNotaryRepository.create(document); + } + + /** + * @description : Delete a document + * @throws {Error} If document cannot be deleted + */ + public async delete(uid: string): Promise { + const documentEntity = await this.documentsNotaryRepository.findOneByUid(uid, { files: true }); + if (!documentEntity) throw new Error("document not found"); + const document = Document.hydrate(documentEntity, { strategy: "excludeAll" }); + + const isDocumentEmpty = document.files && !document!.files.find((file) => file.archived_at === null); + + if (!isDocumentEmpty && document.document_status !== "REFUSED") { + throw new Error("Can't delete a document with file"); + } + return this.documentsNotaryRepository.delete(uid); + } + + /** + * @description : Get a document by uid + * @throws {Error} If document cannot be get by uid + */ + public async getByUid(uid: string, query?: Prisma.DocumentsNotaryInclude): Promise { + return this.documentsNotaryRepository.findOneByUid(uid, query); + } + + /** + * @description : Get a document by uid + * @throws {Error} If document cannot be get by uid + */ + public async getByUidWithOffice(uid: string) { + return this.documentsNotaryRepository.findOneByUidWithOffice(uid); + } +} diff --git a/src/services/notary/DocumentsReminder/DocumentsReminder.ts b/src/services/notary/DocumentsReminder/DocumentsReminder.ts new file mode 100644 index 00000000..3ee1f823 --- /dev/null +++ b/src/services/notary/DocumentsReminder/DocumentsReminder.ts @@ -0,0 +1,28 @@ +import { DocumentsReminder, Prisma } from "@prisma/client"; +import { DocumentReminder } from "le-coffre-resources/dist/Notary"; +import BaseService from "@Services/BaseService"; +import { Service } from "typedi"; +import DocumentsReminderRepository from "@Repositories/DocumentsReminderRepository"; + +@Service() +export default class DocumentsReminderService extends BaseService { + constructor(private documentsReminderRepository: DocumentsReminderRepository) { + super(); + } + + /** + * @description : Get all documents + * @throws {Error} If documents cannot be get + */ + public async get(query: Prisma.DocumentsReminderFindManyArgs) { + return this.documentsReminderRepository.findMany(query); + } + + /** + * @description : Create a new document + * @throws {Error} If document cannot be created + */ + public async create(document: DocumentReminder): Promise { + return this.documentsReminderRepository.create(document); + } +}