Subscriptions (#186)
This commit is contained in:
commit
7081fa046e
@ -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.126",
|
||||
"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",
|
||||
|
42
src/app/api/admin/StripeController.ts
Normal file
42
src/app/api/admin/StripeController.ts
Normal file
@ -0,0 +1,42 @@
|
||||
import authHandler from "@App/middlewares/AuthHandler";
|
||||
// import roleHandler from "@App/middlewares/RolesHandler";
|
||||
import ApiController from "@Common/system/controller-pattern/ApiController";
|
||||
import { Controller, 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 post(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;
|
||||
}
|
||||
}
|
||||
}
|
145
src/app/api/admin/SubscriptionsController.ts
Normal file
145
src/app/api/admin/SubscriptionsController.ts
Normal file
@ -0,0 +1,145 @@
|
||||
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 { validateOrReject } from "class-validator";
|
||||
import ObjectHydrate from "@Common/helpers/ObjectHydrate";
|
||||
import roleHandler from "@App/middlewares/RolesHandler";
|
||||
import authHandler from "@App/middlewares/AuthHandler";
|
||||
|
||||
@Controller()
|
||||
@Service()
|
||||
export default class SubscriptionsController extends ApiController {
|
||||
constructor(private subscriptionsService: SubscriptionsService) {
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
@ -6,13 +6,17 @@ import AuthService, { IUserJwtPayload } from "@Services/common/AuthService/AuthS
|
||||
|
||||
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 whitelistService: WhitelistService, private userService: UsersService, private subscriptionsService: SubscriptionsService, private seatsService: SeatsService) {
|
||||
super();
|
||||
}
|
||||
|
||||
@ -38,7 +42,7 @@ export default class UserController extends ApiController {
|
||||
const user = await this.idNotService.getOrCreateUser(idNotToken);
|
||||
|
||||
if(!user) {
|
||||
this.httpUnauthorized(response, "Email not found");
|
||||
this.httpUnauthorized(response, "User not found");
|
||||
return;
|
||||
}
|
||||
|
||||
@ -61,10 +65,51 @@ export default class UserController extends ApiController {
|
||||
return;
|
||||
}
|
||||
|
||||
//Check if user is whitelisted
|
||||
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) {
|
||||
this.httpUnauthorized(response, "User not subscribed");
|
||||
isSubscribed = false;
|
||||
return;
|
||||
}
|
||||
|
||||
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(!isSubscribed) {
|
||||
this.httpUnauthorized(response, "User not subscribed");
|
||||
return;
|
||||
}
|
||||
|
||||
//Check if user is whitelisted
|
||||
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);
|
||||
|
@ -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);
|
||||
},
|
||||
};
|
||||
|
@ -142,6 +142,21 @@ 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_UNLIMITED_SUBSCRIPTION_PRICE_ID!: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
public readonly STRIPE_PAYMENT_SUCCESS_URL!: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
public readonly STRIPE_PAYMENT_CANCEL_URL!: string;
|
||||
|
||||
public constructor() {
|
||||
dotenv.config();
|
||||
this.DATABASE_PORT = process.env["DATABASE_PORT"]!;
|
||||
@ -190,6 +205,11 @@ export class BackendVariables {
|
||||
this.SCW_ACCESS_KEY_SECRET = process.env["SCW_ACCESS_KEY_SECRET"]!;
|
||||
this.SCW_BUCKET_ENDPOINT = process.env["SCW_BUCKET_ENDPOINT"]!;
|
||||
this.SCW_BUCKET_NAME = process.env["SCW_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_UNLIMITED_SUBSCRIPTION_PRICE_ID = process.env["STRIPE_UNLIMITED_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"]!;
|
||||
}
|
||||
public async validate(groups?: string[]) {
|
||||
const validationOptions = groups ? { groups } : undefined;
|
||||
|
@ -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");
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -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;
|
@ -0,0 +1,5 @@
|
||||
-- CreateEnum
|
||||
CREATE TYPE "ESubscriptionStatus" AS ENUM ('ACTIVE', 'INACTIVE');
|
||||
|
||||
-- AlterTable
|
||||
ALTER TABLE "subscriptions" ADD COLUMN "status" "ESubscriptionStatus" NOT NULL DEFAULT 'INACTIVE';
|
@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "subscriptions" ALTER COLUMN "status" SET DEFAULT 'ACTIVE';
|
@ -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")
|
||||
}
|
||||
@ -366,6 +377,39 @@ 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[]
|
||||
@@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)
|
||||
@@map("seats")
|
||||
}
|
||||
|
||||
enum ESubscriptionStatus {
|
||||
ACTIVE
|
||||
INACTIVE
|
||||
}
|
||||
|
||||
enum ESubscriptionType {
|
||||
STANDARD
|
||||
UNLIMITED
|
||||
}
|
||||
|
||||
enum TotpCodesReasons {
|
||||
LOGIN
|
||||
RESET_PASSWORD
|
||||
|
67
src/common/repositories/SeatsRepository.ts
Normal file
67
src/common/repositories/SeatsRepository.ts
Normal file
@ -0,0 +1,67 @@
|
||||
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 subscriptions
|
||||
*/
|
||||
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 subscription
|
||||
*/
|
||||
public async findOneByUid(uid: string, query?: Prisma.SeatsInclude): Promise<Seats | null> {
|
||||
return this.model.findUnique({
|
||||
where: {
|
||||
uid: uid,
|
||||
},
|
||||
include: query,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* @description : Create a subscription
|
||||
*/
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
}
|
145
src/common/repositories/SubscriptionsRepository.ts
Normal file
145
src/common/repositories/SubscriptionsRepository.ts
Normal file
@ -0,0 +1,145 @@
|
||||
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,
|
||||
},
|
||||
},
|
||||
seats: {
|
||||
create: subscription.seats!.map(seat => ({
|
||||
user: {
|
||||
connect: {
|
||||
uid: seat.user!.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 === "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!,
|
||||
seats: {
|
||||
deleteMany: {},
|
||||
create: subscription.seats!.map(seat => ({
|
||||
user: {
|
||||
connect: {
|
||||
uid: seat.user!.uid,
|
||||
},
|
||||
},
|
||||
})),
|
||||
}
|
||||
},
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
39
src/common/repositories/UserWhitelistRepository.ts
Normal file
39
src/common/repositories/UserWhitelistRepository.ts
Normal 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,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
66
src/common/webhooks/stripeWebhooks.ts
Normal file
66
src/common/webhooks/stripeWebhooks.ts
Normal file
@ -0,0 +1,66 @@
|
||||
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) {
|
||||
super();
|
||||
}
|
||||
|
||||
/**
|
||||
* @description Create a new checkout session
|
||||
*/
|
||||
@Post("/api/v1/webhooks/stripe")
|
||||
protected async post(req: Request, response: Response) {
|
||||
try {
|
||||
// const sig = req.headers["stripe-signature"];
|
||||
// const endpointSecret = "whsec_c4088876914bc166ff5c39253207f84900820b67f7bba3b2669c0ff392cbc838";
|
||||
// const stripe = this.stripeService.getClient();
|
||||
// let event: Stripe.Event;
|
||||
|
||||
// if (!sig || !endpointSecret) {
|
||||
// throw new Error("Signature verification failed");
|
||||
// }
|
||||
// event = stripe.webhooks.constructEvent(req.body, sig, endpointSecret);
|
||||
const event = req.body;
|
||||
|
||||
switch (event.type) {
|
||||
case "checkout.session.completed":
|
||||
if (event.data.object.status !== "complete") break;
|
||||
console.log(event.data.object);
|
||||
|
||||
|
||||
const subscription = JSON.parse(event.data.object.metadata.subscription);
|
||||
subscription.stripe_subscription_id = event.data.object.subscription;
|
||||
|
||||
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;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
response.json({ received: true });
|
||||
} catch (error) {
|
||||
this.httpInternalError(response, error);
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
37
src/services/admin/SeatsService/SeatsService.ts
Normal file
37
src/services/admin/SeatsService/SeatsService.ts
Normal file
@ -0,0 +1,37 @@
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,45 @@
|
||||
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";
|
||||
|
||||
@Service()
|
||||
export default class SubscriptionsService extends BaseService {
|
||||
constructor(private subscriptionsRepository: SubscriptionsRepository) {
|
||||
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> {
|
||||
return this.subscriptionsRepository.update(uid, subscriptionEntity);
|
||||
}
|
||||
}
|
36
src/services/common/StripeService/StripeService.ts
Normal file
36
src/services/common/StripeService/StripeService.ts
Normal file
@ -0,0 +1,36 @@
|
||||
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.nb_seats,
|
||||
},
|
||||
],
|
||||
success_url: this.variables.STRIPE_PAYMENT_SUCCESS_URL,
|
||||
cancel_url: this.variables.STRIPE_PAYMENT_CANCEL_URL,
|
||||
metadata: {
|
||||
subscription: JSON.stringify(subscription),
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
14
src/services/common/UserWhitelistService/WhitelistService.ts
Normal file
14
src/services/common/UserWhitelistService/WhitelistService.ts
Normal 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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user