From 547ab0b23051dbadb40c51cf9340cb38e1d2ac26 Mon Sep 17 00:00:00 2001 From: Vins Date: Wed, 5 Jun 2024 09:39:11 +0200 Subject: [PATCH] Customer notes feature finished --- Dockerfile | 2 +- package.json | 2 +- src/app/api/customer/NotesController.ts | 202 ++++++++++++++++++ .../api/customer/OfficeFoldersController.ts | 2 +- src/app/api/notary/OfficeFoldersController.ts | 7 +- src/app/index.ts | 2 + .../migration.sql | 20 ++ src/common/databases/schema.prisma | 14 ++ src/common/repositories/NotesRepository.ts | 74 +++++++ .../customer/NotesService/NotesService.ts | 48 +++++ 10 files changed, 367 insertions(+), 6 deletions(-) create mode 100644 src/app/api/customer/NotesController.ts create mode 100644 src/common/databases/migrations/20240603201549_folder_customer_notes/migration.sql create mode 100644 src/common/repositories/NotesRepository.ts create mode 100644 src/services/customer/NotesService/NotesService.ts diff --git a/Dockerfile b/Dockerfile index d33dbefb..e98bde33 100644 --- a/Dockerfile +++ b/Dockerfile @@ -36,5 +36,5 @@ COPY --from=deps --chown=lecoffreuser leCoffre/src/common/databases ./src/common RUN apk update && apk add chromium USER lecoffreuser -CMD ["npm", "run", "start"] +CMD ["npm", "run", "api:start"] EXPOSE 3001 \ No newline at end of file diff --git a/package.json b/package.json index 688f4c6c..ba8a4fdc 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.139", + "le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.151", "module-alias": "^2.2.2", "monocle-ts": "^2.3.13", "multer": "^1.4.5-lts.1", diff --git a/src/app/api/customer/NotesController.ts b/src/app/api/customer/NotesController.ts new file mode 100644 index 00000000..11d59973 --- /dev/null +++ b/src/app/api/customer/NotesController.ts @@ -0,0 +1,202 @@ +import { Response, Request } from "express"; +import { Controller, Get, Post, Put } from "@ControllerPattern/index"; +import ApiController from "@Common/system/controller-pattern/ApiController"; +import { Service } from "typedi"; +import { Prisma } from "@prisma/client"; +import authHandler from "@App/middlewares/AuthHandler"; +import Note from "le-coffre-resources/dist/Customer/Note"; +import NotesService from "@Services/customer/NotesService/NotesService"; + +@Controller() +@Service() +export default class NotesController extends ApiController { + constructor(private notesService: NotesService) { + super(); + } + + /** + * @description Get all Notes + * @returns Note[] list of Notes + */ + @Get("/api/v1/customer/notes", [authHandler]) + protected async get(req: Request, response: Response) { + try { + //get query + let query: Prisma.NotesFindManyArgs = {}; + 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 noteEntities = await this.notesService.get(query); + + //Hydrate ressource with prisma entity + const notes = Note.hydrateArray(noteEntities, { strategy: "excludeAll" }); + + //success + this.httpSuccess(response, notes); + } catch (error) { + this.httpBadRequest(response, error); + return; + } + } + + /** + * @description Get a specific customer note + */ + @Get("/api/v1/customer/notes/customer/:customer-uid/folders/:folder-uid/", [authHandler]) + protected async getCustomerNoteByFolderUid(req: Request, response: Response) { + try { + const customerUid = req.params["customer-uid"]; + const folderUid = req.params["folder-uid"]; + if (!customerUid || !folderUid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + + //get query + const query: Prisma.NotesFindManyArgs = { + where: { + customer: { + uid: customerUid + }, + folder: { + uid: folderUid + } + } + }; + + //call service to get prisma entity + const noteEntities = await this.notesService.get(query); + + if(noteEntities.length === 0) { + this.httpNotFoundRequest(response, "No notes found"); + return; + } + + //Hydrate ressource with prisma entity + const notes = Note.hydrateArray(noteEntities, { strategy: "excludeAll" }); + + //success + this.httpSuccess(response, notes); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + /** + * @description Get a specific note by uid + */ + @Get("/api/v1/customer/notes/:uid", [authHandler]) + 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 noteEntity = await this.notesService.getByUid(uid, query); + + if (!noteEntity) { + this.httpNotFoundRequest(response, "note not found"); + return; + } + + //Hydrate ressource with prisma entity + const note = Note.hydrate(noteEntity); + + //success + this.httpSuccess(response, note); + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + /** + * @description Create a new note + */ + @Post("/api/v1/customer/notes", [authHandler]) + protected async post(req: Request, response: Response) { + try { + //init OfficeFolder resource with request body values + const noteRessource = Note.hydrate(req.body); + // noteRessource.validateOrReject?.({ groups: ["createFolder"], forbidUnknownValues: false }); + + //call service to get prisma entity + try { + const noteEntity = await this.notesService.create(noteRessource); + //Hydrate ressource with prisma entity + const note = Note.hydrate(noteEntity, { + strategy: "excludeAll", + }); + //success + this.httpCreated(response, note); + } catch (error) { + this.httpValidationError(response, error); + return; + } + } catch (error) { + this.httpInternalError(response, error); + return; + } + } + + /** + * @description Modify a specific note by uid + */ + @Put("/api/v1/customer/notes/:uid", [authHandler]) + protected async put(req: Request, response: Response) { + try { + const uid = req.params["uid"]; + if (!uid) { + this.httpBadRequest(response, "No uid provided"); + return; + } + + const noteFound = await this.notesService.getByUid(uid); + + if (!noteFound) { + this.httpNotFoundRequest(response, "office folder not found"); + return; + } + + //init OfficeFolder resource with request body values + const noteEntity = Note.hydrate(req.body); + + //validate folder + //await validateOrReject(noteEntity, { groups: ["updateFolder"], forbidUnknownValues: false }); + + //call service to get prisma entity + try { + const noteEntityUpdated = await this.notesService.update(uid, noteEntity); + + //Hydrate ressource with prisma entity + const note = Note.hydrate(noteEntityUpdated, { + strategy: "excludeAll", + }); + + //success + this.httpSuccess(response, note); + } catch (error) { + this.httpValidationError(response, error); + return; + } + } catch (error) { + this.httpInternalError(response, error); + return; + } + } +} diff --git a/src/app/api/customer/OfficeFoldersController.ts b/src/app/api/customer/OfficeFoldersController.ts index a90a5876..f8bea23d 100644 --- a/src/app/api/customer/OfficeFoldersController.ts +++ b/src/app/api/customer/OfficeFoldersController.ts @@ -95,7 +95,7 @@ export default class OfficeFoldersController extends ApiController { } //Hydrate ressource with prisma entity - const officeFolder = OfficeFolderNotary.hydrate(officeFolderEntity, { strategy: "excludeAll" }); + const officeFolder = OfficeFolderNotary.hydrate(officeFolderEntity); if(officeFolder.customers) { officeFolder.customers = officeFolder.customers!.filter((customer) => customer.contact?.email === email); diff --git a/src/app/api/notary/OfficeFoldersController.ts b/src/app/api/notary/OfficeFoldersController.ts index 45941476..2d8f7792 100644 --- a/src/app/api/notary/OfficeFoldersController.ts +++ b/src/app/api/notary/OfficeFoldersController.ts @@ -261,9 +261,9 @@ export default class OfficeFoldersController extends ApiController { let query; if (req.query["q"]) { query = JSON.parse(req.query["q"] as string); - } + } - const officeFolderEntity = await this.officeFoldersService.getByUid(uid, query); + const officeFolderEntity = await this.officeFoldersService.getByUid(uid, query); if (!officeFolderEntity) { this.httpNotFoundRequest(response, "folder not found"); @@ -271,7 +271,8 @@ export default class OfficeFoldersController extends ApiController { } //Hydrate ressource with prisma entity - const officeFolder = OfficeFolder.hydrate(officeFolderEntity, { strategy: "excludeAll" }); + const officeFolder = OfficeFolder.hydrate(officeFolderEntity); + //success this.httpSuccess(response, officeFolder); diff --git a/src/app/index.ts b/src/app/index.ts index 1590b6f1..c5767457 100644 --- a/src/app/index.ts +++ b/src/app/index.ts @@ -53,6 +53,7 @@ import SubscriptionsController from "./api/admin/SubscriptionsController"; import StripeController from "./api/admin/StripeController"; import StripeWebhooks from "@Common/webhooks/stripeWebhooks"; import RulesGroupsController from "./api/admin/RulesGroupsController"; +import NotesController from "./api/customer/NotesController"; /** * @description This allow to declare all controllers used in the application @@ -114,5 +115,6 @@ export default { Container.get(StripeController); Container.get(StripeWebhooks); Container.get(RulesGroupsController); + Container.get(NotesController); }, }; diff --git a/src/common/databases/migrations/20240603201549_folder_customer_notes/migration.sql b/src/common/databases/migrations/20240603201549_folder_customer_notes/migration.sql new file mode 100644 index 00000000..848a2ab9 --- /dev/null +++ b/src/common/databases/migrations/20240603201549_folder_customer_notes/migration.sql @@ -0,0 +1,20 @@ +-- CreateTable +CREATE TABLE "notes" ( + "uid" TEXT NOT NULL, + "content" VARCHAR(1000) NOT NULL, + "created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP, + "updated_at" TIMESTAMP(3), + "customer_uid" VARCHAR(255) NOT NULL, + "folder_uid" VARCHAR(255) NOT NULL, + + CONSTRAINT "notes_pkey" PRIMARY KEY ("uid") +); + +-- CreateIndex +CREATE UNIQUE INDEX "notes_uid_key" ON "notes"("uid"); + +-- AddForeignKey +ALTER TABLE "notes" ADD CONSTRAINT "notes_customer_uid_fkey" FOREIGN KEY ("customer_uid") REFERENCES "customers"("uid") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "notes" ADD CONSTRAINT "notes_folder_uid_fkey" FOREIGN KEY ("folder_uid") REFERENCES "office_folders"("uid") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/src/common/databases/schema.prisma b/src/common/databases/schema.prisma index e42133a9..259c59f1 100644 --- a/src/common/databases/schema.prisma +++ b/src/common/databases/schema.prisma @@ -128,6 +128,7 @@ model Customers { totpCodes TotpCodes[] office Offices @relation(fields: [office_uid], references: [uid], onDelete: Cascade) office_uid String @db.VarChar(255) + notes Notes[] @@map("customers") } @@ -172,6 +173,7 @@ model OfficeFolders { folder_anchor OfficeFolderAnchors? @relation(fields: [folder_anchor_uid], references: [uid]) folder_anchor_uid String? @unique @db.VarChar(255) + notes Notes[] @@unique([folder_number, office_uid]) @@map("office_folders") @@ -417,6 +419,18 @@ model Seats { @@map("seats") } +model Notes { + uid String @id @unique @default(uuid()) + content String @db.VarChar(1000) + created_at DateTime? @default(now()) + updated_at DateTime? @updatedAt + customer Customers @relation(fields: [customer_uid], references: [uid], onDelete: Cascade) + customer_uid String @db.VarChar(255) + folder OfficeFolders @relation(fields: [folder_uid], references: [uid], onDelete: Cascade) + folder_uid String @db.VarChar(255) + @@map("notes") +} + enum ESubscriptionStatus { ACTIVE INACTIVE diff --git a/src/common/repositories/NotesRepository.ts b/src/common/repositories/NotesRepository.ts new file mode 100644 index 00000000..8300ab96 --- /dev/null +++ b/src/common/repositories/NotesRepository.ts @@ -0,0 +1,74 @@ +import Database from "@Common/databases/database"; +import BaseRepository from "@Repositories/BaseRepository"; +import { Service } from "typedi"; +import { Notes, Prisma } from "@prisma/client"; +import Note from "le-coffre-resources/dist/Customer/Note"; + +@Service() +export default class NotesRepository extends BaseRepository { + constructor(private database: Database) { + super(); + } + protected get model() { + return this.database.getClient().notes; + } + protected get instanceDb() { + return this.database.getClient(); + } + + /** + * @description : Find many notes + */ + public async findMany(query: Prisma.NotesFindManyArgs) { + return this.model.findMany(query); + } + + /** + * @description : Find one note + */ + public async findOneByUid(uid: string, query?: Prisma.NotesInclude) { + return this.model.findUnique({ + where: { uid }, + include: query, + }); + } + + /** + * @description : Create new note + */ + public async create(note: Note): Promise { + const createArgs: Prisma.NotesCreateArgs = { + data: { + content: note.content || "", + customer: { + connect: { + uid: note.customer!.uid, + }, + }, + folder: { + connect: { + uid: note.folder!.uid, + }, + }, + }, + }; + + return this.model.create(createArgs); + } + + /** + * @description : Update data of an office folder + */ + public async update(noteUid: string, note: Note): Promise { + const updateArgs: Prisma.NotesUpdateArgs = { + where: { + uid: noteUid, + }, + data: { + content: note.content || "", + }, + }; + + return this.model.update(updateArgs); + } +} diff --git a/src/services/customer/NotesService/NotesService.ts b/src/services/customer/NotesService/NotesService.ts new file mode 100644 index 00000000..81b120be --- /dev/null +++ b/src/services/customer/NotesService/NotesService.ts @@ -0,0 +1,48 @@ +import BaseService from "@Services/BaseService"; +import { Service } from "typedi"; +import { Notes, Prisma } from "@prisma/client"; +import NotesRepository from "@Repositories/NotesRepository"; +import Note from "le-coffre-resources/dist/Customer/Note"; + +@Service() +export default class NotesService extends BaseService { + constructor( + private notesRepository: NotesRepository, + ) { + super(); + } + + /** + * @description : Get all notes + * @throws {Error} If notes cannot be get + */ + public async get(query: Prisma.NotesFindManyArgs) { + return this.notesRepository.findMany(query); + } + + /** + * @description : Get a note by uid + * @throws {Error} If note cannot be get + */ + public async getByUid(uid: string, query?: Prisma.NotesInclude) { + return this.notesRepository.findOneByUid(uid, query); + } + + /** + * @description : Create a new note + * @throws {Error} If note cannot be created + */ + public async create(noteEntity: Note): Promise { + return this.notesRepository.create(noteEntity); + } + + /** + * @description : Modify a note + * @throws {Error} If note cannot be modified + */ + public async update(noteUid: string, noteEntity: Note): Promise { + return this.notesRepository.update(noteUid, noteEntity); + } + + +}