From a9bbc58ad147c819e21e4d42d00a16193f497567 Mon Sep 17 00:00:00 2001 From: Emily Date: Tue, 22 Apr 2025 18:42:18 +0200 Subject: [PATCH] fix payment service + appsumo + ui --- dashboard/components/dashboard/TopCards.vue | 19 ++++++---- .../components/layout/VerticalNavigation.vue | 37 ++++++++++++------- dashboard/components/settings/Codes.vue | 2 +- dashboard/pages/settings.vue | 7 ++-- .../api/pay/redeem_appsumo_code.post.ts | 25 +++++++------ .../server/api/user/delete_account.delete.ts | 28 +++++++++----- .../server/services/PaymentServiceHelper.ts | 4 ++ payments/src/index.ts | 4 +- payments/src/routers/PaymentRouter.ts | 26 +++++++++++++ payments/src/services/StripeService.ts | 25 ++++++------- producer/src/controller.ts | 2 + producer/src/index.ts | 1 + shared_global/data/PLANS.ts | 6 +-- 13 files changed, 121 insertions(+), 65 deletions(-) diff --git a/dashboard/components/dashboard/TopCards.vue b/dashboard/components/dashboard/TopCards.vue index e073502..e319db4 100644 --- a/dashboard/components/dashboard/TopCards.vue +++ b/dashboard/components/dashboard/TopCards.vue @@ -70,27 +70,27 @@ const avgBouncingRate = computed(() => { function weightedAverage(data: number[]): number { if (data.length === 0) return 0; - + // Compute median const sortedData = [...data].sort((a, b) => a - b); const middle = Math.floor(sortedData.length / 2); - const median = sortedData.length % 2 === 0 - ? (sortedData[middle - 1] + sortedData[middle]) / 2 + const median = sortedData.length % 2 === 0 + ? (sortedData[middle - 1] + sortedData[middle]) / 2 : sortedData[middle]; - + // Define a threshold (e.g., 3 times the median) to filter out extreme values const threshold = median * 3; const filteredData = data.filter(num => num <= threshold); - + if (filteredData.length === 0) return median; // Fallback to median if all are removed - + // Compute weights based on inverse absolute deviation from median const weights = filteredData.map(num => 1 / (1 + Math.abs(num - median))); - + // Compute weighted sum and sum of weights const weightedSum = filteredData.reduce((sum, num, i) => sum + num * weights[i], 0); const sumOfWeights = weights.reduce((sum, weight) => sum + weight, 0); - + return weightedSum / sumOfWeights; } const avgSessionDuration = computed(() => { @@ -109,6 +109,9 @@ const avgSessionDuration = computed(() => { seconds += avg * 60; while (seconds >= 60) { seconds -= 60; minutes += 1; } while (minutes >= 60) { minutes -= 60; hours += 1; } + + + if (hours == 0 && minutes == 0 && seconds < 10) return `0m ~10s` return `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds.toFixed()}s` }); diff --git a/dashboard/components/layout/VerticalNavigation.vue b/dashboard/components/layout/VerticalNavigation.vue index 2307862..cda38f5 100644 --- a/dashboard/components/layout/VerticalNavigation.vue +++ b/dashboard/components/layout/VerticalNavigation.vue @@ -32,7 +32,7 @@ const { data: pendingInvites, refresh: refreshInvites } = useFetch('/api/project headers: useComputedHeaders({}) }); -const { userRoles } = useLoggedUser(); +const { userRoles, isPremium } = useLoggedUser(); const { projectList } = useProject(); const debugMode = process.dev; @@ -94,6 +94,9 @@ async function generatePDF() { const { actions } = useProject(); +const { showDrawer } = useDrawer(); + + const modal = useModal(); @@ -138,13 +141,6 @@ function openPendingInvites() { }">
- - -
@@ -304,6 +300,16 @@ function openPendingInvites() {
+ +
+
Upgrade to premium
+ + Upgrade + +
+
+
@@ -316,13 +322,18 @@ function openPendingInvites() {
- - -
+ + +
+
+ +
Premium
+
+
- -
+ diff --git a/dashboard/components/settings/Codes.vue b/dashboard/components/settings/Codes.vue index 463b0c8..1cba5ee 100644 --- a/dashboard/components/settings/Codes.vue +++ b/dashboard/components/settings/Codes.vue @@ -54,7 +54,7 @@ async function redeemCode() { Redeemed codes: {{ valid_codes.data.value?.count || '0' }}
- *Plan upgrades are applicable exclusively to this project(workspace). + *Plan upgrades are applied to account level.
diff --git a/dashboard/pages/settings.vue b/dashboard/pages/settings.vue index bf51344..e650ee4 100644 --- a/dashboard/pages/settings.vue +++ b/dashboard/pages/settings.vue @@ -6,7 +6,8 @@ const selfhosted = useSelfhosted(); const items = [ { label: 'General', slot: 'general', tab: 'general' }, - { label: 'Domains', slot: 'domains', tab: 'domains' } + { label: 'Domains', slot: 'domains', tab: 'domains' }, + { label: 'Codes', slot: 'codes', tab: 'codes' }, ] @@ -30,7 +31,7 @@ const items = [ Billing disabled in self-host mode
--> - +
diff --git a/dashboard/server/api/pay/redeem_appsumo_code.post.ts b/dashboard/server/api/pay/redeem_appsumo_code.post.ts index 86cdfc6..adb4c7b 100644 --- a/dashboard/server/api/pay/redeem_appsumo_code.post.ts +++ b/dashboard/server/api/pay/redeem_appsumo_code.post.ts @@ -1,17 +1,13 @@ import { getPlanFromId, PREMIUM_PLAN } from "@data/PREMIUM"; import { canTryAppsumoCode, checkAppsumoCode, useAppsumoCode, useTryAppsumoCode } from "~/server/services/AppsumoService"; +import { PaymentServiceHelper } from "~/server/services/PaymentServiceHelper"; +import { PremiumModel } from "~/shared/schema/PremiumSchema"; function getPlanToActivate(current_plan_id: number) { if (current_plan_id === PREMIUM_PLAN.FREE.ID) { return PREMIUM_PLAN.APPSUMO_INCUBATION; } - // if (current_plan_id === PREMIUM_PLAN.INCUBATION.ID) { - // return PREMIUM_PLAN.APPSUMO_ACCELERATION; - // } - // if (current_plan_id === PREMIUM_PLAN.ACCELERATION.ID) { - // return PREMIUM_PLAN.APPSUMO_GROWTH; - // } if (current_plan_id === PREMIUM_PLAN.APPSUMO_INCUBATION.ID) { return PREMIUM_PLAN.APPSUMO_ACCELERATION; } @@ -38,13 +34,18 @@ export default defineEventHandler(async event => { const valid = await checkAppsumoCode(code); if (!valid) return setResponseStatus(event, 400, 'Code not valid'); - // const currentPlan = getPlanFromId(project.premium_type); - // if (!currentPlan) return setResponseStatus(event, 400, 'Current plan not found'); - // const planToActivate = getPlanToActivate(currentPlan.ID); - // if (!planToActivate) return setResponseStatus(event, 400, 'Cannot use code on current plan'); + const currentPremiumData = await PremiumModel.findOne({ user_id: user.id }); + if (!currentPremiumData) return setResponseStatus(event, 400, 'Error finding user'); - // await StripeService.createSubscription(project.customer_id, planToActivate.ID); + const currentPlan = getPlanFromId(currentPremiumData.premium_type); + if (!currentPlan) return setResponseStatus(event, 400, 'Current plan not found'); - // await useAppsumoCode(pid, code); + const planToActivate = getPlanToActivate(currentPlan.ID); + if (!planToActivate) return setResponseStatus(event, 400, 'Cannot use code on current plan'); + + const sub = await PaymentServiceHelper.create_subscription(user.id, planToActivate.TAG); + console.log(sub); + + await useAppsumoCode(pid, code); }); \ No newline at end of file diff --git a/dashboard/server/api/user/delete_account.delete.ts b/dashboard/server/api/user/delete_account.delete.ts index f58333d..c94f1ca 100644 --- a/dashboard/server/api/user/delete_account.delete.ts +++ b/dashboard/server/api/user/delete_account.delete.ts @@ -15,6 +15,8 @@ import { TeamMemberModel } from "~/shared/schema/TeamMemberSchema"; import { PasswordModel } from "~/shared/schema/PasswordSchema"; import { PremiumModel } from "~/shared/schema/PremiumSchema"; import { PaymentServiceHelper } from "~/server/services/PaymentServiceHelper"; +import { VisitModel } from "~/shared/schema/metrics/VisitSchema"; +import { EventModel } from "~/shared/schema/metrics/EventSchema"; export default defineEventHandler(async event => { @@ -24,9 +26,8 @@ export default defineEventHandler(async event => { const projects = await ProjectModel.find({ owner: userData.id }); const premium = await PremiumModel.findOne({ user_id: userData.id }); - if (!premium) return; - if (premium.premium_type > 0) return setResponseStatus(event, 400, 'Cannot delete an account with a premium project'); + if (premium && premium.premium_type > 0) return setResponseStatus(event, 400, 'Cannot delete an account with a premium project'); const membersDeletation = await TeamMemberModel.deleteMany({ user_id: userData.id }); const membersEmailDeletation = await TeamMemberModel.deleteMany({ email: userData.user.email }); @@ -36,24 +37,31 @@ export default defineEventHandler(async event => { const limitdeletation = await UserLimitModel.deleteMany({ user_id: userData.id }); const notifiesDeletation = await LimitNotifyModel.deleteMany({ user_id: userData.id }); - await PaymentServiceHelper.delete_customer(premium.customer_id); + if (premium) PaymentServiceHelper.delete_customer(premium.customer_id); + for (const project of projects) { const project_id = project._id; + const projectDeletation = await ProjectModel.deleteOne({ _id: project_id }); const userSettingsDeletation = await UserSettingsModel.deleteOne({ project_id }); const countDeletation = await ProjectCountModel.deleteMany({ project_id }); - const sessionsDeletation = await SessionModel.deleteMany({ project_id }); - const aiChatsDeletation = await AiChatModel.deleteMany({ project_id }); + + const sessionsDeletation = SessionModel.deleteMany({ project_id }); + const visitsDeletation = VisitModel.deleteMany({ project_id }); + const eventsDeletation = EventModel.deleteMany({ project_id }); + + const aiChatsDeletation = AiChatModel.deleteMany({ project_id }); //Shields - const addressBlacklistDeletation = await AddressBlacklistModel.deleteMany({ project_id }); - const botTrafficOptionsDeletation = await BotTrafficOptionModel.deleteMany({ project_id }); - const countryBlacklistDeletation = await CountryBlacklistModel.deleteMany({ project_id }); - const domainWhitelistDeletation = await DomainWhitelistModel.deleteMany({ project_id }); - + const addressBlacklistDeletation = AddressBlacklistModel.deleteMany({ project_id }); + const botTrafficOptionsDeletation = BotTrafficOptionModel.deleteMany({ project_id }); + const countryBlacklistDeletation = CountryBlacklistModel.deleteMany({ project_id }); + const domainWhitelistDeletation = DomainWhitelistModel.deleteMany({ project_id }); + } + const premiumDeletation = await PremiumModel.deleteOne({ user_id: userData.id }); const userDeletation = await UserModel.deleteOne({ _id: userData.id }); return { ok: true }; diff --git a/dashboard/server/services/PaymentServiceHelper.ts b/dashboard/server/services/PaymentServiceHelper.ts index c554bf5..5067db2 100644 --- a/dashboard/server/services/PaymentServiceHelper.ts +++ b/dashboard/server/services/PaymentServiceHelper.ts @@ -31,6 +31,10 @@ export class PaymentServiceHelper { return await this.send('/create_customer', { user_id }); } + static async create_subscription(user_id: string, plan_tag: string): PaymentServiceResponse<{ ok: true }> { + return await this.send('/create_subscription', { user_id, plan_tag }); + } + static async create_payment(user_id: string, plan_id: number): PaymentServiceResponse<{ url: string }> { return await this.send('/create_payment', { user_id, plan_id }); } diff --git a/payments/src/index.ts b/payments/src/index.ts index 7067c01..7b5ad17 100644 --- a/payments/src/index.ts +++ b/payments/src/index.ts @@ -24,6 +24,8 @@ if (!TOKEN || TOKEN.length == 0) { process.exit(); } +app.use('/webhook', webhookRouter); + app.use((req, res, next) => { const token = req.header('x-litlyx-token'); if (token != TOKEN) { @@ -34,8 +36,6 @@ app.use((req, res, next) => { next(); }); - -app.use('/webhook', webhookRouter); app.use('/payment', paymentRouter); const port = parseInt(process.env.PORT); diff --git a/payments/src/routers/PaymentRouter.ts b/payments/src/routers/PaymentRouter.ts index 63bc452..ff599b6 100644 --- a/payments/src/routers/PaymentRouter.ts +++ b/payments/src/routers/PaymentRouter.ts @@ -161,4 +161,30 @@ paymentRouter.post('/delete_customer', json(), async (req, res) => { } catch (ex) { res.status(500).json({ error: ex.message }); } +}); + + +export const ZBodyCreateSubscription = z.object({ + user_id: z.string(), + plan_tag: z.string() +}); + +paymentRouter.post('/create_subscription', json(), async (req, res) => { + try { + const createSubscriptionData = ZBodyCreateSubscription.parse(req.body); + + const premiumData = await PremiumModel.findOne({ user_id: createSubscriptionData.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' }); + + await StripeService.createSubscription( + premiumData.customer_id, + createSubscriptionData.plan_tag + ); + return sendJson(res, 200, { ok: true }); + + } catch (ex) { + console.error(ex); + res.status(500).json({ error: ex.message }); + } }); \ No newline at end of file diff --git a/payments/src/services/StripeService.ts b/payments/src/services/StripeService.ts index 2ea0b00..72364c0 100644 --- a/payments/src/services/StripeService.ts +++ b/payments/src/services/StripeService.ts @@ -153,22 +153,21 @@ class StripeService { // return false; // } - // async createSubscription(customer_id: string, planId: number) { - // if (this.disabledMode) return; - // if (!this.stripe) throw Error('Stripe not initialized'); + async createSubscription(customer_id: string, planTag: string) { + if (!this.stripe) throw Error('Stripe not initialized'); - // const PLAN = getPlanFromId(planId); - // if (!PLAN) throw Error('Plan not found'); + const PLAN_DATA = getPlanFromTag(planTag as any); + if (!PLAN_DATA) throw Error('Plan not found'); - // const subscription = await this.stripe.subscriptions.create({ - // customer: customer_id, - // items: [ - // { price: this.testMode ? PLAN.PRICE_TEST : PLAN.PRICE, quantity: 1 } - // ], - // }); + const subscription = await this.stripe.subscriptions.create({ + customer: customer_id, + items: [ + { price: this.testMode ? PLAN_DATA.PRICE_TEST : PLAN_DATA.PRICE, quantity: 1 } + ], + }); - // return subscription; - // } + return subscription; + } // async createOneTimeSubscriptionDummy(customer_id: string, planId: number) { // if (this.disabledMode) return; diff --git a/producer/src/controller.ts b/producer/src/controller.ts index 0309708..c275816 100644 --- a/producer/src/controller.ts +++ b/producer/src/controller.ts @@ -610,6 +610,8 @@ function isBot(userAgent: string) { export async function isAllowedToLog(project_id: string, website: string, ip: string, userAgent: string) { + console.log({ userAgent }); + const blacklistData = await AddressBlacklistModel.find({ project_id }, { address: 1 }); for (const blacklistedData of blacklistData) { if (blacklistedData.address == ip) return false; diff --git a/producer/src/index.ts b/producer/src/index.ts index 7a1e389..529da19 100644 --- a/producer/src/index.ts +++ b/producer/src/index.ts @@ -16,6 +16,7 @@ const streamName = requireEnv('STREAM_NAME'); import DeprecatedRouter from "./deprecated"; import { isAllowedToLog } from "./controller"; import { connectDatabase } from "./shared/services/DatabaseService"; + app.use('/v1', DeprecatedRouter); app.post('/event', express.json(jsonOptions), async (req, res) => { diff --git a/shared_global/data/PLANS.ts b/shared_global/data/PLANS.ts index 02f15f1..562fc4f 100644 --- a/shared_global/data/PLANS.ts +++ b/shared_global/data/PLANS.ts @@ -144,7 +144,7 @@ export const PREMIUM_PLAN: Record = { COUNT_LIMIT: 50_000, AI_MESSAGE_LIMIT: 30, PRICE: 'price_1QIXwbB2lPUiVs9VKSsoksaU', - PRICE_TEST: '', + PRICE_TEST: 'price_1RBIUsB2lPUiVs9VojGan6WH', COST: 0, TAG: 'APPSUMO_INCUBATION' }, @@ -153,7 +153,7 @@ export const PREMIUM_PLAN: Record = { COUNT_LIMIT: 150_000, AI_MESSAGE_LIMIT: 100, PRICE: 'price_1QIXxRB2lPUiVs9VrjaVRoOl', - PRICE_TEST: '', + PRICE_TEST: 'price_1RBIV5B2lPUiVs9VKQyxvhst', COST: 0, TAG: 'APPSUMO_ACCELERATION' }, @@ -162,7 +162,7 @@ export const PREMIUM_PLAN: Record = { COUNT_LIMIT: 500_000, AI_MESSAGE_LIMIT: 3_000, PRICE: 'price_1QIXy8B2lPUiVs9VQBOUPAoE', - PRICE_TEST: '', + PRICE_TEST: 'price_1RBIVFB2lPUiVs9VsMoldAu3', COST: 0, TAG: 'APPSUMO_GROWTH' },