Merged staging

This commit is contained in:
Vins 2024-04-18 12:08:54 +02:00
commit 8700462c5e
31 changed files with 1338 additions and 33 deletions

View File

@ -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.119",
"le-coffre-resources": "git@github.com:smart-chain-fr/leCoffre-resources.git#v2.130",
"module-alias": "^2.2.2",
"monocle-ts": "^2.3.13",
"multer": "^1.4.5-lts.1",
@ -70,6 +70,7 @@
"prisma-query": "^2.0.0",
"puppeteer": "^21.3.4",
"reflect-metadata": "^0.1.13",
"stripe": "^14.22.0",
"ts-node": "^10.9.1",
"tslib": "^2.4.1",
"typedi": "^0.10.0",

View File

@ -0,0 +1,104 @@
import authHandler from "@App/middlewares/AuthHandler";
// import roleHandler from "@App/middlewares/RolesHandler";
import ApiController from "@Common/system/controller-pattern/ApiController";
import { Controller, Get, Post} from "@ControllerPattern/index";
import StripeService from "@Services/common/StripeService/StripeService";
import { validateOrReject } from "class-validator";
import { Request, Response } from "express";
import { Subscription } from "le-coffre-resources/dist/Admin";
import { Service } from "typedi";
@Controller()
@Service()
export default class StripeController extends ApiController {
constructor(private stripeService: StripeService) {
super();
}
/**
* @description Create a new checkout session
*/
@Post("/api/v1/admin/stripe", [authHandler])
protected async createStripeSubscriptionCheckout(req: Request, response: Response) {
try {
const officeId: string = req.body.user.office_Id;
//add office id to request body
req.body.office = { uid: officeId };
//init Subscription resource with request body values
const subscriptionEntity = Subscription.hydrate<Subscription>(req.body, { strategy: "excludeAll" });
await validateOrReject(subscriptionEntity, { groups: ["createSubscription"], forbidUnknownValues: false });
const stripeSession = await this.stripeService.createCheckoutSession(subscriptionEntity);
this.httpCreated(response, stripeSession);
} catch (error) {
this.httpInternalError(response, error);
return;
}
}
// @Put("/api/v1/admin/stripe/:uid")
// protected async createStripeSubscriptionUpdateCheckout(req: Request, response: Response) {
// try {
// const uid = req.params["uid"];
// if (!uid) {
// this.httpBadRequest(response, "No uid provided");
// return;
// }
// const officeId: string = req.body.user.office_Id;
// //add office id to request body
// req.body.office = { uid: officeId };
// //init Subscription resource with request body values
// const subscriptionEntity = Subscription.hydrate<Subscription>(req.body, { strategy: "excludeAll" });
// await validateOrReject(subscriptionEntity, { groups: ["updateSubscription"], forbidUnknownValues: false });
// const stripeSession = await this.stripeService.createCheckoutSessionUpdate(uid, subscriptionEntity);
// this.httpCreated(response, stripeSession);
// } catch (error) {
// this.httpInternalError(response, error);
// return;
// }
// }
@Get("/api/v1/admin/stripe/:uid", [authHandler])
protected async getClientPortalSession(req: Request, response: Response) {
try {
const uid = req.params["uid"];
if (!uid) {
this.httpBadRequest(response, "No uid provided");
return;
}
const client_portal = await this.stripeService.createClientPortalSession(uid);
this.httpSuccess(response, client_portal);
} catch (error) {
this.httpInternalError(response, error);
return;
}
}
@Get("/api/v1/admin/stripe/:uid/customer")
protected async getCustomerBySubscription(req: Request, response: Response) {
try {
const uid = req.params["uid"];
if (!uid) {
this.httpBadRequest(response, "No uid provided");
return;
}
const customer = await this.stripeService.getCustomerBySubscription(uid);
this.httpSuccess(response, customer);
} catch (error) {
this.httpInternalError(response, error);
return;
}
}
}

View File

