From 6a7143c8d4ef914a6cbe9c43363c09b349e072ea Mon Sep 17 00:00:00 2001 From: Emily Date: Wed, 12 Jun 2024 15:43:23 +0200 Subject: [PATCH] fix payments --- dashboard/components/pricing/PricingCard.vue | 9 +- .../components/pricing/PricingDrawer.vue | 5 + dashboard/pages/plans.vue | 13 +- dashboard/server/api/pay/webhook.post.ts | 187 +++++++++++------- dashboard/server/init.ts | 2 + dashboard/server/services/StripeService.ts | 6 + shared/data/PREMIUM.ts | 19 +- shared/schema/CustomPremiumPriceSchema.ts | 21 ++ 8 files changed, 173 insertions(+), 89 deletions(-) create mode 100644 shared/schema/CustomPremiumPriceSchema.ts diff --git a/dashboard/components/pricing/PricingCard.vue b/dashboard/components/pricing/PricingCard.vue index 352f45e..76296d5 100644 --- a/dashboard/components/pricing/PricingCard.vue +++ b/dashboard/components/pricing/PricingCard.vue @@ -6,7 +6,8 @@ export type PricingCardProp = { features: string[], desc: string, active: boolean, - planId: number + planId: number, + isDowngrade: boolean } const props = defineProps<{ data: PricingCardProp }>(); @@ -43,10 +44,14 @@ async function onUpgradeClick() {
Current active plan
-
Upgrade
+
+ Downgrade +
diff --git a/dashboard/components/pricing/PricingDrawer.vue b/dashboard/components/pricing/PricingDrawer.vue index 8ff0d0e..c11fc70 100644 --- a/dashboard/components/pricing/PricingDrawer.vue +++ b/dashboard/components/pricing/PricingDrawer.vue @@ -4,6 +4,8 @@ import type { PricingCardProp } from './PricingCard.vue'; const activeProject = useActiveProject(); +const props = defineProps<{ currentSub: number }>(); + const starterTierCardData = ref({ title: 'STARTER', @@ -23,6 +25,7 @@ const starterTierCardData = ref({ dedicated server we suggest to upgrade the plan to an higher one!`, active: activeProject.value?.premium === false, + isDowngrade: props.currentSub > 0, planId: 0 }); @@ -41,6 +44,7 @@ const accelerationTierCardData = ref({ ], desc: `Your project is entering a growth phase. We simplify data analysis for you. For more support, try our Expansion plan—it's worth it!`, active: activeProject.value?.premium_type === 1, + isDowngrade: props.currentSub > 1, planId: 1 }); @@ -59,6 +63,7 @@ const expansionTierCardData = ref({ ], desc: `We will support you with everything we can offer and give you the full power of our service. If you need more space and are growing, contact us for a custom offer!`, active: activeProject.value?.premium_type === 2, + isDowngrade: props.currentSub > 2, planId: 2 }); diff --git a/dashboard/pages/plans.vue b/dashboard/pages/plans.vue index 722da92..d937a56 100644 --- a/dashboard/pages/plans.vue +++ b/dashboard/pages/plans.vue @@ -63,7 +63,7 @@ function getPremiumName(type: number) {
- @@ -131,7 +131,7 @@ function getPremiumName(type: number) {
{{ prettyExpireDate }}
+ class="cursor-pointer flex items-center gap-2 text-[.9rem] text-white font-semibold bg-accent px-4 py-1 rounded-lg drop-shadow-[0_0_8px_#000000]">
Upgrade plan
@@ -164,15 +164,6 @@ function getPremiumName(type: number) { -
-
-
-
-
Upgrade plan
- -
-
diff --git a/dashboard/server/api/pay/webhook.post.ts b/dashboard/server/api/pay/webhook.post.ts index 3bbd049..afbba75 100644 --- a/dashboard/server/api/pay/webhook.post.ts +++ b/dashboard/server/api/pay/webhook.post.ts @@ -2,42 +2,11 @@ import StripeService from '~/server/services/StripeService'; import type Event from 'stripe'; import { ProjectModel } from '@schema/ProjectSchema'; -import { PREMIUM_DATA, PREMIUM_PLAN, getPlanFromPrice } from '@data/PREMIUM'; +import { PREMIUM_DATA, PREMIUM_PLAN, getPlanFromId, getPlanFromPrice, getPlanFromTag } from '@data/PREMIUM'; import { ProjectCountModel } from '@schema/ProjectsCounts'; import { ProjectLimitModel } from '@schema/ProjectsLimits'; import { UserModel } from '@schema/UserSchema'; -async function onPaymentSuccess(event: Event.InvoicePaidEvent) { - - if (event.data.object.status === 'paid') { - - const customer_id = event.data.object.customer as string; - const subscription_id = event.data.object.subscription as string; - - const project = await ProjectModel.findOne({ customer_id }); - if (!project) return { error: 'CUSTOMER NOT EXIST' } - - const subscriptionInfo = await StripeService.getSubscription(subscription_id); - - if (subscriptionInfo.status === 'active') { - - const price = subscriptionInfo.items.data[0].price.id; - if (!price) return { error: 'Price not found' } - - const PLAN = getPlanFromPrice(price, StripeService.testMode || false); - if (!PLAN) return { error: 'Plan not found' } - - await addSubscriptionToProject(project._id.toString(), PLAN, subscription_id, subscriptionInfo.current_period_start, subscriptionInfo.current_period_end) - - return { ok: true }; - } else { - return { received: true, warn: 'subscription status not active' } - } - - } - - return { received: true, warn: 'payment status not paid' } -} async function addSubscriptionToProject(project_id: string, plan: PREMIUM_DATA, subscription_id: string, current_period_start: number, current_period_end: number) { @@ -46,7 +15,7 @@ async function addSubscriptionToProject(project_id: string, plan: PREMIUM_DATA, premium: plan.ID != 0, premium_type: plan.ID, subscription_id, - premium_expire_at: current_period_end + premium_expire_at: current_period_end * 1000 }); await ProjectLimitModel.updateOne({ project_id }, { @@ -61,35 +30,108 @@ async function addSubscriptionToProject(project_id: string, plan: PREMIUM_DATA, } -async function onSubscriptionCreated(event: Event.CustomerSubscriptionCreatedEvent) { - - const project = await ProjectModel.findOne({ customer_id: event.data.object.customer }); - if (!project) return { error: 'CUSTOMER NOT EXIST' } - - const price = event.data.object.items.data[0].price.id; - if (!price) return { error: 'Price not found' } - - const PLAN = getPlanFromPrice(price, StripeService.testMode || false); - if (!PLAN) return { error: 'Plan not found' } - - if (project.subscription_id != event.data.object.id) { - try { - await StripeService.deleteSubscription(project.subscription_id); - } catch (ex) { } - } - if (event.data.object.status === 'active') { +async function onPaymentFailed(event: Event.InvoicePaymentFailedEvent) { + + //TODO: Send emails + + if (event.data.object.attempt_count > 1) { + const customer_id = event.data.object.customer as string; + const project = await ProjectModel.findOne({ customer_id }); + if (!project) return { error: 'CUSTOMER NOT EXIST' } + + const allSubscriptions = await StripeService.getAllSubscriptions(customer_id); + + for (const subscription of allSubscriptions.data) { + await StripeService.deleteSubscription(subscription.id); + } + + const freeSub = await StripeService.createFreeSubscription(customer_id); + await addSubscriptionToProject( project._id.toString(), - PLAN, - event.data.object.id, - event.data.object.current_period_start, - event.data.object.current_period_end - ); + getPlanFromTag('FREE'), + freeSub.id, + freeSub.current_period_start, + freeSub.current_period_end + ) + + return { ok: true }; } +} + +async function onPaymentSuccess(event: Event.InvoicePaidEvent) { + + const customer_id = event.data.object.customer as string; + const project = await ProjectModel.findOne({ customer_id }); + if (!project) return { error: 'CUSTOMER NOT EXIST' } + + + if (event.data.object.status === 'paid') { + + const subscription_id = event.data.object.subscription as string; + + const allSubscriptions = await StripeService.getAllSubscriptions(customer_id); + + const currentSubscription = allSubscriptions.data.find(e => e.id === subscription_id); + if (!currentSubscription) return { error: 'SUBSCRIPTION NOT EXIST' } + + if (currentSubscription.status !== 'active') return { error: 'SUBSCRIPTION NOT ACTIVE' } + + for (const subscription of allSubscriptions.data) { + if (subscription.id === subscription_id) continue; + await StripeService.deleteSubscription(subscription.id); + } + + const price = currentSubscription.items.data[0].price.id; + if (!price) return { error: 'Price not found' } + + const PLAN = getPlanFromPrice(price, StripeService.testMode || false); + if (!PLAN) return { error: 'Plan not found' } + + await addSubscriptionToProject(project._id.toString(), PLAN, subscription_id, currentSubscription.current_period_start, currentSubscription.current_period_end) + + return { ok: true }; + + + } + return { received: true, warn: 'payment status not paid' } +} + + + +async function onSubscriptionCreated(event: Event.CustomerSubscriptionCreatedEvent) { + + // const project = await ProjectModel.findOne({ customer_id: event.data.object.customer }); + // if (!project) return { error: 'CUSTOMER NOT EXIST' } + + // const price = event.data.object.items.data[0].price.id; + // if (!price) return { error: 'Price not found' } + + // const PLAN = getPlanFromPrice(price, StripeService.testMode || false); + // if (!PLAN) return { error: 'Plan not found' } + + // if (project.subscription_id != event.data.object.id) { + // try { + // await StripeService.deleteSubscription(project.subscription_id); + // } catch (ex) { } + // } + + + // if (event.data.object.status === 'active') { + // await addSubscriptionToProject( + // project._id.toString(), + // PLAN, + // event.data.object.id, + // event.data.object.current_period_start, + // event.data.object.current_period_end + // ); + // } + + return { ok: true } } @@ -123,27 +165,27 @@ async function onSubscriptionDeleted(event: Event.CustomerSubscriptionDeletedEve async function onSubscriptionUpdated(event: Event.CustomerSubscriptionUpdatedEvent) { - const project = await ProjectModel.findOne({ - customer_id: event.data.object.customer, - }); + // const project = await ProjectModel.findOne({ + // customer_id: event.data.object.customer, + // }); - if (!project) return { error: 'Project not found' } + // if (!project) return { error: 'Project not found' } - const price = event.data.object.items.data[0].price.id; - if (!price) return { error: 'Price not found' } + // const price = event.data.object.items.data[0].price.id; + // if (!price) return { error: 'Price not found' } - const PLAN = getPlanFromPrice(price, StripeService.testMode || false); - if (!PLAN) return { error: 'Plan not found' } + // const PLAN = getPlanFromPrice(price, StripeService.testMode || false); + // if (!PLAN) return { error: 'Plan not found' } - if (event.data.object.status === 'active') { - await addSubscriptionToProject( - project._id.toString(), - PLAN, - event.data.object.id, - event.data.object.current_period_start, - event.data.object.current_period_end - ); - } + // if (event.data.object.status === 'active') { + // await addSubscriptionToProject( + // project._id.toString(), + // PLAN, + // event.data.object.id, + // event.data.object.current_period_start, + // event.data.object.current_period_end + // ); + // } return { ok: true } } @@ -159,6 +201,7 @@ export default defineEventHandler(async event => { const eventData = StripeService.parseWebhook(body, signature); if (!eventData) return; if (eventData.type === 'invoice.paid') return await onPaymentSuccess(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); diff --git a/dashboard/server/init.ts b/dashboard/server/init.ts index 926a676..922af49 100644 --- a/dashboard/server/init.ts +++ b/dashboard/server/init.ts @@ -17,8 +17,10 @@ export default async () => { config.EMAIL_PASS, ); + StripeService.init(config.STRIPE_SECRET, config.STRIPE_WH_SECRET, false); + if (!connection || connection.connection.readyState == mongoose.ConnectionStates.disconnected) { console.log('[DATABASE] Connecting'); connection = await mongoose.connect(config.MONGO_CONNECTION_STRING); diff --git a/dashboard/server/services/StripeService.ts b/dashboard/server/services/StripeService.ts index ee2b630..32d6382 100644 --- a/dashboard/server/services/StripeService.ts +++ b/dashboard/server/services/StripeService.ts @@ -54,6 +54,12 @@ class StripeService { return subscription; } + async getAllSubscriptions(customer_id: string) { + if (!this.stripe) throw Error('Stripe not initialized'); + const subscriptions = await this.stripe.subscriptions.list({customer: customer_id}); + return subscriptions; + } + async getInvoices(customer_id: string) { const invoices = await this.stripe?.invoices.list({ customer: customer_id }); return invoices; diff --git a/shared/data/PREMIUM.ts b/shared/data/PREMIUM.ts index f902337..d16dbc8 100644 --- a/shared/data/PREMIUM.ts +++ b/shared/data/PREMIUM.ts @@ -1,9 +1,8 @@ +import { CustomPremiumPriceModel } from "../schema/CustomPremiumPriceSchema"; export type PREMIUM_TAG = typeof PREMIUM_TAGS[number]; -export const PREMIUM_TAGS = [ - 'FREE', 'PLAN_1', 'PLAN_2', 'CUSTOM_1' -] as const; +export const PREMIUM_TAGS = ['FREE', 'PLAN_1', 'PLAN_2', 'CUSTOM_1'] as const; export type PREMIUM_DATA = { @@ -34,7 +33,7 @@ export const PREMIUM_PLAN: Record = { COUNT_LIMIT: 500_000, AI_MESSAGE_LIMIT: 5_000, PRICE: 'price_1POKCKB2lPUiVs9Vol8XOmhW', - PRICE_TEST: '' + PRICE_TEST: 'price_1POK34B2lPUiVs9VIROb0IIV' }, CUSTOM_1: { ID: 1001, @@ -45,6 +44,18 @@ export const PREMIUM_PLAN: Record = { } } +CustomPremiumPriceModel.find({}).then(custom_prices => { + for (const custom_price of custom_prices) { + PREMIUM_PLAN[custom_price.tag] = { + ID: custom_price.price_id, + COUNT_LIMIT: custom_price.count_limit, + AI_MESSAGE_LIMIT: custom_price.ai_message_limit, + PRICE: custom_price.price, + PRICE_TEST: custom_price.price_test || '' + } + } +}); + export function getPlanFromTag(tag: PREMIUM_TAG) { return PREMIUM_PLAN[tag]; diff --git a/shared/schema/CustomPremiumPriceSchema.ts b/shared/schema/CustomPremiumPriceSchema.ts new file mode 100644 index 0000000..1b20e0c --- /dev/null +++ b/shared/schema/CustomPremiumPriceSchema.ts @@ -0,0 +1,21 @@ +import { model, Schema } from 'mongoose'; + +export type TCustomPremiumPrice = { + tag: string, + price_id: number, + count_limit: number, + ai_message_limit: number, + price: string, + price_test?: string +} + +const CustomPremiumPriceSchema = new Schema({ + tag: { type: String, required: true }, + price_id: { type: Number, required: true }, + count_limit: { type: Number, required: true }, + ai_message_limit: { type: Number, required: true }, + price: { type: String, required: true }, + price_test: { type: String }, +}) + +export const CustomPremiumPriceModel = model('custom_premium_prices', CustomPremiumPriceSchema);