fix payments

This commit is contained in:
Emily
2024-06-12 15:43:23 +02:00
parent b7c3ef19ba
commit 6a7143c8d4
8 changed files with 173 additions and 89 deletions

View File

@@ -6,7 +6,8 @@ export type PricingCardProp = {
features: string[], features: string[],
desc: string, desc: string,
active: boolean, active: boolean,
planId: number planId: number,
isDowngrade: boolean
} }
const props = defineProps<{ data: PricingCardProp }>(); const props = defineProps<{ data: PricingCardProp }>();
@@ -43,10 +44,14 @@ async function onUpgradeClick() {
<div v-if="data.active" class="text-[1rem] bg-[#1f1f22] rounded-md py-2 text-center"> <div v-if="data.active" class="text-[1rem] bg-[#1f1f22] rounded-md py-2 text-center">
Current active plan Current active plan
</div> </div>
<div @click="onUpgradeClick()" v-if="!data.active" <div @click="onUpgradeClick()" v-if="!data.active && !data.isDowngrade"
class="cursor-pointer text-[1rem] font-semibold bg-[#3a3af5] rounded-md py-2 text-center"> class="cursor-pointer text-[1rem] font-semibold bg-[#3a3af5] rounded-md py-2 text-center">
Upgrade Upgrade
</div> </div>
<div @click="onUpgradeClick()" v-if="!data.active && data.isDowngrade"
class="cursor-pointer text-[1rem] font-semibold bg-[#1f1f22] text-red-400 rounded-md py-2 text-center">
Downgrade
</div>
</div> </div>
<div class="bg-gray-400 h-[1px] w-full my-4"></div> <div class="bg-gray-400 h-[1px] w-full my-4"></div>

View File

@@ -4,6 +4,8 @@ import type { PricingCardProp } from './PricingCard.vue';
const activeProject = useActiveProject(); const activeProject = useActiveProject();
const props = defineProps<{ currentSub: number }>();
const starterTierCardData = ref<PricingCardProp>({ const starterTierCardData = ref<PricingCardProp>({
title: 'STARTER', title: 'STARTER',
@@ -23,6 +25,7 @@ const starterTierCardData = ref<PricingCardProp>({
dedicated server we suggest to upgrade the dedicated server we suggest to upgrade the
plan to an higher one!`, plan to an higher one!`,
active: activeProject.value?.premium === false, active: activeProject.value?.premium === false,
isDowngrade: props.currentSub > 0,
planId: 0 planId: 0
}); });
@@ -41,6 +44,7 @@ const accelerationTierCardData = ref<PricingCardProp>({
], ],
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!`, 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, active: activeProject.value?.premium_type === 1,
isDowngrade: props.currentSub > 1,
planId: 1 planId: 1
}); });
@@ -59,6 +63,7 @@ const expansionTierCardData = ref<PricingCardProp>({
], ],
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!`, 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, active: activeProject.value?.premium_type === 2,
isDowngrade: props.currentSub > 2,
planId: 2 planId: 2
}); });

View File

