From 7e093251faaaf2ddc5681e2a064386350f3ca658 Mon Sep 17 00:00:00 2001 From: Emily Date: Fri, 28 Mar 2025 16:57:57 +0100 Subject: [PATCH] implementing new payment system + rewrite deploy scripts --- payments/src/controllers/WebhookController.ts | 66 +++++++++ payments/src/index.ts | 26 ++++ payments/src/routers/PaymentRouter.ts | 14 +- payments/src/routers/WebhookRouter.ts | 21 ++- payments/src/services/StripeService.ts | 4 +- scripts/consumer/deploy.ts | 15 +- scripts/payments/deploy.ts | 131 +++++++++--------- scripts/payments/shared.ts | 5 +- scripts/producer/deploy.ts | 15 +- shared_global/data/PLANS.ts | 16 +-- shared_global/schema/PremiumSchema.ts | 22 +++ shared_global/schema/UserLimitSchema.ts | 26 ++++ shared_global/schema/UserSchema.ts | 18 +-- shared_global/schema/project/ProjectSchema.ts | 10 -- 14 files changed, 265 insertions(+), 124 deletions(-) create mode 100644 payments/src/controllers/WebhookController.ts create mode 100644 shared_global/schema/PremiumSchema.ts create mode 100644 shared_global/schema/UserLimitSchema.ts diff --git a/payments/src/controllers/WebhookController.ts b/payments/src/controllers/WebhookController.ts new file mode 100644 index 0000000..a1c8eaa --- /dev/null +++ b/payments/src/controllers/WebhookController.ts @@ -0,0 +1,66 @@ + +import type Event from 'stripe'; +import StripeService from '../services/StripeService'; +import { getPlanFromPrice, PLAN_DATA } from '../shared/data/PLANS'; +import { PremiumModel } from '../shared/schema/PremiumSchema'; +import { UserLimitModel } from '../shared/schema/UserLimitSchema'; + +import { EmailService } from '../shared/services/EmailService'; + + + +async function addSubscriptionToUser(user_id: string, plan: PLAN_DATA, subscription_id: string, current_period_start: number, current_period_end: number) { + + await PremiumModel.updateOne({ _id: user_id }, { + premium_type: plan.ID, + subscription_id, + expire_at: current_period_end * 1000 + }, { upsert: true }); + + await UserLimitModel.updateOne({ _id: user_id }, { + events: 0, + visits: 0, + ai_messages: 0, + limit: plan.COUNT_LIMIT, + ai_limit: plan.AI_MESSAGE_LIMIT, + billing_start_at: current_period_start * 1000, + billing_expire_at: current_period_end * 1000, + }, { upsert: true }) + +} + + +export async function onPaymentSuccess(event: Event.InvoicePaidEvent) { + + const customer_id = event.data.object.customer; + const premiumData = await PremiumModel.findOne({ customer_id }); + if (!premiumData) return { error: 'customer not found' } + + if (event.data.object.status !== 'paid') return { received: true, warn: 'payment status not paid' } + + const subscription_id = event.data.object.subscription as string; + + const price = event.data.object.lines.data[0].price.id; + if (!price) return { error: 'price not found' } + + const plan = getPlanFromPrice(price, StripeService.testMode); + if (!plan) return { error: 'plan not found' } + + const databaseSubscription = premiumData.subscription_id; + + if (databaseSubscription != subscription_id) { + await StripeService.deleteSubscription(subscription_id); + } + + await addSubscriptionToUser(premiumData.user_id.toString(), plan, subscription_id, event.data.object.period_start, event.data.object.period_end); + + setTimeout(() => { + if (plan.ID == 0) return; + //TODO: Email service template + // const emailData = EmailService.getEmailServerInfo('purchase', { target: user.email, projectName: project.name }); + // EmailServiceHelper.sendEmail(emailData); + }, 1); + + return { ok: true }; + +} \ No newline at end of file diff --git a/payments/src/index.ts b/payments/src/index.ts index e69de29..4807bff 100644 --- a/payments/src/index.ts +++ b/payments/src/index.ts @@ -0,0 +1,26 @@ +import express from 'express'; +import StripeService from './services/StripeService' +import { webhookRouter } from './routers/WebhookRouter'; +import { paymentRouter } from './routers/PaymentRouter'; + + +const STRIPE_PRIVATE_KEY = process.env.STRIPE_PRIVATE_KEY; +const STRIPE_WEBHOOK_SECRET = process.env.STRIPE_WEBHOOK_SECRET; +const STRIPE_TESTMODE = process.env.STRIPE_TESTMODE === 'true'; + +StripeService.init(STRIPE_PRIVATE_KEY, STRIPE_WEBHOOK_SECRET, STRIPE_TESTMODE); + +console.log('Stripe started in', STRIPE_TESTMODE ? 'TESTMODE' : 'LIVEMODE'); + +const app = express(); + +app.use('/webhook', webhookRouter); +app.use('/payment', paymentRouter); + +const port = parseInt(process.env.PORT); +if (!port) { + console.error('PORT is not set'); + process.exit(); +} + +app.listen(port, () => console.log(`Listening on port ${port}`)); \ No newline at end of file diff --git a/payments/src/routers/PaymentRouter.ts b/payments/src/routers/PaymentRouter.ts index 737c28e..5ab74fd 100644 --- a/payments/src/routers/PaymentRouter.ts +++ b/payments/src/routers/PaymentRouter.ts @@ -3,13 +3,13 @@ import z from 'zod'; import { getPlanFromId } from '../shared/data/PLANS'; import StripeService from '../services/StripeService'; import { sendJson } from '../Utils'; -import { ProjectModel } from '../shared/schema/project/ProjectSchema'; +import { PremiumModel } from '../shared/schema/PremiumSchema'; export const paymentRouter = Router(); export const ZBodyCreatePayment = z.object({ - pid: z.string(), + user_id: z.string(), plan_id: z.number() }) @@ -20,17 +20,17 @@ paymentRouter.post('/create', json(), async (req, res) => { const plan = getPlanFromId(createPaymentData.plan_id); if (!plan) return sendJson(res, 400, { error: 'plan not found' }); - const project = await ProjectModel.findById(createPaymentData.pid); - if (!project) return sendJson(res, 400, { error: 'project not found' }); - if (!project.customer_id) return sendJson(res, 400, { error: 'project have no customer_id' }); + const premiumData = await PremiumModel.findById(createPaymentData.user_id); + if (!premiumData) return sendJson(res, 400, { error: 'user not found' }); + if (!premiumData.customer_id) return sendJson(res, 400, { error: 'user have no customer_id' }); const price = StripeService.testMode ? plan.PRICE_TEST : plan.PRICE; const checkout = await StripeService.createPayment( price, 'https://dashboard.litlyx.com/payment_ok', - createPaymentData.pid, - project.customer_id + createPaymentData.user_id, + premiumData.customer_id ); if (!checkout) return sendJson(res, 400, { error: 'cannot create payment' }); diff --git a/payments/src/routers/WebhookRouter.ts b/payments/src/routers/WebhookRouter.ts index cad316f..8eac8c1 100644 --- a/payments/src/routers/WebhookRouter.ts +++ b/payments/src/routers/WebhookRouter.ts @@ -1,5 +1,9 @@ import { json, Router } from 'express'; +import { sendJson } from '../Utils'; +import StripeService from '../services/StripeService'; + +import * as WebhookController from '../controllers/WebhookController' export const webhookRouter = Router(); @@ -8,10 +12,21 @@ webhookRouter.get('/', json(), async (req, res) => { try { const signature = req.header('stripe-signature'); - if (!signature) { - console.error('No signature on the webhook') - } + if (!signature) return sendJson(res, 400, { error: 'No signature' }); + const eventData = StripeService.parseWebhook(req.body, signature); + if (!eventData) return sendJson(res, 400, { error: 'Error parsing event data' }); + + if (eventData.type === 'invoice.paid') { + const response = await WebhookController.onPaymentSuccess(eventData); + return sendJson(res, 200, response); + } + + // if (eventData.type === 'payment_intent.succeeded') return await onPaymentOnetimeSuccess(eventData); + // if (eventData.type === 'invoice.payment_failed') return await onPaymentFailed(eventData); + // if (eventData.type === 'customer.subscription.deleted') return await onSubscriptionDeleted(eventData); + // if (eventData.type === 'customer.subscription.created') return await onSubscriptionCreated(eventData); + // if (eventData.type === 'customer.subscription.updated') return await onSubscriptionUpdated(eventData); } catch (ex) { res.status(500).json({ error: ex.message }); diff --git a/payments/src/services/StripeService.ts b/payments/src/services/StripeService.ts index 5cd22d8..642f2b6 100644 --- a/payments/src/services/StripeService.ts +++ b/payments/src/services/StripeService.ts @@ -51,7 +51,7 @@ class StripeService { return checkout; } - async createPayment(price: string, success_url: string, pid: string, customer: string) { + async createPayment(price: string, success_url: string, user_id: string, customer: string) { if (!this.stripe) throw Error('Stripe not initialized'); const checkout = await this.stripe.checkout.sessions.create({ @@ -61,7 +61,7 @@ class StripeService { { price, quantity: 1 } ], subscription_data: { - metadata: { pid }, + metadata: { user_id }, }, customer, success_url, diff --git a/scripts/consumer/deploy.ts b/scripts/consumer/deploy.ts index 6faa3ed..06c17de 100644 --- a/scripts/consumer/deploy.ts +++ b/scripts/consumer/deploy.ts @@ -4,7 +4,7 @@ import path from 'path'; import child from 'child_process'; import { createZip } from '../helpers/zip-helper'; import { DeployHelper } from '../helpers/deploy-helper'; -import { DATABASE_CONNECTION_STRING_PRODUCTION, DATABASE_CONNECTION_STRING_TESTMODE, REMOTE_HOST_TESTMODE } from '../.config'; +import { DATABASE_CONNECTION_STRING_PRODUCTION, DATABASE_CONNECTION_STRING_TESTMODE, REDIS_URL_PRODUCTION, REDIS_URL_TESTMODE, REMOTE_HOST_TESTMODE } from '../.config'; const TMP_PATH = path.join(__dirname, '../../tmp'); const LOCAL_PATH = path.join(__dirname, '../../consumer'); @@ -30,19 +30,22 @@ async function main() { } - console.log('Creting zip file'); + console.log('Creating zip file'); const archive = createZip(TMP_PATH + '/' + ZIP_NAME); archive.directory(LOCAL_PATH + '/dist', '/dist'); if (MODE === 'testmode') { const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8'); - const REDIS_URL = ecosystemContent.match(/REDIS_URL: ["'](.*?)["']/)[1]; const devContent = ecosystemContent - .replace(REDIS_URL, `redis://${REMOTE_HOST_TESTMODE}`) - .replace(DATABASE_CONNECTION_STRING_PRODUCTION, `redis://${DATABASE_CONNECTION_STRING_TESTMODE}`); + .replace("$REDIS_URL$", `${REDIS_URL_TESTMODE}`) + .replace("$MONGO_CONNECTION_STRING$", `${DATABASE_CONNECTION_STRING_TESTMODE}`); archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' }); } else { - archive.file(LOCAL_PATH + '/ecosystem.config.js', { name: '/ecosystem.config.js' }) + const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8'); + const devContent = ecosystemContent + .replace("$REDIS_URL$", `${REDIS_URL_PRODUCTION}`) + .replace("$MONGO_CONNECTION_STRING$", `${DATABASE_CONNECTION_STRING_PRODUCTION}`); + archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' }); } diff --git a/scripts/payments/deploy.ts b/scripts/payments/deploy.ts index 012307c..72ba76a 100644 --- a/scripts/payments/deploy.ts +++ b/scripts/payments/deploy.ts @@ -1,87 +1,94 @@ -// import fs from 'fs-extra'; -// import path from 'path'; -// import child from 'child_process'; -// import { createZip } from '../helpers/zip-helper'; -// import { DeployHelper } from '../helpers/deploy-helper'; -// import { DATABASE_CONNECTION_STRING_PRODUCTION, DATABASE_CONNECTION_STRING_TESTMODE, REMOTE_HOST_TESTMODE } from '../.config'; +import fs from 'fs-extra'; +import path from 'path'; +import child from 'child_process'; +import { createZip } from '../helpers/zip-helper'; +import { DeployHelper } from '../helpers/deploy-helper'; +import { DATABASE_CONNECTION_STRING_PRODUCTION, DATABASE_CONNECTION_STRING_TESTMODE, REMOTE_HOST_TESTMODE, STRIPE_PRIVATE_KEY_PRODUCTION, STRIPE_PRIVATE_KEY_TESTMODE, STRIPE_WEBHOOK_SECRET_PRODUCTION, STRIPE_WEBHOOK_SECRET_TESTMODE } from '../.config'; -// const TMP_PATH = path.join(__dirname, '../../tmp'); -// const LOCAL_PATH = path.join(__dirname, '../../consumer'); -// const REMOTE_PATH = '/home/litlyx/consumer'; -// const ZIP_NAME = 'consumer.zip'; +const TMP_PATH = path.join(__dirname, '../../tmp'); +const LOCAL_PATH = path.join(__dirname, '../../payments'); +const REMOTE_PATH = '/home/litlyx/payments'; +const ZIP_NAME = 'payments.zip'; -// const MODE = DeployHelper.getMode(); -// const SKIP_BUILD = DeployHelper.getArgAt(0) == '--no-build'; +const MODE = DeployHelper.getMode(); +const SKIP_BUILD = DeployHelper.getArgAt(0) == '--no-build'; -// console.log('Deploying consumer in mode:', MODE); +console.log('Deploying payments in mode:', MODE); -// setTimeout(() => { main(); }, 3000); +setTimeout(() => { main(); }, 3000); -// async function main() { +async function main() { -// if (fs.existsSync(TMP_PATH)) fs.rmSync(TMP_PATH, { force: true, recursive: true }); -// fs.ensureDirSync(TMP_PATH); + if (fs.existsSync(TMP_PATH)) fs.rmSync(TMP_PATH, { force: true, recursive: true }); + fs.ensureDirSync(TMP_PATH); -// if (!SKIP_BUILD) { -// console.log('Building'); -// child.execSync(`cd ${LOCAL_PATH} && pnpm run build`); -// } + if (!SKIP_BUILD) { + console.log('Building'); + child.execSync(`cd ${LOCAL_PATH} && pnpm run build`); + } -// console.log('Creting zip file'); -// const archive = createZip(TMP_PATH + '/' + ZIP_NAME); -// archive.directory(LOCAL_PATH + '/dist', '/dist'); + console.log('Creating zip file'); + const archive = createZip(TMP_PATH + '/' + ZIP_NAME); + archive.directory(LOCAL_PATH + '/dist', '/dist'); -// if (MODE === 'testmode') { -// const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8'); -// const REDIS_URL = ecosystemContent.match(/REDIS_URL: ["'](.*?)["']/)[1]; -// const devContent = ecosystemContent -// .replace(REDIS_URL, `redis://${REMOTE_HOST_TESTMODE}`) -// .replace(DATABASE_CONNECTION_STRING_PRODUCTION, `redis://${DATABASE_CONNECTION_STRING_TESTMODE}`); -// archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' }); -// } else { -// archive.file(LOCAL_PATH + '/ecosystem.config.js', { name: '/ecosystem.config.js' }) -// } + if (MODE === 'testmode') { + const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8'); + const devContent = ecosystemContent + .replace("$MONGO_CONNECTION_STRING$", `${DATABASE_CONNECTION_STRING_TESTMODE}`) + .replace("$STRIPE_PRIVATE_KEY$", `${STRIPE_PRIVATE_KEY_TESTMODE}`) + .replace("$STRIPE_WEBHOOK_SECRET$", `${STRIPE_WEBHOOK_SECRET_TESTMODE}`) + .replace("$STRIPE_TESTMODE$", `true`); + archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' }); + } else { + const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8'); + const devContent = ecosystemContent + .replace(DATABASE_CONNECTION_STRING_PRODUCTION, `${DATABASE_CONNECTION_STRING_PRODUCTION}`) + .replace("$STRIPE_PRIVATE_KEY$", `${STRIPE_PRIVATE_KEY_PRODUCTION}`) + .replace("$STRIPE_WEBHOOK_SECRET$", `${STRIPE_WEBHOOK_SECRET_PRODUCTION}`) + .replace("$STRIPE_TESTMODE$", `false`); + archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' }); + } -// archive.file(LOCAL_PATH + '/package.json', { name: '/package.json' }); -// archive.file(LOCAL_PATH + '/pnpm-lock.yaml', { name: '/pnpm-lock.yaml' }); -// await archive.finalize(); + archive.file(LOCAL_PATH + '/package.json', { name: '/package.json' }); + archive.file(LOCAL_PATH + '/pnpm-lock.yaml', { name: '/pnpm-lock.yaml' }); + await archive.finalize(); -// await DeployHelper.connect(); + await DeployHelper.connect(); -// const { scp, ssh } = DeployHelper.instances(); + const { scp, ssh } = DeployHelper.instances(); -// console.log('Creating remote structure'); -// console.log('Check existing'); -// const remoteExist = await scp.exists(REMOTE_PATH); -// console.log('Exist', remoteExist); -// if (remoteExist) { -// console.log('Deleting'); -// await DeployHelper.execute(`rm -r ${REMOTE_PATH}`); -// } + console.log('Creating remote structure'); + console.log('Check existing'); + const remoteExist = await scp.exists(REMOTE_PATH); + console.log('Exist', remoteExist); + if (remoteExist) { + console.log('Deleting'); + await DeployHelper.execute(`rm -r ${REMOTE_PATH}`); + } -// console.log('Creating folder'); -// await scp.mkdir(REMOTE_PATH); + console.log('Creating folder'); + await scp.mkdir(REMOTE_PATH); -// console.log('Uploading zip file'); -// await scp.uploadFile(TMP_PATH + '/' + ZIP_NAME, REMOTE_PATH + '/' + ZIP_NAME); -// scp.close(); + console.log('Uploading zip file'); + await scp.uploadFile(TMP_PATH + '/' + ZIP_NAME, REMOTE_PATH + '/' + ZIP_NAME); + scp.close(); -// console.log('Cleaning local'); -// fs.rmSync(TMP_PATH + '/' + ZIP_NAME, { force: true, recursive: true }); + console.log('Cleaning local'); + fs.rmSync(TMP_PATH + '/' + ZIP_NAME, { force: true, recursive: true }); -// console.log('Extracting remote'); -// await DeployHelper.execute(`cd ${REMOTE_PATH} && unzip ${ZIP_NAME} && rm -r ${ZIP_NAME}`); + console.log('Extracting remote'); + await DeployHelper.execute(`cd ${REMOTE_PATH} && unzip ${ZIP_NAME} && rm -r ${ZIP_NAME}`); -// console.log('Installing remote'); -// await DeployHelper.execute(`cd ${REMOTE_PATH} && /root/.nvm/versions/node/v21.2.0/bin/pnpm i`); + console.log('Installing remote'); + await DeployHelper.execute(`cd ${REMOTE_PATH} && /root/.nvm/versions/node/v21.2.0/bin/pnpm i`); -// console.log('Executing remote'); -// await DeployHelper.execute(`cd ${REMOTE_PATH} && /root/.nvm/versions/node/v21.2.0/bin/pm2 start ecosystem.config.js`); + console.log('Executing remote'); + await DeployHelper.execute(`cd ${REMOTE_PATH} && /root/.nvm/versions/node/v21.2.0/bin/pm2 start ecosystem.config.js`); -// ssh.dispose(); + ssh.dispose(); -// } +} diff --git a/scripts/payments/shared.ts b/scripts/payments/shared.ts index ebdd3ab..26ce3c8 100644 --- a/scripts/payments/shared.ts +++ b/scripts/payments/shared.ts @@ -15,9 +15,8 @@ helper.copy('services/EmailService.ts'); helper.create('schema'); helper.copy('schema/UserSchema.ts'); - -helper.create('schema/project'); -helper.copy('schema/project/ProjectSchema.ts'); +helper.copy('schema/PremiumSchema.ts'); +helper.copy('schema/UserLimitSchema.ts'); helper.create('data'); helper.copy('data/PLANS.ts'); \ No newline at end of file diff --git a/scripts/producer/deploy.ts b/scripts/producer/deploy.ts index 4f4a496..25a301b 100644 --- a/scripts/producer/deploy.ts +++ b/scripts/producer/deploy.ts @@ -4,7 +4,7 @@ import path from 'path'; import child from 'child_process'; import { createZip } from '../helpers/zip-helper'; import { DeployHelper } from '../helpers/deploy-helper'; -import { DATABASE_CONNECTION_STRING_PRODUCTION, DATABASE_CONNECTION_STRING_TESTMODE, REMOTE_HOST_TESTMODE } from '../.config'; +import { DATABASE_CONNECTION_STRING_PRODUCTION, DATABASE_CONNECTION_STRING_TESTMODE, REDIS_URL_PRODUCTION, REDIS_URL_TESTMODE, REMOTE_HOST_TESTMODE } from '../.config'; const TMP_PATH = path.join(__dirname, '../../tmp'); const LOCAL_PATH = path.join(__dirname, '../../producer'); @@ -30,19 +30,22 @@ async function main() { } - console.log('Creting zip file'); + console.log('Creating zip file'); const archive = createZip(TMP_PATH + '/' + ZIP_NAME); archive.directory(LOCAL_PATH + '/dist', '/dist'); if (MODE === 'testmode') { const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8'); - const REDIS_URL = ecosystemContent.match(/REDIS_URL: ["'](.*?)["']/)[1]; const devContent = ecosystemContent - .replace(REDIS_URL, `redis://${REMOTE_HOST_TESTMODE}`) - .replace(DATABASE_CONNECTION_STRING_PRODUCTION, `redis://${DATABASE_CONNECTION_STRING_TESTMODE}`); + .replace("$REDIS_URL$", `${REDIS_URL_TESTMODE}`) + .replace("$MONGO_CONNECTION_STRING$", `${DATABASE_CONNECTION_STRING_TESTMODE}`); archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' }); } else { - archive.file(LOCAL_PATH + '/ecosystem.config.js', { name: '/ecosystem.config.js' }) + const ecosystemContent = fs.readFileSync(LOCAL_PATH + '/ecosystem.config.js', 'utf8'); + const devContent = ecosystemContent + .replace("$REDIS_URL$", `${REDIS_URL_PRODUCTION}`) + .replace("$MONGO_CONNECTION_STRING$", `${DATABASE_CONNECTION_STRING_PRODUCTION}`); + archive.append(Buffer.from(devContent), { name: '/ecosystem.config.js' }); } diff --git a/shared_global/data/PLANS.ts b/shared_global/data/PLANS.ts index eacdbf6..02f15f1 100644 --- a/shared_global/data/PLANS.ts +++ b/shared_global/data/PLANS.ts @@ -1,6 +1,6 @@ -export type PREMIUM_TAG = typeof PREMIUM_TAGS[number]; +export type PLAN_TAG = typeof PLAN_TAGS[number]; -export const PREMIUM_TAGS = [ +export const PLAN_TAGS = [ 'FREE', 'PLAN_1', 'PLAN_2', @@ -20,17 +20,17 @@ export const PREMIUM_TAGS = [ ] as const; -export type PREMIUM_DATA = { +export type PLAN_DATA = { COUNT_LIMIT: number, AI_MESSAGE_LIMIT: number, PRICE: string, PRICE_TEST: string, ID: number, COST: number, - TAG: PREMIUM_TAG + TAG: PLAN_TAG } -export const PREMIUM_PLAN: Record = { +export const PREMIUM_PLAN: Record = { FREE: { ID: 0, COUNT_LIMIT: 5_000, @@ -177,19 +177,19 @@ export const PREMIUM_PLAN: Record = { } } -export function getPlanFromTag(tag: PREMIUM_TAG) { +export function getPlanFromTag(tag: PLAN_TAG) { return PREMIUM_PLAN[tag]; } export function getPlanFromId(id: number) { - for (const tag of PREMIUM_TAGS) { + for (const tag of PLAN_TAGS) { const plan = getPlanFromTag(tag); if (plan.ID === id) return plan; } } export function getPlanFromPrice(price: string, testMode: boolean) { - for (const tag of PREMIUM_TAGS) { + for (const tag of PLAN_TAGS) { const plan = getPlanFromTag(tag); if (testMode) { if (plan.PRICE_TEST === price) return plan; diff --git a/shared_global/schema/PremiumSchema.ts b/shared_global/schema/PremiumSchema.ts new file mode 100644 index 0000000..707fa16 --- /dev/null +++ b/shared_global/schema/PremiumSchema.ts @@ -0,0 +1,22 @@ +import { model, Schema, Types } from 'mongoose'; + +export type TPremium = { + user_id: Schema.Types.ObjectId, + premium_type: number, + customer_id: string, + subscription_id: string, + expire_at: number, + created_at: Date, +} + +const PremiumSchema = new Schema({ + user_id: { type: Types.ObjectId, unique: true, index: 1 }, + customer_id: { type: String }, + premium_type: { type: Number }, + subscription_id: { type: String }, + expire_at: { type: Number }, + created_at: { type: Date, default: () => Date.now() } +}) + +export const PremiumModel = model('premiums', PremiumSchema); + diff --git a/shared_global/schema/UserLimitSchema.ts b/shared_global/schema/UserLimitSchema.ts new file mode 100644 index 0000000..d35052e --- /dev/null +++ b/shared_global/schema/UserLimitSchema.ts @@ -0,0 +1,26 @@ +import { model, Schema, Types } from 'mongoose'; + +export type TUserLimit = { + _id: Schema.Types.ObjectId, + user_id: Schema.Types.ObjectId, + events: number, + visits: number, + ai_messages: number, + limit: number, + ai_limit: number, + billing_expire_at: Date, + billing_start_at: Date, +} + +const UserLimitSchema = new Schema({ + user_id: { type: Types.ObjectId, index: true, unique: true }, + events: { type: Number, required: true, default: 0 }, + visits: { type: Number, required: true, default: 0 }, + ai_messages: { type: Number, required: true, default: 0 }, + limit: { type: Number, required: true }, + ai_limit: { type: Number, required: true }, + billing_start_at: { type: Date, required: true }, + billing_expire_at: { type: Date, required: true }, +}); + +export const UserLimitModel = model('user_limits', UserLimitSchema); \ No newline at end of file diff --git a/shared_global/schema/UserSchema.ts b/shared_global/schema/UserSchema.ts index d312393..5fc4021 100644 --- a/shared_global/schema/UserSchema.ts +++ b/shared_global/schema/UserSchema.ts @@ -7,14 +7,6 @@ export type TUser = { locale: string, picture: string, created_at: Date, - google_tokens?: { - refresh_token?: string; - expiry_date?: number; - access_token?: string; - token_type?: string; - id_token?: string; - scope?: string; - } } const UserSchema = new Schema({ @@ -22,15 +14,7 @@ const UserSchema = new Schema({ name: String, given_name: String, locale: String, - picture: String, - google_tokens: { - refresh_token: String, - expiry_date: Number, - access_token: String, - token_type: String, - id_token: String, - scope: String - }, + picture: String, created_at: { type: Date, default: () => Date.now() } }) diff --git a/shared_global/schema/project/ProjectSchema.ts b/shared_global/schema/project/ProjectSchema.ts index 5470139..b711c29 100644 --- a/shared_global/schema/project/ProjectSchema.ts +++ b/shared_global/schema/project/ProjectSchema.ts @@ -4,22 +4,12 @@ export type TProject = { _id: Schema.Types.ObjectId, owner: Schema.Types.ObjectId, name: string, - premium: boolean, - premium_type: number, - customer_id: string, - subscription_id: string, - premium_expire_at: Date, created_at: Date } const ProjectSchema = new Schema({ owner: { type: Types.ObjectId, index: 1 }, name: { type: String, required: true }, - premium: { type: Boolean, default: false }, - premium_type: { type: Number, default: 0 }, - customer_id: { type: String, required: true }, - subscription_id: { type: String, required: true }, - premium_expire_at: { type: Date, required: true }, created_at: { type: Date, default: () => Date.now() }, })