@ -0,0 +1,210 @@
import { Controller, Get, Post, Put } from "@ControllerPattern/index";
import { Response, Request } from "express";
import ApiController from "@Common/system/controller-pattern/ApiController";
import { Service } from "typedi";
// import authHandler from "@App/middlewares/AuthHandler";
// import roleHandler from "@App/middlewares/RolesHandler";
// import ruleHandler from "@App/middlewares/RulesHandler";
import { Prisma } from "@prisma/client";
import SubscriptionsService from "@Services/admin/SubscriptionsService/SubscriptionsService.ts";
import { Subscription } from "le-coffre-resources/dist/Admin";
import ObjectHydrate from "@Common/helpers/ObjectHydrate";
import roleHandler from "@App/middlewares/RolesHandler";
import authHandler from "@App/middlewares/AuthHandler";
import EmailBuilder from "@Common/emails/EmailBuilder";
@Controller()
@Service()
export default class SubscriptionsController extends ApiController {
constructor(private subscriptionsService: SubscriptionsService, private emailBuilder: EmailBuilder) {
super();
}
/**
* @description Get all subscriptions
*/
@Get("/api/v1/admin/subscriptions", [authHandler, roleHandler])
protected async get(req: Request, response: Response) {
try {
//get query
let query: Prisma.SubscriptionsFindManyArgs = {};
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 subscriptionsEntities = await this.subscriptionsService.get(query);
//Hydrate ressource with prisma entity
const subscriptions = Subscription.hydrateArray<Subscription>(subscriptionsEntities, { strategy: "excludeAll" });
//success
this.httpSuccess(response, subscriptions);
} catch (error) {
this.httpInternalError(response, error);
return;
}
}
/**
* @description Get a specific documentType by uid
*/
@Get("/api/v1/admin/subscriptions/:uid", [authHandler, roleHandler])
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 subscriptionEntity = await this.subscriptionsService.getByUid(uid, query);
//Hydrate resource with prisma entity
const subscription = ObjectHydrate.hydrate<Subscription>(new Subscription(), subscriptionEntity!, { strategy: "excludeAll" });
//success
this.httpSuccess(response, subscription);
} catch (error) {
this.httpInternalError(response, error);
return;
}
}
// /**
// * @description Create a new documentType
// */
// @Post("/api/v1/admin/subscriptions", [authHandler, roleHandler])
// protected async post(req: Request, response: Response) {
// try {
// //init Subscription resource with request body values
// const subscriptionEntity = Subscription.hydrate<Subscription>(req.body);
// //validate subscription
// await validateOrReject(subscriptionEntity, { groups: ["createSubscription"], forbidUnknownValues: false });
// //call service to get prisma entity
// const subscriptionEntityCreated = await this.subscriptionsService.create(subscriptionEntity);
// //Hydrate ressource with prisma entity
// const subscription = Subscription.hydrate<Subscription>(subscriptionEntityCreated, {
// strategy: "excludeAll",
// });
// //success
// this.httpCreated(response, subscription);
// } catch (error) {
// this.httpInternalError(response, error);
// return;
// }
// }
/**
* @description Update a subscription
*/
@Put("/api/v1/admin/subscriptions/:uid", [authHandler, roleHandler])
protected async put(req: Request, response: Response) {
try {
const uid = req.params["uid"];
if (!uid) {
this.httpBadRequest(response, "No uid provided");
return;
}
const subscriptionFound = await this.subscriptionsService.getByUid(uid);
if (!subscriptionFound) {
this.httpNotFoundRequest(response, "subscription not found");
return;
}
//init Subscription resource with request body values
const subscriptionEntity = Subscription.hydrate<Subscription>(req.body);
//call service to get prisma entity
const subscriptionEntityUpdated = await this.subscriptionsService.update(uid, subscriptionEntity);
//Hydrate ressource with prisma entity
const subscription = Subscription.hydrate<Subscription>(subscriptionEntityUpdated, {
strategy: "excludeAll",
});
//success
this.httpSuccess(response, subscription);
} catch (error) {
this.httpInternalError(response, error);
return;
}
}
/**
* @description Invite collaborators to a subscription
*/
@Post("/api/v1/admin/subscriptions/invite", [authHandler, roleHandler])
protected async inviteCollaborators(req: Request, response: Response) {
try {
//get email list from body
const emails: [string] = req.body.emails;
if (!emails || emails.length < 1){
this.httpBadRequest(response, "No emails provided");
return;
}
//create emails for asked document
await this.emailBuilder.sendInvitationEmails(emails);
//success
this.httpSuccess(response, {message: "Invitations sent"});
} catch (error) {
this.httpInternalError(response, error);
return;
}
}
// /**
// * @description Update a subscription seats
// */
// @Put("/api/v1/admin/subscriptions/:uid/seats", [authHandler, roleHandler])
// protected async updateSubscriptionSeats(req: Request, response: Response) {
// try {
// const uid = req.params["uid"];
// if (!uid) {
// this.httpBadRequest(response, "No uid provided");
// return;
// }
// const subscriptionFound = await this.subscriptionsService.getByUid(uid);
// if (!subscriptionFound) {
// this.httpNotFoundRequest(response, "subscription not found");
// return;
// }
// //init Subscription resource with request body values
// const seatEntities = Seat.hydrateArray<Seat>(req.body);
// //call service to get prisma entity
// const subscriptionEntityUpdated = await this.subscriptionsService.update(uid, subscriptionEntity);
// //Hydrate ressource with prisma entity
// const subscription = Subscription.hydrate<Subscription>(subscriptionEntityUpdated, {
// strategy: "excludeAll",
// });
// //success
// this.httpSuccess(response, subscription);
// } catch (error) {
// this.httpInternalError(response, error);
// return;
// }
// }
}

View File

