Notifications (#79)

Generic controller to GET PUT on user notifications.
Notification builder to create notifs
This commit is contained in:
VincentAlamelle 2023-09-25 14:45:18 +02:00 committed by GitHub
commit 575bcc6580
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 1823 additions and 794 deletions

2127
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@ -53,7 +53,7 @@
"express": "^4.18.2",
"fp-ts": "^2.16.1",
"jsonwebtoken": "^9.0.0",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.77",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.79",
"module-alias": "^2.2.2",
"monocle-ts": "^2.3.13",
"multer": "^1.4.5-lts.1",

View File

@ -0,0 +1,114 @@
import { Response, Request } from "express";
import { Controller, Get, Put } from "@ControllerPattern/index";
import ApiController from "@Common/system/controller-pattern/ApiController";
import { Service } from "typedi";
import UserNotification from "le-coffre-resources/dist/Notary/UserNotification";
import UserNotificationService from "@Services/common/UserNotificationService/UserNotificationService";
@Controller()
@Service()
export default class UserNotificationController extends ApiController {
constructor(private userNotificationService: UserNotificationService) {
super();
}
/**
* @description Get all customers
*/
@Get("/api/v1/notifications")
protected async get(req: Request, response: Response) {
try {
//get query
let query;
if (req.query["q"]) {
query = JSON.parse(req.query["q"] as string);
}
//call service to get prisma entity
const userNotificationEntities = await this.userNotificationService.get(query);
//Hydrate ressource with prisma entity
const userNotifications = UserNotification.hydrateArray<UserNotification>(userNotificationEntities, { strategy: "excludeAll" });
//success
this.httpSuccess(response, userNotifications);
} catch (error) {
this.httpInternalError(response, error);
return;
}
}
/**
* @description Modify a specific customer by uid
*/
@Put("/api/v1/notifications/:uid")
protected async put(req: Request, response: Response) {
try {
const uid = req.params["uid"];
if (!uid) {
this.httpBadRequest(response, "No uid provided");
return;
}
const userNotificationFound = await this.userNotificationService.getByUid(uid);
if (!userNotificationFound) {
this.httpNotFoundRequest(response, "user notification not found");
return;
}
//init IUser resource with request body values
const userNotificationEntity = UserNotification.hydrate<UserNotification>(req.body);
//call service to get prisma entity
const userNotificationEntityUpdated = await this.userNotificationService.update(uid, userNotificationEntity);
//Hydrate ressource with prisma entity
const customer = UserNotification.hydrate<UserNotification>(userNotificationEntityUpdated, {
strategy: "excludeAll",
});
//success
this.httpSuccess(response, customer);
} catch (error) {
this.httpInternalError(response, error);
return;
}
}
/**
* @description Get a specific customer by uid
*/
@Get("/api/v1/notifications/:uid")
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 userNotificationEntity = await this.userNotificationService.getByUid(uid, query);
if (!userNotificationEntity) {
this.httpNotFoundRequest(response, "user notification not found");
return;
}
//Hydrate ressource with prisma entity
const userNotification = UserNotification.hydrate<UserNotification>(userNotificationEntity, { strategy: "excludeAll" });
//success
this.httpSuccess(response, userNotification);
} catch (error) {
this.httpInternalError(response, error);
return;
}
}
}

View File

