mirror of
https://github.com/Litlyx/litlyx
synced 2025-12-10 07:48:37 +01:00
fix pricing + stripe payments
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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>
|
||||
40
dashboard/server/api/pay/[project_id]/create-onetime.post.ts
Normal file
40
dashboard/server/api/pay/[project_id]/create-onetime.post.ts
Normal 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;
|
||||
|
||||
});
|
||||
@@ -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);
|
||||
|
||||
@@ -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 });
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user