fix payment service + appsumo + ui

This commit is contained in:
Emily
2025-04-22 18:42:18 +02:00
parent f631c29fb2
commit a9bbc58ad1
13 changed files with 121 additions and 65 deletions

View File

@@ -109,6 +109,9 @@ const avgSessionDuration = computed(() => {
seconds += avg * 60; seconds += avg * 60;
while (seconds >= 60) { seconds -= 60; minutes += 1; } while (seconds >= 60) { seconds -= 60; minutes += 1; }
while (minutes >= 60) { minutes -= 60; hours += 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` return `${hours > 0 ? hours + 'h ' : ''}${minutes}m ${seconds.toFixed()}s`
}); });

View File

@@ -32,7 +32,7 @@ const { data: pendingInvites, refresh: refreshInvites } = useFetch('/api/project
headers: useComputedHeaders({}) headers: useComputedHeaders({})
}); });
const { userRoles } = useLoggedUser(); const { userRoles, isPremium } = useLoggedUser();
const { projectList } = useProject(); const { projectList } = useProject();
const debugMode = process.dev; const debugMode = process.dev;
@@ -94,6 +94,9 @@ async function generatePDF() {
const { actions } = useProject(); const { actions } = useProject();
const { showDrawer } = useDrawer();
const modal = useModal(); const modal = useModal();
@@ -138,13 +141,6 @@ function openPendingInvites() {
}"> }">
<div class="py-4 pb-2 px-2 gap-6 flex flex-col w-full"> <div class="py-4 pb-2 px-2 gap-6 flex flex-col w-full">
<!-- <div class="flex px-2" v-if="!isPremium">
<LyxUiButton type="primary" class="w-full text-center text-[.8rem] font-medium" @click="pricingDrawer.visible.value = true;">
Upgrade plan
</LyxUiButton>
</div> -->
<div class="flex px-2 flex-col"> <div class="flex px-2 flex-col">
<div class="flex items-center gap-2 w-full"> <div class="flex items-center gap-2 w-full">
@@ -304,6 +300,16 @@ function openPendingInvites() {
<div class="grow"></div> <div class="grow"></div>
<LyxUiCard class="w-full mb-4" v-if="!isPremium">
<div class="flex flex-col gap-3">
<div class="text-center"> Upgrade to premium </div>
<LyxUiButton type="primary" class="w-full text-center text-[.8rem] font-medium"
@click="showDrawer('PRICING')">
Upgrade
</LyxUiButton>
</div>
</LyxUiCard>
<div v-if="pendingInvites && pendingInvites.length > 0" @click="openPendingInvites()" <div v-if="pendingInvites && pendingInvites.length > 0" @click="openPendingInvites()"
class="w-full bg-[#fbbf2422] p-4 rounded-lg text-[.9rem] flex flex-col justify-center cursor-pointer"> class="w-full bg-[#fbbf2422] p-4 rounded-lg text-[.9rem] flex flex-col justify-center cursor-pointer">
<div class="poppins font-medium dark:text-lyx-text text-lyx-lightmode-text"> <div class="poppins font-medium dark:text-lyx-text text-lyx-lightmode-text">
@@ -316,13 +322,18 @@ function openPendingInvites() {
</div> </div>
</div> </div>
<LyxUiSeparator class="px-4 mb-2"></LyxUiSeparator> <!-- <LyxUiSeparator class="px-4 mb-2"></LyxUiSeparator> -->
<div class="flex px-1 w-full">
<LayoutVerticalBottomMenu></LayoutVerticalBottomMenu>
<LyxUiCard class="flex py-1 px-1 w-full relative">
<div class="absolute top-[-22%] right-0">
<div @click="showDrawer('PRICING')"
class="flex items-center gap-1 poppins text-[.5rem] bg-[#fbbe244f] outline outline-[1px] outline-[#fbbf24] px-3 py-[.12rem] rounded-sm">
<i class="far fa-crown mb-[2px]"></i>
<div>Premium</div>
</div> </div>
</div>
<LayoutVerticalBottomMenu></LayoutVerticalBottomMenu>
</LyxUiCard>

View File

@@ -54,7 +54,7 @@ async function redeemCode() {
Redeemed codes: {{ valid_codes.data.value?.count || '0' }} Redeemed codes: {{ valid_codes.data.value?.count || '0' }}
</div> </div>
<div class="poppins text-[1.1rem] text-lyx-lightmode-text dark:text-yellow-400 mb-2"> <div class="poppins text-[1.1rem] text-lyx-lightmode-text dark:text-yellow-400 mb-2">
*Plan upgrades are applicable exclusively to this project(workspace). *Plan upgrades are applied to account level.
</div> </div>
</template> </template>
</SettingsTemplate> </SettingsTemplate>

View File

@@ -6,7 +6,8 @@ const selfhosted = useSelfhosted();
const items = [ const items = [
{ label: 'General', slot: 'general', tab: 'general' }, { label: 'General', slot: 'general', tab: 'general' },
{ label: 'Domains', slot: 'domains', tab: 'domains' } { label: 'Domains', slot: 'domains', tab: 'domains' },
{ label: 'Codes', slot: 'codes', tab: 'codes' },
] ]
</script> </script>
@@ -30,7 +31,7 @@ const items = [
Billing disabled in self-host mode Billing disabled in self-host mode
</div> </div>
</template> --> </template> -->
<!-- <template #codes> <template #codes>
<SettingsCodes v-if="!selfhosted" :key="refreshKey"></SettingsCodes> <SettingsCodes v-if="!selfhosted" :key="refreshKey"></SettingsCodes>
<div class="flex popping text-[1.2rem] font-semibold justify-center mt-[20vh] text-lyx-lightmode-text dark:text-lyx-text" <div class="flex popping text-[1.2rem] font-semibold justify-center mt-[20vh] text-lyx-lightmode-text dark:text-lyx-text"
v-if="selfhosted"> v-if="selfhosted">
@@ -39,7 +40,7 @@ const items = [
</template> </template>
<template #account> <template #account>
<SettingsAccount :key="refreshKey"></SettingsAccount> <SettingsAccount :key="refreshKey"></SettingsAccount>
</template> --> </template>
</CustomTab> </CustomTab>
</div> </div>

View File

@@ -1,17 +1,13 @@
import { getPlanFromId, PREMIUM_PLAN } from "@data/PREMIUM"; import { getPlanFromId, PREMIUM_PLAN } from "@data/PREMIUM";
import { canTryAppsumoCode, checkAppsumoCode, useAppsumoCode, useTryAppsumoCode } from "~/server/services/AppsumoService"; 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) { function getPlanToActivate(current_plan_id: number) {
if (current_plan_id === PREMIUM_PLAN.FREE.ID) { if (current_plan_id === PREMIUM_PLAN.FREE.ID) {
return PREMIUM_PLAN.APPSUMO_INCUBATION; 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) { if (current_plan_id === PREMIUM_PLAN.APPSUMO_INCUBATION.ID) {
return PREMIUM_PLAN.APPSUMO_ACCELERATION; return PREMIUM_PLAN.APPSUMO_ACCELERATION;
} }
@@ -38,13 +34,18 @@ export default defineEventHandler(async event => {
const valid = await checkAppsumoCode(code); const valid = await checkAppsumoCode(code);
if (!valid) return setResponseStatus(event, 400, 'Code not valid'); if (!valid) return setResponseStatus(event, 400, 'Code not valid');
// const currentPlan = getPlanFromId(project.premium_type); const currentPremiumData = await PremiumModel.findOne({ user_id: user.id });
// if (!currentPlan) return setResponseStatus(event, 400, 'Current plan not found'); if (!currentPremiumData) return setResponseStatus(event, 400, 'Error finding user');
// const planToActivate = getPlanToActivate(currentPlan.ID);
// if (!planToActivate) return setResponseStatus(event, 400, 'Cannot use code on current plan');
// 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);
}); });

View File

@@ -15,6 +15,8 @@ import { TeamMemberModel } from "~/shared/schema/TeamMemberSchema";
import { PasswordModel } from "~/shared/schema/PasswordSchema"; import { PasswordModel } from "~/shared/schema/PasswordSchema";
import { PremiumModel } from "~/shared/schema/PremiumSchema"; import { PremiumModel } from "~/shared/schema/PremiumSchema";
import { PaymentServiceHelper } from "~/server/services/PaymentServiceHelper"; 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 => { export default defineEventHandler(async event => {
@@ -24,9 +26,8 @@ export default defineEventHandler(async event => {
const projects = await ProjectModel.find({ owner: userData.id }); const projects = await ProjectModel.find({ owner: userData.id });
const premium = await PremiumModel.findOne({ user_id: 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 membersDeletation = await TeamMemberModel.deleteMany({ user_id: userData.id });
const membersEmailDeletation = await TeamMemberModel.deleteMany({ email: userData.user.email }); 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 limitdeletation = await UserLimitModel.deleteMany({ user_id: userData.id });
const notifiesDeletation = await LimitNotifyModel.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) { for (const project of projects) {
const project_id = project._id; const project_id = project._id;
const projectDeletation = await ProjectModel.deleteOne({ _id: project_id }); const projectDeletation = await ProjectModel.deleteOne({ _id: project_id });
const userSettingsDeletation = await UserSettingsModel.deleteOne({ project_id }); const userSettingsDeletation = await UserSettingsModel.deleteOne({ project_id });
const countDeletation = await ProjectCountModel.deleteMany({ 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 //Shields
const addressBlacklistDeletation = await AddressBlacklistModel.deleteMany({ project_id }); const addressBlacklistDeletation = AddressBlacklistModel.deleteMany({ project_id });
const botTrafficOptionsDeletation = await BotTrafficOptionModel.deleteMany({ project_id }); const botTrafficOptionsDeletation = BotTrafficOptionModel.deleteMany({ project_id });
const countryBlacklistDeletation = await CountryBlacklistModel.deleteMany({ project_id }); const countryBlacklistDeletation = CountryBlacklistModel.deleteMany({ project_id });
const domainWhitelistDeletation = await DomainWhitelistModel.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 }); const userDeletation = await UserModel.deleteOne({ _id: userData.id });
return { ok: true }; return { ok: true };

View File

@@ -31,6 +31,10 @@ export class PaymentServiceHelper {
return await this.send('/create_customer', { user_id }); 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 }> { static async create_payment(user_id: string, plan_id: number): PaymentServiceResponse<{ url: string }> {
return await this.send('/create_payment', { user_id, plan_id }); return await this.send('/create_payment', { user_id, plan_id });
} }

View File

@@ -24,6 +24,8 @@ if (!TOKEN || TOKEN.length == 0) {
process.exit(); process.exit();
} }
app.use('/webhook', webhookRouter);
app.use((req, res, next) => { app.use((req, res, next) => {
const token = req.header('x-litlyx-token'); const token = req.header('x-litlyx-token');
if (token != TOKEN) { if (token != TOKEN) {
@@ -34,8 +36,6 @@ app.use((req, res, next) => {
next(); next();
}); });
app.use('/webhook', webhookRouter);
app.use('/payment', paymentRouter); app.use('/payment', paymentRouter);
const port = parseInt(process.env.PORT); const port = parseInt(process.env.PORT);

View File

@@ -162,3 +162,29 @@ paymentRouter.post('/delete_customer', json(), async (req, res) => {
res.status(500).json({ error: ex.message }); 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 });
}
});

View File

@@ -153,22 +153,21 @@ class StripeService {
// return false; // return false;
// } // }
// async createSubscription(customer_id: string, planId: number) { async createSubscription(customer_id: string, planTag: string) {
// if (this.disabledMode) return; if (!this.stripe) throw Error('Stripe not initialized');
// if (!this.stripe) throw Error('Stripe not initialized');
// const PLAN = getPlanFromId(planId); const PLAN_DATA = getPlanFromTag(planTag as any);
// if (!PLAN) throw Error('Plan not found'); if (!PLAN_DATA) throw Error('Plan not found');
// const subscription = await this.stripe.subscriptions.create({ const subscription = await this.stripe.subscriptions.create({
// customer: customer_id, customer: customer_id,
// items: [ items: [
// { price: this.testMode ? PLAN.PRICE_TEST : PLAN.PRICE, quantity: 1 } { price: this.testMode ? PLAN_DATA.PRICE_TEST : PLAN_DATA.PRICE, quantity: 1 }
// ], ],
// }); });
// return subscription; return subscription;
// } }
// async createOneTimeSubscriptionDummy(customer_id: string, planId: number) { // async createOneTimeSubscriptionDummy(customer_id: string, planId: number) {
// if (this.disabledMode) return; // if (this.disabledMode) return;

View File

@@ -610,6 +610,8 @@ function isBot(userAgent: string) {
export async function isAllowedToLog(project_id: string, website: string, ip: string, 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 }); const blacklistData = await AddressBlacklistModel.find({ project_id }, { address: 1 });
for (const blacklistedData of blacklistData) { for (const blacklistedData of blacklistData) {
if (blacklistedData.address == ip) return false; if (blacklistedData.address == ip) return false;

View File

@@ -16,6 +16,7 @@ const streamName = requireEnv('STREAM_NAME');
import DeprecatedRouter from "./deprecated"; import DeprecatedRouter from "./deprecated";
import { isAllowedToLog } from "./controller"; import { isAllowedToLog } from "./controller";
import { connectDatabase } from "./shared/services/DatabaseService"; import { connectDatabase } from "./shared/services/DatabaseService";
app.use('/v1', DeprecatedRouter); app.use('/v1', DeprecatedRouter);
app.post('/event', express.json(jsonOptions), async (req, res) => { app.post('/event', express.json(jsonOptions), async (req, res) => {

View File

@@ -144,7 +144,7 @@ export const PREMIUM_PLAN: Record<PLAN_TAG, PLAN_DATA> = {
COUNT_LIMIT: 50_000, COUNT_LIMIT: 50_000,
AI_MESSAGE_LIMIT: 30, AI_MESSAGE_LIMIT: 30,
PRICE: 'price_1QIXwbB2lPUiVs9VKSsoksaU', PRICE: 'price_1QIXwbB2lPUiVs9VKSsoksaU',
PRICE_TEST: '', PRICE_TEST: 'price_1RBIUsB2lPUiVs9VojGan6WH',
COST: 0, COST: 0,
TAG: 'APPSUMO_INCUBATION' TAG: 'APPSUMO_INCUBATION'
}, },
@@ -153,7 +153,7 @@ export const PREMIUM_PLAN: Record<PLAN_TAG, PLAN_DATA> = {
COUNT_LIMIT: 150_000, COUNT_LIMIT: 150_000,
AI_MESSAGE_LIMIT: 100, AI_MESSAGE_LIMIT: 100,
PRICE: 'price_1QIXxRB2lPUiVs9VrjaVRoOl', PRICE: 'price_1QIXxRB2lPUiVs9VrjaVRoOl',
PRICE_TEST: '', PRICE_TEST: 'price_1RBIV5B2lPUiVs9VKQyxvhst',
COST: 0, COST: 0,
TAG: 'APPSUMO_ACCELERATION' TAG: 'APPSUMO_ACCELERATION'
}, },
@@ -162,7 +162,7 @@ export const PREMIUM_PLAN: Record<PLAN_TAG, PLAN_DATA> = {
COUNT_LIMIT: 500_000, COUNT_LIMIT: 500_000,
AI_MESSAGE_LIMIT: 3_000, AI_MESSAGE_LIMIT: 3_000,
PRICE: 'price_1QIXy8B2lPUiVs9VQBOUPAoE', PRICE: 'price_1QIXy8B2lPUiVs9VQBOUPAoE',
PRICE_TEST: '', PRICE_TEST: 'price_1RBIVFB2lPUiVs9VsMoldAu3',
COST: 0, COST: 0,
TAG: 'APPSUMO_GROWTH' TAG: 'APPSUMO_GROWTH'
}, },