@ -3,6 +3,8 @@ import { Controller, Get } from "@ControllerPattern/index";
import ApiController from "@Common/system/controller-pattern/ApiController";
import { Service } from "typedi";
import IdNotService from "@Services/common/IdNotService/IdNotService";
import userHandler from "@App/middlewares/OfficeMembershipHandlers/UserHandler";
import authHandler from "@App/middlewares/AuthHandler";
@Controller()
@Service()
@ -11,7 +13,7 @@ export default class UserController extends ApiController {
super();
}
@Get("/api/v1/idnot/office/:uid/office-memberships")
@Get("/api/v1/idnot/office/:uid/office-memberships", [authHandler, userHandler])
protected async getOfficeMemberships(req: Request, response: Response) {
try {
const uid = req.params["uid"];
@ -21,6 +23,7 @@ export default class UserController extends ApiController {
}
const officeMemberships = await this.idNotService.getOfficeMemberships(uid);
this.httpSuccess(response, officeMemberships);
} catch (error) {
console.log(error);

View File

@ -5,14 +5,23 @@ import { Service } from "typedi";
import AuthService, { IUserJwtPayload } from "@Services/common/AuthService/AuthService";
import IdNotService from "@Services/common/IdNotService/IdNotService";
import WhitelistService from "@Services/common/WhitelistService/WhitelistService";
import User from "le-coffre-resources/dist/SuperAdmin";
import User from "le-coffre-resources/dist/Admin";
import UsersService from "@Services/super-admin/UsersService/UsersService";
import SubscriptionsService from "@Services/admin/SubscriptionsService/SubscriptionsService.ts";
import { ESubscriptionStatus } from "@prisma/client";
import SeatsService from "@Services/admin/SeatsService/SeatsService";
import { EType } from "le-coffre-resources/dist/Admin/Subscription";
@Controller()
@Service()
export default class UserController extends ApiController {
constructor(private authService: AuthService, private idNotService: IdNotService, private whitelistService: WhitelistService, private userService: UsersService) {
constructor(
private authService: AuthService,
private idNotService: IdNotService,
private userService: UsersService,
private subscriptionsService: SubscriptionsService,
private seatsService: SeatsService,
) {
super();
}
@ -30,15 +39,15 @@ export default class UserController extends ApiController {
const idNotToken = await this.idNotService.getIdNotToken(code);
if(!idNotToken) {
if (!idNotToken) {
this.httpValidationError(response, "IdNot token undefined");
return;
}
const user = await this.idNotService.getOrCreateUser(idNotToken);
if(!user) {
this.httpUnauthorized(response, "Email not found");
if (!user) {
this.httpUnauthorized(response, "User not found");
return;
}
@ -46,7 +55,7 @@ export default class UserController extends ApiController {
//Whitelist feature
//Get user with contact
const prismaUser = await this.userService.getByUid(user.uid, {contact: true });
const prismaUser = await this.userService.getByUid(user.uid, { contact: true, role: true });
if (!prismaUser) {
this.httpNotFoundRequest(response, "user not found");
@ -56,21 +65,61 @@ export default class UserController extends ApiController {
//Hydrate user to be able to use his contact
const userHydrated = User.hydrate<User>(prismaUser, { strategy: "excludeAll" });
if(!userHydrated.contact?.email || userHydrated.contact?.email === "") {
if (!userHydrated.contact?.email || userHydrated.contact?.email === "") {
this.httpUnauthorized(response, "Email not found");
return;
}
let isSubscribed = false;
const subscriptions = await this.subscriptionsService.get({ where: { office_uid: userHydrated.office_membership?.uid } });
if (!subscriptions || subscriptions.length === 0 || subscriptions[0]?.status === ESubscriptionStatus.INACTIVE) {
isSubscribed = false;
}
else if (subscriptions[0]?.type === EType.Unlimited) {
isSubscribed = true;
} else {
const hasSeat = await this.subscriptionsService.get({
where: { status: ESubscriptionStatus.ACTIVE, seats: { some: { user_uid: userHydrated.uid } } },
});
if (hasSeat && hasSeat.length > 0) {
isSubscribed = true;
} else {
const nbMaxSeats = subscriptions[0]!.nb_seats;
const nbCurrentSeats = await this.seatsService.get({ where: { subscription_uid: subscriptions[0]!.uid } });
//if nbMaxSeats < nbCurrentSeats, create a new seat for the user
if (nbMaxSeats > nbCurrentSeats.length) {
const seatAdded = await this.seatsService.create(user.uid, subscriptions[0]!.uid);
if (seatAdded) {
isSubscribed = true;
}
}
}
}
if (userHydrated.role?.name === "admin") {
isSubscribed = true;
}
if (!isSubscribed) {
this.httpUnauthorized(response, "User not subscribed");
return;
}
//Check if user is whitelisted
// const isWhitelisted = await this.whitelistService.getByEmail(userHydrated.contact!.email);
const isWhitelisted = await this.whitelistService.getByEmail(userHydrated.contact!.email);
//When we'll switch to idNotId whitelisting
// const isWhitelisted = await this.userWhitelistService.getByIdNotId(user.idNot);
//If not whitelisted, return 409 Not whitelisted
if (!isWhitelisted || isWhitelisted.length === 0) {
this.httpNotWhitelisted(response);
return;
}
// if (!isWhitelisted || isWhitelisted.length === 0) {
// this.httpNotWhitelisted(response);
// return;
// }
await this.idNotService.updateOffice(user.office_uid);

View File

@ -49,6 +49,9 @@ import AuthController from "./api/customer/AuthController";
import NotaryOfficeRibController from "./api/notary/OfficeRibController";
import CustomerOfficeRibController from "./api/customer/OfficeRibController";
import IdNotOfficeController from "./api/idnot/OfficeController";
import SubscriptionsController from "./api/admin/SubscriptionsController";
import StripeController from "./api/admin/StripeController";
import StripeWebhooks from "@Common/webhooks/stripeWebhooks";
/**
* @description This allow to declare all controllers used in the application
*/
@ -104,6 +107,9 @@ export default {
Container.get(AuthController);
Container.get(NotaryOfficeRibController);
Container.get(CustomerOfficeRibController);
Container.get(IdNotOfficeController)
Container.get(IdNotOfficeController);
Container.get(SubscriptionsController);
Container.get(StripeController);
Container.get(StripeWebhooks);
},
};

View File

@ -142,6 +142,30 @@ export class BackendVariables {
@IsNotEmpty()
public readonly SCW_BUCKET_NAME!: string;
@IsNotEmpty()
public readonly STRIPE_SECRET_KEY!: string;
@IsNotEmpty()
public readonly STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID!: string;
@IsNotEmpty()
public readonly STRIPE_STANDARD_ANNUAL_SUBSCRIPTION_PRICE_ID!: string;
@IsNotEmpty()
public readonly STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID!: string;
@IsNotEmpty()
public readonly STRIPE_UNLIMITED_ANNUAL_SUBSCRIPTION_PRICE_ID!: string;
@IsNotEmpty()
public readonly STRIPE_PAYMENT_SUCCESS_URL!: string;
@IsNotEmpty()
public readonly STRIPE_PAYMENT_CANCEL_URL!: string;
@IsNotEmpty()
public readonly IDNOT_PROD_BASE_URL!: string;
public constructor() {
dotenv.config();
this.DATABASE_PORT = process.env["DATABASE_PORT"]!;
@ -190,6 +214,14 @@ export class BackendVariables {
this.SCW_ACCESS_KEY_SECRET = process.env["ACCESS_KEY_SECRET"]!;
this.SCW_BUCKET_ENDPOINT = process.env["BUCKET_ENDPOINT"]!;
this.SCW_BUCKET_NAME = process.env["BUCKET_NAME"]!;
this.STRIPE_SECRET_KEY = process.env["STRIPE_SECRET_KEY"]!;
this.STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID = process.env["STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID"]!;
this.STRIPE_STANDARD_ANNUAL_SUBSCRIPTION_PRICE_ID = process.env["STRIPE_STANDARD_ANNUAL_SUBSCRIPTION_PRICE_ID"]!;
this.STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID = process.env["STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID"]!;
this.STRIPE_UNLIMITED_ANNUAL_SUBSCRIPTION_PRICE_ID = process.env["STRIPE_UNLIMITED_ANNUAL_SUBSCRIPTION_PRICE_ID"]!;
this.STRIPE_PAYMENT_SUCCESS_URL = process.env["STRIPE_PAYMENT_SUCCESS_URL"]!;
this.STRIPE_PAYMENT_CANCEL_URL = process.env["STRIPE_PAYMENT_CANCEL_URL"]!;
this.IDNOT_PROD_BASE_URL = process.env["IDNOT_PROD_BASE_URL"]!;
}
public async validate(groups?: string[]) {
const validationOptions = groups ? { groups } : undefined;

View File

@ -0,0 +1,16 @@
-- CreateTable
CREATE TABLE "user_whitelist" (
"uid" TEXT NOT NULL,
"idNot" VARCHAR(255) NOT NULL,
"active" BOOLEAN NOT NULL DEFAULT true,
"created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3),
CONSTRAINT "user_whitelist_pkey" PRIMARY KEY ("uid")
);
-- CreateIndex
CREATE UNIQUE INDEX "user_whitelist_uid_key" ON "user_whitelist"("uid");
-- CreateIndex
CREATE UNIQUE INDEX "user_whitelist_idNot_key" ON "user_whitelist"("idNot");

View File

@ -0,0 +1,34 @@
-- CreateTable
CREATE TABLE "subscriptions" (
"uid" TEXT NOT NULL,
"start_date" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
"end_date" TIMESTAMP(3),
"nb_seats" INTEGER NOT NULL,
"office_uid" VARCHAR(255) NOT NULL,
CONSTRAINT "subscriptions_pkey" PRIMARY KEY ("uid")
);
-- CreateTable
CREATE TABLE "seats" (
"uid" TEXT NOT NULL,
"subscription_uid" VARCHAR(255) NOT NULL,
"user_uid" VARCHAR(255) NOT NULL,
CONSTRAINT "seats_pkey" PRIMARY KEY ("uid")
);
-- CreateIndex
CREATE UNIQUE INDEX "subscriptions_uid_key" ON "subscriptions"("uid");
-- CreateIndex
CREATE UNIQUE INDEX "seats_uid_key" ON "seats"("uid");
-- AddForeignKey
ALTER TABLE "subscriptions" ADD CONSTRAINT "subscriptions_office_uid_fkey" FOREIGN KEY ("office_uid") REFERENCES "offices"("uid") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "seats" ADD CONSTRAINT "seats_subscription_uid_fkey" FOREIGN KEY ("subscription_uid") REFERENCES "subscriptions"("uid") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "seats" ADD CONSTRAINT "seats_user_uid_fkey" FOREIGN KEY ("user_uid") REFERENCES "users"("uid") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -0,0 +1,12 @@
/*
Warnings:
- Added the required column `priceId` to the `subscriptions` table without a default value. This is not possible if the table is not empty.
- Made the column `start_date` on table `subscriptions` required. This step will fail if there are existing NULL values in that column.
- Made the column `end_date` on table `subscriptions` required. This step will fail if there are existing NULL values in that column.
*/
-- AlterTable
ALTER TABLE "subscriptions" ADD COLUMN "priceId" VARCHAR(255) NOT NULL,
ALTER COLUMN "start_date" SET NOT NULL,
ALTER COLUMN "end_date" SET NOT NULL;

View File

@ -0,0 +1,11 @@
/*
Warnings:
- Added the required column `type` to the `subscriptions` table without a default value. This is not possible if the table is not empty.
*/
-- CreateEnum
CREATE TYPE "ESubscriptionType" AS ENUM ('STANDARD', 'UNLIMITED');
-- AlterTable
ALTER TABLE "subscriptions" ADD COLUMN "type" "ESubscriptionType" NOT NULL;

View File

@ -0,0 +1,10 @@
/*
Warnings:
- You are about to drop the column `priceId` on the `subscriptions` table. All the data in the column will be lost.
- Added the required column `stripe_subscription_id` to the `subscriptions` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "subscriptions" DROP COLUMN "priceId",
ADD COLUMN "stripe_subscription_id" VARCHAR(255) NOT NULL;

View File

@ -0,0 +1,5 @@
-- CreateEnum
CREATE TYPE "ESubscriptionStatus" AS ENUM ('ACTIVE', 'INACTIVE');
-- AlterTable
ALTER TABLE "subscriptions" ADD COLUMN "status" "ESubscriptionStatus" NOT NULL DEFAULT 'INACTIVE';

View File

@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "subscriptions" ALTER COLUMN "status" SET DEFAULT 'ACTIVE';

View File

@ -0,0 +1,7 @@
-- AlterTable
ALTER TABLE "seats" ADD COLUMN "created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3);
-- AlterTable
ALTER TABLE "subscriptions" ADD COLUMN "created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
ADD COLUMN "updated_at" TIMESTAMP(3);

View File

@ -0,0 +1,30 @@
-- CreateTable
CREATE TABLE "rules_groups" (
"uid" TEXT NOT NULL,
"name" VARCHAR(255) NOT NULL,
"created_at" TIMESTAMP(3) DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3),
CONSTRAINT "rules_groups_pkey" PRIMARY KEY ("uid")
);
-- CreateTable
CREATE TABLE "_RulesGroupsHasRules" (
"A" TEXT NOT NULL,
"B" TEXT NOT NULL
);
-- CreateIndex
CREATE UNIQUE INDEX "rules_groups_uid_key" ON "rules_groups"("uid");
-- CreateIndex
CREATE UNIQUE INDEX "_RulesGroupsHasRules_AB_unique" ON "_RulesGroupsHasRules"("A", "B");
-- CreateIndex
CREATE INDEX "_RulesGroupsHasRules_B_index" ON "_RulesGroupsHasRules"("B");
-- AddForeignKey
ALTER TABLE "_RulesGroupsHasRules" ADD CONSTRAINT "_RulesGroupsHasRules_A_fkey" FOREIGN KEY ("A") REFERENCES "rules"("uid") ON DELETE CASCADE ON UPDATE CASCADE;
-- AddForeignKey
ALTER TABLE "_RulesGroupsHasRules" ADD CONSTRAINT "_RulesGroupsHasRules_B_fkey" FOREIGN KEY ("B") REFERENCES "rules_groups"("uid") ON DELETE CASCADE ON UPDATE CASCADE;

View File

@ -68,6 +68,7 @@ model Users {
appointment Appointments[]
votes Votes[]
user_notifications UserNotifications[]
seats Seats[]
@@map("users")
}
@ -81,6 +82,15 @@ model Whitelist {
@@map("whitelist")
}
model UserWhitelist {
uid String @id @unique @default(uuid())
idNot String @unique @db.VarChar(255)
active Boolean @default(true)
created_at DateTime? @default(now())
updated_at DateTime? @updatedAt
@@map("user_whitelist")
}
model Offices {
uid String @id @unique @default(uuid())
idNot String @unique @db.VarChar(255)
@ -99,6 +109,7 @@ model Offices {
office_folders OfficeFolders[]
document_types DocumentTypes[]
office_roles OfficeRoles[]
subscriptions Subscriptions[]
@@map("offices")
}
@ -312,9 +323,19 @@ model Rules {
role Roles[] @relation("RolesHasRules")
office_roles OfficeRoles[] @relation("OfficeRolesHasRules")
namespace String @db.VarChar(255) @default("notary")
groups RulesGroups[] @relation("RulesGroupsHasRules")
@@map("rules")
}
model RulesGroups {
uid String @id @unique @default(uuid())
name String @db.VarChar(255)
created_at DateTime? @default(now())
updated_at DateTime? @updatedAt
rules Rules[] @relation("RulesGroupsHasRules")
@@map("rules_groups")
}
model Emails {
uid String @id @unique @default(uuid())
templateName String @db.VarChar(255)
@ -366,6 +387,43 @@ model TotpCodes {
@@map("totp_codes")
}
model Subscriptions {
uid String @id @unique @default(uuid())
type ESubscriptionType
status ESubscriptionStatus @default(ACTIVE)
stripe_subscription_id String @db.VarChar(255)
start_date DateTime @default(now())
end_date DateTime
nb_seats Int
office Offices @relation(fields: [office_uid], references: [uid], onDelete: Cascade)
office_uid String @db.VarChar(255)
seats Seats[]
created_at DateTime? @default(now())
updated_at DateTime? @updatedAt
@@map("subscriptions")
}
model Seats {
uid String @id @unique @default(uuid())
subscription Subscriptions @relation(fields: [subscription_uid], references: [uid], onDelete: Cascade)
subscription_uid String @db.VarChar(255)
user Users @relation(fields: [user_uid], references: [uid], onDelete: Cascade)
user_uid String @db.VarChar(255)
created_at DateTime? @default(now())
updated_at DateTime? @updatedAt
@@map("seats")
}
enum ESubscriptionStatus {
ACTIVE
INACTIVE
}
enum ESubscriptionType {
STANDARD
UNLIMITED
}
enum TotpCodesReasons {
LOGIN
RESET_PASSWORD

View File

@ -795,6 +795,74 @@ export default async function main() {
updated_at: new Date(),
namespace: "notary",
},
{
name: "GET subscriptions",
label: "Récupérer les abonnements",
created_at: new Date(),
updated_at: new Date(),
namespace: "notary",
},
{
name: "POST subscriptions",
label: "Inviter un collaborateur à l'abonnement",
created_at: new Date(),
updated_at: new Date(),
namespace: "notary",
},
{
name: "PUT subscriptions",
label: "Modifier l'abonnement",
created_at: new Date(),
updated_at: new Date(),
namespace: "notary",
},
{
name: "GET stripe",
label: "Gérer l'abonnement de l'office",
created_at: new Date(),
updated_at: new Date(),
namespace: "notary",
},
{
name: "POST stripe",
label: "Payer un abonnement",
created_at: new Date(),
updated_at: new Date(),
namespace: "notary",
},
];
const rulesGroups = [
{
name: "Gestion des types d'actes et des documents",
created_at: new Date(),
updated_at: new Date(),
rules: [
"POST deeds",
"PUT deeds",
"DELETE deeds",
]
},
{
name: "Gestion de l'abonnement",
created_at: new Date(),
updated_at: new Date(),
rules : [
"GET subscriptions",
"POST subscriptions",
"PUT subscriptions",
"GET stripe",
"POST stripe",
]
},
{
name: "Gestion du RIB",
created_at: new Date(),
updated_at: new Date(),
rules : [
"PUT rib"
]
},
];
const collaboratorRules = rules.filter((rule) => rule.namespace === "collaborator");
@ -1869,6 +1937,19 @@ export default async function main() {
role.uid = roleCreated.uid;
}
for (const ruleGroup of rulesGroups) {
await prisma.rulesGroups.create({
data: {
name: ruleGroup.name,
rules: {
connect: ruleGroup.rules?.map((rule) => ({
uid: rules.find((r) => r.name === rule)!.uid!,
})),
},
},
});
}
for (const officeRole of officeRoles) {
const officeRoleCreated = await prisma.officeRoles.create({
data: {

View File

@ -106,4 +106,35 @@ export default class EmailBuilder {
if (civility === "MALE") return "Mr";
else return "Mme";
}
public async sendInvitationEmails(emails: string[]) {
emails.forEach((email) => {
const to = email;
const templateVariables = {
link: this.variables.APP_HOST,
idNotLink: this.variables.IDNOT_PROD_BASE_URL,
};
const templateName = ETemplates.SUBSCRIPTION_INVITATION;
const subject = "Invitation abonnement LeCoffre";
this.mailchimpService.create({
templateName,
to,
subject,
templateVariables,
uid: "",
from: null,
cc: [],
cci: [],
sentAt: null,
nbTrySend: null,
lastTrySendDate: null,
});
this.mailchimpService.sendEmails();
});
}
}

View File

@ -2,4 +2,5 @@ export const ETemplates = {
DOCUMENT_ASKED: "DOCUMENT_ASKED",
DOCUMENT_REFUSED: "DOCUMENT_REFUSED",
DOCUMENT_RECAP: "DOCUMENT_RECAP",
SUBSCRIPTION_INVITATION: "SUBSCRIPTION_INVITATION",
};

View File

@ -0,0 +1,78 @@
import Database from "@Common/databases/database";
import BaseRepository from "@Repositories/BaseRepository";
import { Service } from "typedi";
import { Prisma, Seats } from "@prisma/client";
@Service()
export default class SeatsRepository extends BaseRepository {
constructor(private database: Database) {
super();
}
protected get model() {
return this.database.getClient().seats;
}
protected get instanceDb() {
return this.database.getClient();
}
/**
* @description : Find many seats
*/
public async findMany(query: Prisma.SeatsFindManyArgs) {
query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows);
if (!query.include) return this.model.findMany({ ...query });
return this.model.findMany({ ...query });
}
/**
* @description : find unique seat
*/
public async findOneByUid(uid: string, query?: Prisma.SeatsInclude): Promise<Seats | null> {
return this.model.findUnique({
where: {
uid: uid,
},
include: query,
});
}
/**
* @description : Create a seat
*/
public async create(userUid: string, subscriptionUid: string): Promise<Seats> {
const createArgs: Prisma.SeatsCreateArgs = {
data: {
subscription: {
connect: {
uid: subscriptionUid
},
},
user: {
connect: {
uid: userUid
},
},
},
};
{
return this.model.create(createArgs);
}
}
/**
* @description : Delete a seat
*/
public async delete(uid: string) {
return this.model.delete({
where: {
uid: uid,
},
});
}
}

View File

@ -0,0 +1,154 @@
import Database from "@Common/databases/database";
import BaseRepository from "@Repositories/BaseRepository";
import { Service } from "typedi";
import { ESubscriptionStatus, ESubscriptionType, Prisma, Subscriptions } from "@prisma/client";
import { Subscription } from "le-coffre-resources/dist/Admin";
@Service()
export default class SubscriptionsRepository extends BaseRepository {
constructor(private database: Database) {
super();
}
protected get model() {
return this.database.getClient().subscriptions;
}
protected get instanceDb() {
return this.database.getClient();
}
/**
* @description : Find many subscriptions
*/
public async findMany(query: Prisma.SubscriptionsFindManyArgs) {
query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows);
if (!query.include) return this.model.findMany({ ...query });
return this.model.findMany({ ...query });
}
/**
* @description : find unique subscription
*/
public async findOneByUid(uid: string, query?: Prisma.SubscriptionsInclude): Promise<Subscriptions | null> {
return this.model.findUnique({
where: {
uid: uid,
},
include: query,
});
}
/**
* @description : Create a subscription
*/
public async create(subscription: Subscription): Promise<Subscriptions> {
if(subscription.type === "STANDARD")
{
const createArgs: Prisma.SubscriptionsCreateArgs = {
data: {
start_date: subscription.start_date,
end_date: subscription.end_date,
type: ESubscriptionType.STANDARD,
status: ESubscriptionStatus.ACTIVE,
nb_seats: subscription.nb_seats!,
stripe_subscription_id: subscription.stripe_subscription_id || "",
office: {
connect: {
uid: subscription.office!.uid,
},
},
},
};
return this.model.create(createArgs);
}
else
{
const createArgs: Prisma.SubscriptionsCreateArgs = {
data: {
start_date: subscription.start_date,
end_date: subscription.end_date,
type: ESubscriptionType.UNLIMITED,
status: ESubscriptionStatus.ACTIVE,
nb_seats: 0,
stripe_subscription_id: subscription.stripe_subscription_id || "",
office: {
connect: {
uid: subscription.office!.uid,
},
},
},
};
return this.model.create(createArgs);
}
}
/**
* @description : update given subscription
*/
public async update(uid: string, subscription: Subscription): Promise<Subscriptions> {
if(!subscription.type || subscription.type === ""){
const updateArgs: Prisma.SubscriptionsUpdateArgs = {
where: {
uid: uid,
},
data: {
seats:{
deleteMany: {},
createMany: {
data: subscription.seats?.map((seat) => ({
user_uid: seat.user.uid || "",
})) ?? [],
},
}
},
};
return this.model.update(updateArgs);
}
if(subscription.type === "STANDARD")
{
const updateArgs: Prisma.SubscriptionsUpdateArgs = {
where: {
uid: uid,
},
data: {
end_date: subscription.end_date,
type: ESubscriptionType.STANDARD,
status: subscription.status as ESubscriptionStatus,
nb_seats: subscription.nb_seats!,
},
};
return this.model.update(updateArgs);
}
else
{
const updateArgs: Prisma.SubscriptionsUpdateArgs = {
where: {
uid: uid,
},
data: {
end_date: subscription.end_date,
type: ESubscriptionType.UNLIMITED,
status: subscription.status as ESubscriptionStatus,
nb_seats: 0,
seats: {
deleteMany: {},
},
},
};
return this.model.update(updateArgs);
}
}
/**
* @description : Delete a subscription
*/
public async delete(uid: string) {
return this.model.delete({
where: {
uid: uid,
},
});
}
}

View File

@ -0,0 +1,39 @@
import Database from "@Common/databases/database";
import BaseRepository from "@Repositories/BaseRepository";
import { Service } from "typedi";
import { Prisma } from "prisma/prisma-client";
@Service()
export default class UserWhitelistRepository extends BaseRepository {
constructor(private database: Database) {
super();
}
protected get model() {
return this.database.getClient().userWhitelist;
}
protected get instanceDb() {
return this.database.getClient();
}
/**
* @description : Find many whitelist
*/
public async findMany(query: Prisma.UserWhitelistFindManyArgs) {
query.take = Math.min(query.take || this.defaultFetchRows, this.maxFetchRows);
return this.model.findMany(query);
}
/**
* @description : find unique by email
*/
public async findOneByIdNotId(idNotId: string) {
return this.model.findMany({
where: {
idNot: idNotId,
},
});
}
}

View File

@ -0,0 +1,104 @@
import { BackendVariables } from "@Common/config/variables/Variables";
import ApiController from "@Common/system/controller-pattern/ApiController";
import { Controller, Post } from "@ControllerPattern/index";
import SubscriptionsService from "@Services/admin/SubscriptionsService/SubscriptionsService.ts";
import StripeService from "@Services/common/StripeService/StripeService";
import { validateOrReject } from "class-validator";
import { Request, Response } from "express";
import { Subscription } from "le-coffre-resources/dist/Admin";
import { Service } from "typedi";
@Controller()
@Service()
export default class StripeWebhooks extends ApiController {
constructor(private stripeService: StripeService, private subscriptionsService: SubscriptionsService, private backendVariables: BackendVariables) {
super();
}
/**
* @description Create a new checkout session
*/
@Post("/api/v1/webhooks/stripe")
protected async post(req: Request, response: Response) {
try {
const event = req.body;
switch (event.type) {
//Manage plan switch + recurring payment
case "invoice.payment_succeeded":
if (event.data.object.billing_reason !== "subscription_update" && event.data.object.billing_reason !== "subscription_cycle") break;
const stripeSubscription = await this.stripeService.getClient().subscriptions.retrieve(event.data.object.subscription);
if(stripeSubscription.metadata['env'] !== this.backendVariables.ENV) break;
const existingSubscription = await this.subscriptionsService.get({where : {stripe_subscription_id : stripeSubscription.id}});
if(!existingSubscription[0]) break;
const subscriptionUpdate: any = {};
subscriptionUpdate.start_date = new Date(stripeSubscription.current_period_start * 1000);
subscriptionUpdate.end_date = new Date(stripeSubscription.current_period_end * 1000);
subscriptionUpdate.nb_seats = stripeSubscription.items.data[0]?.quantity;
if(stripeSubscription.items.data[0]?.price?.id === this.backendVariables.STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID || stripeSubscription.items.data[0]?.price?.id === this.backendVariables.STRIPE_STANDARD_ANNUAL_SUBSCRIPTION_PRICE_ID){
subscriptionUpdate.type = "STANDARD";
}
else{
subscriptionUpdate.type = "UNLIMITED";
}
const subscriptionEntityUpdate = Subscription.hydrate<Subscription>(subscriptionUpdate);
await validateOrReject(subscriptionEntityUpdate, { groups: ["updateSubscription"], forbidUnknownValues: false });
await this.subscriptionsService.update(existingSubscription[0].uid ,subscriptionEntityUpdate);
//Manage subscription creation and first payment
case "checkout.session.completed":
if (event.data.object.status !== "complete") break;
const subscription = JSON.parse(event.data.object.metadata.subscription);
const env = event.data.object.metadata.env;
if (env !== this.backendVariables.ENV) break;
subscription.stripe_subscription_id = event.data.object.subscription;
await this.stripeService.getClient().subscriptions.update(subscription.stripe_subscription_id, {
metadata: {
env: env,
},
}
)
const subscriptionInfo = await this.stripeService
.getClient()
.subscriptions.retrieve(subscription.stripe_subscription_id);
subscription.start_date = new Date(subscriptionInfo.current_period_start * 1000);
subscription.end_date = new Date(subscriptionInfo.current_period_end * 1000);
const subscriptionEntity = Subscription.hydrate<Subscription>(subscription);
await validateOrReject(subscriptionEntity, { groups: ["createSubscription"], forbidUnknownValues: false });
await this.subscriptionsService.create(subscriptionEntity);
break;
//Manage plan expiration
case "customer.subscription.deleted":
const currentSubscription = await this.stripeService.getClient().subscriptions.retrieve(event.data.object.id);
if(currentSubscription.metadata['env'] !== this.backendVariables.ENV) break;
const subscriptionToDelete = await this.subscriptionsService.get({where : {stripe_subscription_id : event.data.object.id}});
if(!subscriptionToDelete[0]) break;
await this.subscriptionsService.delete(subscriptionToDelete[0].uid);
default:
break;
}
response.json({ received: true });
} catch (error) {
this.httpInternalError(response, error);
return;
}
}
}

View File

@ -0,0 +1,45 @@
import BaseService from "@Services/BaseService";
import "reflect-metadata";
import { Service } from "typedi";
import { Prisma, Seats } from "@prisma/client";
import SeatsRepository from "@Repositories/SeatsRepository";
@Service()
export default class SeatsService extends BaseService {
constructor(private seatsRepository: SeatsRepository) {
super();
}
/**
* @description : Get all seats
* @throws {Error} If seats cannot be get
*/
public get(query: Prisma.SeatsFindManyArgs) {
return this.seatsRepository.findMany(query);
}
/**
* @description : Get a seat by uid
* @throws {Error} If seat is not found
*/
public async getByUid(uid: string, query?: Prisma.SeatsInclude) {
return this.seatsRepository.findOneByUid(uid, query);
}
/**
* @description : Create a new seat
* @throws {Error} If seat cannot be created
*/
public async create(subscriptionUid: string, userUid: string): Promise<Seats> {
return this.seatsRepository.create(subscriptionUid, userUid);
}
/**
* @description : Delete a seat
* @throws {Error} If seat cannot be deleted
*/
public async delete(uid: string) {
return this.seatsRepository.delete(uid);
}
}

View File

@ -0,0 +1,63 @@
import BaseService from "@Services/BaseService";
import "reflect-metadata";
import { Service } from "typedi";
import { Prisma, Subscriptions } from "@prisma/client";
import SubscriptionsRepository from "@Repositories/SubscriptionsRepository";
import { Subscription } from "le-coffre-resources/dist/Admin";
import SeatsService from "../SeatsService/SeatsService";
@Service()
export default class SubscriptionsService extends BaseService {
constructor(private subscriptionsRepository: SubscriptionsRepository, private seatsService: SeatsService) {
super();
}
/**
* @description : Get all subscriptions
* @throws {Error} If subscriptions cannot be get
*/
public get(query: Prisma.SubscriptionsFindManyArgs) {
return this.subscriptionsRepository.findMany(query);
}
/**
* @description : Get a subscription by uid
* @throws {Error} If subscription is not found
*/
public async getByUid(uid: string, query?: Prisma.SubscriptionsInclude) {
return this.subscriptionsRepository.findOneByUid(uid, query);
}
/**
* @description : Create a new subscription
* @throws {Error} If subsctiption cannot be created
*/
public async create(subscriptionEntity: Subscription): Promise<Subscriptions> {
return this.subscriptionsRepository.create(subscriptionEntity);
}
/**
* @description : Modify a subscription
* @throws {Error} If subscription cannot be modified
*/
public async update(uid: string, subscriptionEntity: Subscription): Promise<Subscriptions> {
if(subscriptionEntity.type === "STANDARD"){
const seats = await this.seatsService.get({ where: { subscription: { uid: uid } }, orderBy: {created_at: 'asc'} });
const seatsToKeep = subscriptionEntity.nb_seats;
const seatsToDelete = seats.slice(seatsToKeep);
for (const seat of seatsToDelete) {
await this.seatsService.delete(seat.uid);
}
}
return this.subscriptionsRepository.update(uid, subscriptionEntity);
}
/**
* @description : Delete a subscription
* @throws {Error} If subscription cannot be deleted
*/
public async delete(uid: string) {
return this.subscriptionsRepository.delete(uid);
}
}

View File

@ -147,7 +147,7 @@ export default class IdNotService extends BaseService {
case EIdnotRole.SUPPLEANT:
return (await this.rolesService.get({ where: { name: "notary" } }))[0]!;
case EIdnotRole.ADMINISTRATEUR:
return (await this.rolesService.get({ where: { name: "notary" } }))[0]!;
return (await this.rolesService.get({ where: { name: "admin" } }))[0]!;
case EIdnotRole.CURATEUR:
return (await this.rolesService.get({ where: { name: "notary" } }))[0]!;
default:

View File

@ -0,0 +1,104 @@
import { BackendVariables } from "@Common/config/variables/Variables";
import { Subscription } from "le-coffre-resources/dist/Admin";
import Stripe from "stripe";
import { Service } from "typedi";
@Service()
export default class StripeService {
private client: Stripe;
constructor(protected variables: BackendVariables) {
this.client = new Stripe(variables.STRIPE_SECRET_KEY);
}
public getClient(): Stripe {
return this.client;
}
public async createCheckoutSession(subscription: Subscription) {
const priceId = subscription.type === "STANDARD" ? this.variables.STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID : this.variables.STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID;
return this.client.checkout.sessions.create({
mode: "subscription",
payment_method_types: ["card", "paypal"],
billing_address_collection: "auto",
line_items: [
{
price: priceId,
quantity: subscription.type === "STANDARD" ? subscription.nb_seats : 1,
},
],
success_url: this.variables.APP_HOST + "/subscription/success",
cancel_url: this.variables.APP_HOST + "/subscription/error",
metadata: {
subscription: JSON.stringify(subscription),
env: this.variables.ENV,
},
});
}
public async createCheckoutSessionUpdate(uid: string, subscription: Subscription) {
// return this.client.checkout.sessions.create({
// mode: "payment",
// payment_method_types: ["card", "paypal"],
// billing_address_collection: "auto",
// success_url: this.variables.APP_HOST + "/subscription/success",
// cancel_url: this.variables.APP_HOST + "/subscription/error",
// metadata: {
// subscription: JSON.stringify(subscription),
// },
// });
// const priceId =
// subscription.type === "STANDARD"
// ? this.variables.STRIPE_STANDARD_SUBSCRIPTION_PRICE_ID
// : this.variables.STRIPE_UNLIMITED_SUBSCRIPTION_PRICE_ID;
// return this.client.checkout.sessions.create({
// mode: "subscription",
// payment_method_types: ["card", "paypal"],
// billing_address_collection: "auto",
// line_items: [
// {
// price: priceId,
// quantity: subscription.type === "STANDARD" ? subscription.nb_seats : 1,
// },
// ],
// success_url: this.variables.APP_HOST + "/subscription/success",
// cancel_url: this.variables.APP_HOST + "/subscription/error",
// metadata: {
// subscription: JSON.stringify(subscription),
// },
// });
// const subscriptions = await this.client.subscriptions.retrieve(uid);
// const itemId = subscriptions.items.data[0]?.id;
// return await this.client.subscriptions.update(uid, {
// items: [
// {
// id: itemId,
// price: priceId,
// quantity: subscription.nb_seats,
// },
// ],
// });
}
public async createClientPortalSession(subscriptionId: string) {
const subscription = await this.client.subscriptions.retrieve(subscriptionId);
return this.client.billingPortal.sessions.create({
customer: subscription.customer as string,
return_url: this.variables.APP_HOST + "/subscription/manage",
});
}
public async getCustomerBySubscription(subscriptionId: string) {
const subscription = await this.client.subscriptions.retrieve(subscriptionId);
return this.client.customers.retrieve(subscription.customer as string);
}
}

View File

@ -0,0 +1,14 @@
import UserWhitelistRepository from "@Repositories/UserWhitelistRepository";
import BaseService from "@Services/BaseService";
import { Service } from "typedi";
@Service()
export default class UserWhitelistService extends BaseService {
constructor(private userWhitelistRepository: UserWhitelistRepository) {
super();
}
public async getByIdNotId(idNotId: string): Promise<any> {
return this.userWhitelistRepository.findOneByIdNotId(idNotId);
}
}

View File

@ -2,7 +2,7 @@ import BaseService from "@Services/BaseService";
import "reflect-metadata";
import { Service } from "typedi";
import UsersRepository from "@Repositories/UsersRepository";
import User from "le-coffre-resources/dist/SuperAdmin";
import User from "le-coffre-resources/dist/Admin";
import { Prisma, Users } from "@prisma/client";
@Service()
@ -91,4 +91,5 @@ export default class UsersService extends BaseService {
return this.userRepository.findManyToCheck();
}
}