fix pricing + stripe payments

This commit is contained in:
Emily
2024-09-05 16:56:21 +02:00
parent 8e56069b1a
commit 30b3ed80e2
10 changed files with 326 additions and 19 deletions

View File

@@ -13,11 +13,11 @@ export type PricingCardProp = {
planId: number
}
const props = defineProps<{ datas: PricingCardProp[] }>();
const props = defineProps<{ datas: PricingCardProp[], defaultIndex?: number }>();
const activeProject = useActiveProject();
const currentIndex = ref<number>(0);
const currentIndex = ref<number>(props.defaultIndex || 0);
const data = computed(() => {
return props.datas[currentIndex.value];
@@ -37,13 +37,19 @@ async function onUpgradeClick() {
<template>
<div class="relative bg-[#151515] outline outline-[1px] outline-[#262626] py-8 px-10 rounded-lg w-full max-w-[30rem]">
<div
class="relative bg-[#151515] outline outline-[1px] outline-[#262626] py-8 px-10 rounded-lg w-full max-w-[30rem]">
<div class="flex flex-col gap-3 text-center">
<div class="poppins text-xl font-light"> {{ data.title }} </div>
<div v-if="data.active" class="absolute right-6 top-3 poppins text-[.75rem] bg-[#222A42] outline outline-[1px] outline-[#5680F8] px-3 py-[.1rem] rounded-xl">
<div class="flex flex-col gap-3 text-center pt-3">
<div v-if="data.active"
class="absolute right-6 top-3 poppins text-[.75rem] bg-[#222A42] outline outline-[1px] outline-[#5680F8] px-3 py-[.1rem] rounded-sm">
Active
</div>
<div v-if="!data.active && data.title === 'Growth'"
class="absolute right-6 top-3 poppins text-[.75rem] bg-[#fbbe244f] outline outline-[1px] outline-[#fbbf24] px-3 py-[.1rem] rounded-sm">
Most popular
</div>
<div class="poppins text-xl font-light"> {{ data.title }} </div>
<div class="poppins text-4xl font-medium"> {{ data.price }} </div>
</div>

View File

@@ -40,7 +40,6 @@ const customPricing: PricingCardProp[] = [
'DB instance: DEDICATED',
'Dedicated operator',
'White label',
'Custom Charts',
'Custom Data Aggregation'
],
cta: 'Let\'s Talk!',
@@ -179,10 +178,22 @@ const emits = defineEmits<{
(evt: 'onCloseClick'): void
}>();
const activeProject = useActiveProject()
async function onLifetimeUpgradeClick() {
const res = await $fetch<string>(`/api/pay/${activeProject.value?._id.toString()}/create-onetime`, {
...signHeaders({ 'content-type': 'application/json' }),
method: 'POST',
body: JSON.stringify({ planId: 2001 })
})
if (!res) alert('Something went wrong');
window.open(res);
}
</script>
<template>
<div class="p-8 overflow-y-auto xl:overflow-y-hidden">
<div class="p-8 overflow-y-auto">
<div @click="$emit('onCloseClick')"
class="cursor-pointer fixed top-4 right-4 rounded-full bg-menu drop-shadow-[0_0_2px_#CCCCCCCC] w-9 h-9 flex items-center justify-center">
@@ -191,10 +202,56 @@ const emits = defineEmits<{
<div class="flex gap-8 mt-10 h-max xl:flex-row flex-col">
<PricingCardGeneric class="flex-1" :datas="freePricing"></PricingCardGeneric>
<PricingCardGeneric class="flex-1" :datas="slidePricings"></PricingCardGeneric>
<PricingCardGeneric class="flex-1" :datas="slidePricings" :default-index="2"></PricingCardGeneric>
<PricingCardGeneric class="flex-1" :datas="customPricing"></PricingCardGeneric>
</div>
<LyxUiCard class="w-full mt-6">
<div class="flex">
<div class="flex flex-col gap-3">
<div>
<span class="text-lyx-primary font-semibold text-[1.4rem]">
LIFETIME DEAL
</span>
<span class="text-lyx-text-dark text-[.8rem]"> (Growh plan) </span>
</div>
<div class="text-[2rem]"> 2.399,00 </div>
<div> Up to 500.000 visits/events per month </div>
<LyxUiButton type="primary" @click="onLifetimeUpgradeClick()"> Purchase </LyxUiButton>
</div>
<div class="flex justify-evenly grow">
<div class="flex flex-col justify-evenly">
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> Slack support </div>
</div>
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> Unlimited domanis </div>
</div>
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> Unlimited reports </div>
</div>
</div>
<div class="flex flex-col justify-evenly">
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> AI Tokens: 3.000 / month </div>
</div>
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> Server type: SHARED </div>
</div>
<div class="flex items-center gap-2">
<img class="h-6" :src="'/check.png'" alt="Check">
<div> Data retention: 1 Year </div>
</div>
</div>
</div>
</div>
</LyxUiCard>
<div class="flex justify-between items-center mt-10 flex-col xl:flex-row">
<div class="flex flex-col gap-2">
<div class="poppins text-[2rem] font-semibold">
@@ -212,5 +269,8 @@ const emits = defineEmits<{
</div>
</div>
</div>
</div>
</template>

View File

@@ -0,0 +1,40 @@
import { getPlanFromId } from "@data/PREMIUM";
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => {
const project_id = getRequestProjectId(event);
if (!project_id) return;
const user = getRequestUser(event);
const project = await getUserProjectFromId(project_id, user);
if (!project) return;
const body = await readBody(event);
const { planId } = body;
const PLAN = getPlanFromId(planId);
if (!PLAN) {
console.error('PLAN', planId, 'NOT EXIST');
return setResponseStatus(event, 400, 'Plan not exist');
}
const intent = await StripeService.createOnetimePayment(
StripeService.testMode ? PLAN.PRICE_TEST : PLAN.PRICE,
'https://dashboard.litlyx.com/payment_ok',
project_id,
project.customer_id
)
if (!intent) {
console.error('Cannot create Intent', { plan: PLAN });
return setResponseStatus(event, 400, 'Cannot create intent');
}
return intent.url;
});

View File

@@ -63,6 +63,36 @@ async function onPaymentFailed(event: Event.InvoicePaymentFailedEvent) {
}
async function onPaymentOnetimeSuccess(event: Event.PaymentIntentSucceededEvent) {
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 === 'succeeded') {
const PLAN = getPlanFromPrice(event.data.object.metadata.price, StripeService.testMode || false);
if (!PLAN) return { error: 'Plan not found' }
const dummyPlan = PLAN.ID + 3000;
const subscription = await StripeService.createOneTimeSubscriptionDummy(customer_id, dummyPlan);
if (!subscription) return { error: 'Error creating subscription' }
const allSubscriptions = await StripeService.getAllSubscriptions(customer_id);
if (!allSubscriptions) return;
for (const subscription of allSubscriptions.data) {
if (subscription.id === subscription.id) continue;
await StripeService.deleteSubscription(subscription.id);
}
await addSubscriptionToProject(project._id.toString(), PLAN, subscription.id, subscription.current_period_start, subscription.current_period_end)
return { ok: true };
}
return { received: true, warn: 'object status not succeeded' }
}
async function onPaymentSuccess(event: Event.InvoicePaidEvent) {
const customer_id = event.data.object.customer as string;
@@ -76,7 +106,7 @@ async function onPaymentSuccess(event: Event.InvoicePaidEvent) {
const allSubscriptions = await StripeService.getAllSubscriptions(customer_id);
if (!allSubscriptions) return;
const currentSubscription = allSubscriptions.data.find(e => e.id === subscription_id);
if (!currentSubscription) return { error: 'SUBSCRIPTION NOT EXIST' }
@@ -201,7 +231,11 @@ export default defineEventHandler(async event => {
const eventData = StripeService.parseWebhook(body, signature);
if (!eventData) return;
// console.log('WEBHOOK FIRED', eventData.type);
if (eventData.type === 'invoice.paid') return await onPaymentSuccess(eventData);
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);

View File

@@ -16,6 +16,25 @@ export default defineEventHandler(async event => {
const project = await ProjectModel.findById(project_id);
if (!project) return setResponseStatus(event, 400, 'Project not found');
if (project.subscription_id === 'onetime') {
const projectLimits = await ProjectLimitModel.findOne({ project_id });
if (!projectLimits) return setResponseStatus(event, 400, 'Project limits not found');
const result = {
premium: project.premium,
premium_type: project.premium_type,
billing_start_at: projectLimits.billing_start_at,
billing_expire_at: projectLimits.billing_expire_at,
limit: projectLimits.limit,
count: projectLimits.events + projectLimits.visits,
subscription_status: StripeService.isDisabled() ? 'Disabled mode' : ('One time payment')
}
return result;
}
const subscription = await StripeService.getSubscription(project.subscription_id);
const projectLimits = await ProjectLimitModel.findOne({ project_id });

View File

@@ -1,4 +1,4 @@
import { getPlanFromTag } from '@data/PREMIUM';
import { getPlanFromId, getPlanFromTag } from '@data/PREMIUM';
import Stripe from 'stripe';
class StripeService {
@@ -29,6 +29,33 @@ class StripeService {
return this.stripe.webhooks.constructEvent(body, sig, this.webhookSecret);
}
async createOnetimePayment(price: string, success_url: string, pid: string, customer?: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const checkout = await this.stripe.checkout.sessions.create({
allow_promotion_codes: true,
payment_method_types: ['card'],
invoice_creation: {
enabled: true,
},
line_items: [
{ price, quantity: 1 }
],
payment_intent_data: {
metadata: {
pid, price
}
},
customer,
success_url,
mode: 'payment'
});
return checkout;
}
async cretePayment(price: string, success_url: string, pid: string, customer?: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
@@ -50,6 +77,13 @@ class StripeService {
return checkout;
}
async getPriceData(priceId: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const priceData = await this.stripe.prices.retrieve(priceId);
return priceData;
}
async deleteSubscription(subscriptionId: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
@@ -78,7 +112,6 @@ class StripeService {
return invoices;
}
async getCustomer(customer_id: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
@@ -100,8 +133,27 @@ class StripeService {
return deleted;
}
async createOneTimeCoupon() {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
}
async createOneTimeSubscriptionDummy(customer_id: string, planId: number) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const PLAN = getPlanFromId(planId);
if (!PLAN) 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 }
],
});
return subscription;
}
async createFreeSubscription(customer_id: string) {
if (this.disabledMode) return;