@@ -63,7 +63,7 @@ function getPremiumName(type: number) {
<div class="w-full h-full p-8 overflow-y-auto pb-40 lg:pb-0 relative overflow-x-hidden"> <div class="w-full h-full p-8 overflow-y-auto pb-40 lg:pb-0 relative overflow-x-hidden">
<Transition name="pdrawer"> <Transition name="pdrawer">
<PricingDrawer @onCloseClick="showPricingDrawer = false" <PricingDrawer @onCloseClick="showPricingDrawer = false" :currentSub="planData?.premium_type || 0"
class="bg-black fixed right-0 top-0 w-full xl:w-[60vw] xl:min-w-[65rem] h-full z-[20]" class="bg-black fixed right-0 top-0 w-full xl:w-[60vw] xl:min-w-[65rem] h-full z-[20]"
v-if=showPricingDrawer> v-if=showPricingDrawer>
</PricingDrawer> </PricingDrawer>
@@ -131,7 +131,7 @@ function getPremiumName(type: number) {
<div> {{ prettyExpireDate }}</div> <div> {{ prettyExpireDate }}</div>
</div> </div>
<div @click="onPlanUpgradeClick()" <div @click="onPlanUpgradeClick()"
class="cursor-pointer flex items-center gap-2 text-[.9rem] text-accent drop-shadow-[0_0_8px_#000000]"> 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]">
<div class="poppins"> Upgrade plan </div> <div class="poppins"> Upgrade plan </div>
<i class="fas fa-arrow-up-right"></i> <i class="fas fa-arrow-up-right"></i>
</div> </div>
@@ -164,15 +164,6 @@ function getPremiumName(type: number) {
</div> </div>
</div> </div>
</div> </div>
<div class="my-4 w-full bg-gray-400/30 h-[1px]">
</div>
<div class="flex justify-end px-8 flex-col sm:flex-row">
<div @click="onPlanUpgradeClick()"
class="cursor-pointer flex items-center gap-2 text-[.9rem] text-accent drop-shadow-[0_0_8px_#000000]">
<div class="poppins"> Upgrade plan </div>
<i class="fas fa-arrow-up-right"></i>
</div>
</div>
</Card> </Card>
</div> </div>

View File

@@ -2,42 +2,11 @@
import StripeService from '~/server/services/StripeService'; import StripeService from '~/server/services/StripeService';
import type Event from 'stripe'; import type Event from 'stripe';
import { ProjectModel } from '@schema/ProjectSchema'; 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 { ProjectCountModel } from '@schema/ProjectsCounts';
import { ProjectLimitModel } from '@schema/ProjectsLimits'; import { ProjectLimitModel } from '@schema/ProjectsLimits';
import { UserModel } from '@schema/UserSchema'; 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) { 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: plan.ID != 0,
premium_type: plan.ID, premium_type: plan.ID,
subscription_id, subscription_id,
premium_expire_at: current_period_end premium_expire_at: current_period_end * 1000
}); });
await ProjectLimitModel.updateOne({ project_id }, { await ProjectLimitModel.updateOne({ project_id }, {
@@ -61,33 +30,106 @@ 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 });
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' } if (!project) return { error: 'CUSTOMER NOT EXIST' }
const price = event.data.object.items.data[0].price.id; 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(),
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' } if (!price) return { error: 'Price not found' }
const PLAN = getPlanFromPrice(price, StripeService.testMode || false); const PLAN = getPlanFromPrice(price, StripeService.testMode || false);
if (!PLAN) return { error: 'Plan not found' } if (!PLAN) return { error: 'Plan not found' }
if (project.subscription_id != event.data.object.id) { await addSubscriptionToProject(project._id.toString(), PLAN, subscription_id, currentSubscription.current_period_start, currentSubscription.current_period_end)
try {
await StripeService.deleteSubscription(project.subscription_id); return { ok: true };
} catch (ex) { }
}
return { received: true, warn: 'payment status not paid' }
} }
if (event.data.object.status === 'active') {
await addSubscriptionToProject( async function onSubscriptionCreated(event: Event.CustomerSubscriptionCreatedEvent) {
project._id.toString(),
PLAN, // const project = await ProjectModel.findOne({ customer_id: event.data.object.customer });
event.data.object.id, // if (!project) return { error: 'CUSTOMER NOT EXIST' }
event.data.object.current_period_start,
event.data.object.current_period_end // 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
// );
// }
@@ -123,27 +165,27 @@ async function onSubscriptionDeleted(event: Event.CustomerSubscriptionDeletedEve
async function onSubscriptionUpdated(event: Event.CustomerSubscriptionUpdatedEvent) { async function onSubscriptionUpdated(event: Event.CustomerSubscriptionUpdatedEvent) {
const project = await ProjectModel.findOne({ // const project = await ProjectModel.findOne({
customer_id: event.data.object.customer, // 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; // const price = event.data.object.items.data[0].price.id;
if (!price) return { error: 'Price not found' } // if (!price) return { error: 'Price not found' }
const PLAN = getPlanFromPrice(price, StripeService.testMode || false); // const PLAN = getPlanFromPrice(price, StripeService.testMode || false);
if (!PLAN) return { error: 'Plan not found' } // if (!PLAN) return { error: 'Plan not found' }
if (event.data.object.status === 'active') { // if (event.data.object.status === 'active') {
await addSubscriptionToProject( // await addSubscriptionToProject(
project._id.toString(), // project._id.toString(),
PLAN, // PLAN,
event.data.object.id, // event.data.object.id,
event.data.object.current_period_start, // event.data.object.current_period_start,
event.data.object.current_period_end // event.data.object.current_period_end
); // );
} // }
return { ok: true } return { ok: true }
} }
@@ -159,6 +201,7 @@ export default defineEventHandler(async event => {
const eventData = StripeService.parseWebhook(body, signature); const eventData = StripeService.parseWebhook(body, signature);
if (!eventData) return; if (!eventData) return;
if (eventData.type === 'invoice.paid') return await onPaymentSuccess(eventData); 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.deleted') return await onSubscriptionDeleted(eventData);
if (eventData.type === 'customer.subscription.created') return await onSubscriptionCreated(eventData); if (eventData.type === 'customer.subscription.created') return await onSubscriptionCreated(eventData);
if (eventData.type === 'customer.subscription.updated') return await onSubscriptionUpdated(eventData); if (eventData.type === 'customer.subscription.updated') return await onSubscriptionUpdated(eventData);

View File

@@ -17,8 +17,10 @@ export default async () => {
config.EMAIL_PASS, config.EMAIL_PASS,
); );
StripeService.init(config.STRIPE_SECRET, config.STRIPE_WH_SECRET, false); StripeService.init(config.STRIPE_SECRET, config.STRIPE_WH_SECRET, false);
if (!connection || connection.connection.readyState == mongoose.ConnectionStates.disconnected) { if (!connection || connection.connection.readyState == mongoose.ConnectionStates.disconnected) {
console.log('[DATABASE] Connecting'); console.log('[DATABASE] Connecting');
connection = await mongoose.connect(config.MONGO_CONNECTION_STRING); connection = await mongoose.connect(config.MONGO_CONNECTION_STRING);

View File

@@ -54,6 +54,12 @@ class StripeService {
return subscription; 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) { async getInvoices(customer_id: string) {
const invoices = await this.stripe?.invoices.list({ customer: customer_id }); const invoices = await this.stripe?.invoices.list({ customer: customer_id });
return invoices; return invoices;

View File

@@ -1,9 +1,8 @@
import { CustomPremiumPriceModel } from "../schema/CustomPremiumPriceSchema";
export type PREMIUM_TAG = typeof PREMIUM_TAGS[number]; export type PREMIUM_TAG = typeof PREMIUM_TAGS[number];
export const PREMIUM_TAGS = [ export const PREMIUM_TAGS = ['FREE', 'PLAN_1', 'PLAN_2', 'CUSTOM_1'] as const;
'FREE', 'PLAN_1', 'PLAN_2', 'CUSTOM_1'
] as const;
export type PREMIUM_DATA = { export type PREMIUM_DATA = {
@@ -34,7 +33,7 @@ export const PREMIUM_PLAN: Record<PREMIUM_TAG, PREMIUM_DATA> = {
COUNT_LIMIT: 500_000, COUNT_LIMIT: 500_000,
AI_MESSAGE_LIMIT: 5_000, AI_MESSAGE_LIMIT: 5_000,
PRICE: 'price_1POKCKB2lPUiVs9Vol8XOmhW', PRICE: 'price_1POKCKB2lPUiVs9Vol8XOmhW',
PRICE_TEST: '' PRICE_TEST: 'price_1POK34B2lPUiVs9VIROb0IIV'
}, },
CUSTOM_1: { CUSTOM_1: {
ID: 1001, ID: 1001,
@@ -45,6 +44,18 @@ export const PREMIUM_PLAN: Record<PREMIUM_TAG, PREMIUM_DATA> = {
} }
} }
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) { export function getPlanFromTag(tag: PREMIUM_TAG) {
return PREMIUM_PLAN[tag]; return PREMIUM_PLAN[tag];

View File

@@ -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<TCustomPremiumPrice>({
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<TCustomPremiumPrice>('custom_premium_prices', CustomPremiumPriceSchema);