@ -13,11 +13,12 @@ import fileHandler from "@App/middlewares/CustomerHandler/FileHandler";
import DocumentTypesService from "@Services/super-admin/DocumentTypesService/DocumentTypesService";
import { DocumentType } from "le-coffre-resources/dist/SuperAdmin";
import ObjectHydrate from "@Common/helpers/ObjectHydrate";
import NotificationBuilder from "@Common/notifications/NotificationBuilder";
@Controller()
@Service()
export default class FilesController extends ApiController {
constructor(private filesService: FilesService, private documentService: DocumentsService, private documentTypesService : DocumentTypesService) {
constructor(private filesService: FilesService, private documentService: DocumentsService, private documentTypesService : DocumentTypesService, private notificationBuilder : NotificationBuilder) {
super();
}
@ -90,7 +91,7 @@ export default class FilesController extends ApiController {
//init File resource with request body values
const fileEntity = File.hydrate<File>(JSON.parse(req.body["q"]));
//validate File
// await validateOrReject(fileEntity, { groups: ["createFile"] });
@ -102,7 +103,8 @@ export default class FilesController extends ApiController {
const documentToUpdate = Document.hydrate<Document>(document!);
documentToUpdate!.document_status = "DEPOSITED";
await this.documentService.update(document!.uid!, documentToUpdate);
const documentUpdated = await this.documentService.update(document!.uid!, documentToUpdate);
await this.notificationBuilder.sendDocumentDepositedNotification(documentUpdated!);
//Hydrate ressource with prisma entity
const fileEntityHydrated = File.hydrate<File>(fileEntityCreated, {

View File

@ -10,6 +10,7 @@ import authHandler from "@App/middlewares/AuthHandler";
import ruleHandler from "@App/middlewares/RulesHandler";
import documentHandler from "@App/middlewares/OfficeMembershipHandlers/DocumentHandler";
import EmailBuilder from "@Common/emails/EmailBuilder";
// import NotificationBuilder from "@Common/notifications/NotificationBuilder";
@Controller()
@Service()
@ -103,6 +104,8 @@ export default class DocumentsController extends ApiController {
//init Document resource with request body values
const documentEntity = Document.hydrate<Document>(req.body);
//validate document
await validateOrReject(documentEntity, { groups: ["updateDocument"] });
@ -110,7 +113,8 @@ export default class DocumentsController extends ApiController {
const documentEntityUpdated: Documents = await this.documentsService.update(uid, documentEntity, req.body.refused_reason);
//create email for asked document
this.emailBuilder.sendDocumentEmails(documentEntityUpdated);
// this.emailBuilder.sendDocumentEmails(documentEntityUpdated);
// this.notificationBuilder.sendDocumentAnchoredNotificatiom(documentEntityUpdated);
//Hydrate ressource with prisma entity
const document = Document.hydrate<Document>(documentEntityUpdated, { strategy: "excludeAll" });

View File

@ -1,5 +1,6 @@
import authHandler from "@App/middlewares/AuthHandler";
import roleHandler from "@App/middlewares/RolesHandler";
import NotificationBuilder from "@Common/notifications/NotificationBuilder";
import ApiController from "@Common/system/controller-pattern/ApiController";
import { Controller, Post } from "@ControllerPattern/index";
import { EAppointmentStatus } from "@prisma/client";
@ -20,6 +21,7 @@ export default class LiveVoteController extends ApiController {
private votesService: VotesService,
private usersService: UsersService,
private appointmentService: AppointmentService,
private notificationBuilder : NotificationBuilder,
) {
super();
}
@ -85,6 +87,8 @@ export default class LiveVoteController extends ApiController {
strategy: "excludeAll",
});
await this.notificationBuilder.sendVoteNotification(vote);
//success
this.httpCreated(response, vote);
} catch (error) {

View File

@ -47,6 +47,7 @@ import AppointmentsController from "./api/super-admin/AppointmentsController";
import VotesController from "./api/super-admin/VotesController";
import LiveVoteController from "./api/super-admin/LiveVoteController";
import UserNotificationController from "./api/common/UserNotificationController";
/**
@ -102,6 +103,7 @@ export default {
Container.get(DocumentsControllerCustomer);
Container.get(OfficeFoldersController);
Container.get(OfficeFolderAnchorsController);
Container.get(CustomersController)
Container.get(CustomersController);
Container.get(UserNotificationController);
},
};

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "notifications" ADD COLUMN "status" "ENotificationStatus" NOT NULL DEFAULT 'UNREAD';

View File

@ -0,0 +1,37 @@
/*
Warnings:
- You are about to drop the column `status` on the `notifications` table. All the data in the column will be lost.
- You are about to drop the `_UserHasNotifications` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "_UserHasNotifications" DROP CONSTRAINT "_UserHasNotifications_A_fkey";
-- DropForeignKey
ALTER TABLE "_UserHasNotifications" DROP CONSTRAINT "_UserHasNotifications_B_fkey";
-- AlterTable
ALTER TABLE "notifications" DROP COLUMN "status";
-- DropTable
DROP TABLE "_UserHasNotifications";
-- CreateTable
CREATE TABLE "user_notifications" (
"uid" TEXT NOT NULL,
"user_uid" VARCHAR(255) NOT NULL,
"read" BOOLEAN NOT NULL DEFAULT false,
"notification_uid" VARCHAR(255) NOT NULL,
CONSTRAINT "user_notifications_pkey" PRIMARY KEY ("uid")
);
-- CreateIndex
CREATE UNIQUE INDEX "user_notifications_uid_key" ON "user_notifications"("uid");
-- AddForeignKey
ALTER TABLE "user_notifications" ADD CONSTRAINT "user_notifications_user_uid_fkey" FOREIGN KEY ("user_uid") REFERENCES "users"("uid") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "user_notifications" ADD CONSTRAINT "user_notifications_notification_uid_fkey" FOREIGN KEY ("notification_uid") REFERENCES "notifications"("uid") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -64,10 +64,10 @@ model Users {
checked_at DateTime?
office_membership Offices @relation(fields: [office_uid], references: [uid], onDelete: Cascade)
office_uid String @db.VarChar(255)
notifications Notifications[] @relation("UserHasNotifications")
office_folders OfficeFolders[] @relation("OfficeFolderHasStakeholders")
appointment Appointments[]
votes Votes[]
user_notifications UserNotifications[]
@@map("users")
}
@ -106,16 +106,27 @@ model Customers {
}
model Notifications {
uid String @id @unique @default(uuid())
message String @db.VarChar(255)
redirection_url String @db.VarChar(255)
created_at DateTime? @default(now())
updated_at DateTime? @updatedAt
users Users[] @relation("UserHasNotifications")
uid String @id @unique @default(uuid())
message String @db.VarChar(255)
redirection_url String @db.VarChar(255)
created_at DateTime? @default(now())
updated_at DateTime? @updatedAt
userNotifications UserNotifications[]
@@map("notifications")
}
model UserNotifications {
uid String @id @unique @default(uuid())
user Users @relation(fields: [user_uid], references: [uid], onDelete: Cascade)
user_uid String @db.VarChar(255)
read Boolean @default(false)
notification Notifications @relation(fields: [notification_uid], references: [uid], onDelete: Cascade)
notification_uid String @db.VarChar(255)
@@map("user_notifications")
}
model OfficeFolders {
uid String @id @unique @default(uuid())
folder_number String @db.VarChar(255)

View File

@ -0,0 +1,85 @@
import DocumentsService from "@Services/super-admin/DocumentsService/DocumentsService";
import { Documents } from "@prisma/client";
import User, { Document, Vote } from "le-coffre-resources/dist/SuperAdmin";
import { Service } from "typedi";
import NotificationsService from "@Services/common/NotificationsService/NotificationsService";
import UsersService from "@Services/super-admin/UsersService/UsersService";
@Service()
export default class NotificationBuilder {
public constructor(private notificationsService : NotificationsService, private documentsService: DocumentsService, private usersService: UsersService){}
public async sendDocumentDepositedNotification(documentEntity: Documents){
if(documentEntity.document_status !== "DEPOSITED") return;
const documentPrisma = await this.documentsService.getByUid(documentEntity.uid, { depositor: {include: {contact: true}}, folder:{include:{ office: true, stakeholders: true}} });
if(!documentPrisma) throw new Error("Document not found");
const document = Document.hydrate<Document>(documentPrisma);
this.notificationsService.create({
message: "Votre client " + document.depositor?.contact?.first_name + " " + document.depositor?.contact?.last_name + " vous a envoyé un document à valider",
redirection_url: "",
created_at: new Date(),
updated_at: new Date(),
user : document.folder!.stakeholders || [],
});
}
public async sendDocumentAnchoredNotification(documentEntity: Documents){
const documentPrisma = await this.documentsService.getByUid(documentEntity.uid, { depositor: {include: {contact: true}}, folder:{include:{ folder_anchor : true ,office: true, stakeholders: true}} });
if(!documentPrisma) throw new Error("Document not found");
const document = Document.hydrate<Document>(documentPrisma);
if(document.folder?.anchor?.status !== "VERIFIED_ON_CHAIN") return;
this.notificationsService.create({
message: "Le dossier " + document.folder?.folder_number + " - " + document.folder?.name + " a été certifié. Vous pouvez désormais télécharger les fiches de preuve pour les mettre dans la GED de votre logiciel de rédaction d'acte.",
redirection_url: "",
created_at: new Date(),
updated_at: new Date(),
user : document.folder!.stakeholders || [],
});
}
public async sendVoteNotification(vote: Vote){
if(vote.appointment.status !== "OPEN") return;
const superAdminList = await this.usersService.get({where: {role : {label : "super-admin"}}});
let message = "";
if(vote.appointment.choice === "NOMINATE"){
message = "Un collaborateur souhaite attribuer le titre de Super Administrateur à " + vote.appointment.targeted_user + ". Cliquez ici pour voter."
}
else if(vote.appointment.choice === "DISMISS"){
message = "Un collaborateur souhaite retirer le titre de Super Administrateur à " + vote.appointment.targeted_user + ". Cliquez ici pour voter."
}
this.notificationsService.create({
message: message,
redirection_url: "",
created_at: new Date(),
updated_at: new Date(),
user : superAdminList || [],
});
}
public async sendDismissNotification(user: User){
this.notificationsService.create({
message: "Vous navez désormais plus le rôle de Super Administrateur de la plateforme.",
redirection_url: "",
created_at: new Date(),
updated_at: new Date(),
user : [user] || [],
});
}
public async sendNominateNotification(user: User){
this.notificationsService.create({
message: "Vous avez désormais le rôle de Super Administrateur de la plateforme.",
redirection_url: "",
created_at: new Date(),
updated_at: new Date(),
user : [user] || [],
});
}
}

View File

@ -0,0 +1,4 @@
export const ETemplates = {
DOCUMENT_ASKED: "DOCUMENT_ASKED",
DOCUMENT_REFUSED: "DOCUMENT_REFUSED",
};

View File

@ -0,0 +1,70 @@
import Database from "@Common/databases/database";
import BaseRepository from "@Repositories/BaseRepository";
import { Service } from "typedi";
import { Notifications, Prisma } from "prisma/prisma-client";
import { Notification } from "le-coffre-resources/dist/SuperAdmin";
@Service()
export default class NotificationRepository extends BaseRepository {
constructor(private database: Database) {
super();
}
protected get model() {
return this.database.getClient().notifications;
}
protected get instanceDb() {
return this.database.getClient();
}
/**
* @description : Find many emails
*/
public async findMany(query: Prisma.NotificationsFindManyArgs) {
query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows);
return this.model.findMany(query);
}
/**
* @description : Create an email
*/
public async create(notification: Notification): Promise<Notifications> {
const createArgs: Prisma.NotificationsCreateArgs = {
data: {
message: notification.message,
redirection_url: notification.redirection_url,
// users: {
// connect: notification.user!.map((user) => {
// return { uid: user.uid };
// }),
// }
userNotifications:{
create: notification.user!.map((user) => {
return {
user: {
connect: {
uid: user.uid
}
}
}
})
}
},
};
return this.model.create(createArgs);
}
/**
* @description : find unique email
*/
public async findOneByUid(uid: string) {
return this.model.findUnique({
where: {
uid: uid,
},
});
}
}

View File

@ -0,0 +1,53 @@
import Database from "@Common/databases/database";
import BaseRepository from "@Repositories/BaseRepository";
import { Service } from "typedi";
import { Prisma, UserNotifications } from "prisma/prisma-client";
import UserNotification from "le-coffre-resources/dist/Notary/UserNotification";
@Service()
export default class UserNotificationRepository extends BaseRepository {
constructor(private database: Database) {
super();
}
protected get model() {
return this.database.getClient().userNotifications;
}
protected get instanceDb() {
return this.database.getClient();
}
/**
* @description : Find many user notifications
*/
public async findMany(query: Prisma.UserNotificationsFindManyArgs) {
query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows);
return this.model.findMany(query);
}
/**
* @description : find unique user notification
*/
public async findOneByUid(uid: string) {
return this.model.findUnique({
where: {
uid: uid,
},
});
}
/**
* @description : UUpdate a user notification
*/
public async update(uid: string, userNotification: UserNotification): Promise<UserNotifications> {
const updateArgs: Prisma.UserNotificationsUpdateArgs = {
where: {
uid: uid,
},
data: {
read: userNotification.read,
},
};
return this.model.update({ ...updateArgs });
}
}

View File

@ -1,9 +1,12 @@
import NotificationRepository from "@Repositories/NotificationRepository";
import BaseService from "@Services/BaseService";
import { Notifications } from "@prisma/client";
import { Notification } from "le-coffre-resources/dist/SuperAdmin";
import { Service } from "typedi";
@Service()
export default class NotificationsService extends BaseService {
constructor() {
constructor(private notificationRepository: NotificationRepository) {
super();
}
@ -11,47 +14,26 @@ export default class NotificationsService extends BaseService {
* @description : Get all notifications
* @returns : T
* @throws {Error} If notifications cannot be get
* @param : projectEntity: Partial<ProjectEntity>
*/
public async get() {
// const notifications = await this.usersRepository.findOne(uuid);
// if (!notifications) Promise.reject(new Error("Cannot get notifications"));
return { response: "/api/notifications > GET : All notifications > Not implemented yet" };
public async get(query: any): Promise<Notifications[]> {
return this.notificationRepository.findMany(query);
}
/**
* @description : Create a new notification
* @returns : T
* @throws {Error} If notification cannot be created
* @param : projectEntity: Partial<ProjectEntity>
*/
public async create() {
// const notification = await this.projectRepository.create(projectEntity);
// if (!notification) Promise.reject(new Error("Cannot create project"));
return { response: "/api/notifications > POST : Create notification > Not implemented yet" };
}
/**
* @description : Modify a new notification
* @returns : T
* @throws {Error} If notification cannot be modified
* @param : projectEntity: Partial<ProjectEntity>
*/
public async put() {
// const notification = await this.projectRepository.create(projectEntity);
// if (!notification) Promise.reject(new Error("Cannot create project"));
return { response: "/api/notifications > PUT : Modified notification > Not implemented yet" };
public async create(notification: Notification): Promise<Notifications> {
return this.notificationRepository.create(notification);
}
/**
* @description : Get a notification by uid
* @returns : T
* @throws {Error} If project cannot be created
* @param : projectEntity: Partial<ProjectEntity>
*/
public async getByUid(uid: string) {
// const notification = await this.usersRepository.findOne(uid);
// if (!notification) Promise.reject(new Error("Cannot get notification by uid"));
return { response: "/api/notifications/:uid > GET : notification by uid > Not implemented yet" };
public async getByUid(uid: string, query?: any): Promise<Notifications | null> {
return this.notificationRepository.findOneByUid(uid);
}
}

View File

@ -0,0 +1,38 @@
import UserNotificationRepository from "@Repositories/UserNotificationRepository";
import BaseService from "@Services/BaseService";
import { UserNotifications } from "@prisma/client";
import UserNotification from "le-coffre-resources/dist/Notary/UserNotification";
import { Service } from "typedi";
@Service()
export default class UserNotificationService extends BaseService {
constructor(private userNotificationRepository: UserNotificationRepository) {
super();
}
/**
* @description : Get all user notifications
* @returns : T
* @throws {Error} If user notifications cannot be get
*/
public async get(query: any): Promise<UserNotifications[]> {
return this.userNotificationRepository.findMany(query);
}
/**
* @description : Update data of a deed with document types
* @throws {Error} If deeds cannot be updated with document types or one of them
*/
public async update(uid: string, userNotification: UserNotification): Promise<UserNotifications> {
return this.userNotificationRepository.update(uid, userNotification);
}
/**
* @description : Get a user notification by uid
* @returns : T
* @throws {Error} If user notification cannot found
*/
public async getByUid(uid: string, query?: any): Promise<UserNotifications | null> {
return this.userNotificationRepository.findOneByUid(uid);
}
}