fix dashboard + payments

This commit is contained in:
Emily
2025-04-13 18:15:43 +02:00
parent 1d5dad44fa
commit 946f9d4d32
22 changed files with 272 additions and 521 deletions

View File

@@ -1,7 +1,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import dayjs from 'dayjs'; import dayjs from 'dayjs';
import type { SettingsTemplateEntry } from './Template.vue'; import type { SettingsTemplateEntry } from './Template.vue';
import { getPlanFromId, PREMIUM_PLAN, type PREMIUM_TAG } from '@data/PREMIUM'; import { getPlanFromId, PREMIUM_PLAN, type PLAN_TAG } from '@data/PREMIUM';
const { projectId, isGuest } = useProject(); const { projectId, isGuest } = useProject();
@@ -53,10 +53,10 @@ function openInvoice(link: string) {
window.open(link, '_blank'); window.open(link, '_blank');
} }
function getPremiumName(type: number) { function getTagName(type: number) {
return Object.keys(PREMIUM_PLAN).map(e => ({ return Object.keys(PREMIUM_PLAN).map(e => ({
...PREMIUM_PLAN[e as PREMIUM_TAG], name: e ...PREMIUM_PLAN[e as PLAN_TAG], name: e
})).find(e => e.ID == type)?.name; })).find(e => e.ID == type)?.name;
} }
@@ -168,7 +168,7 @@ const { showDrawer } = useDrawer();
</div> </div>
<div <div
class="flex lato text-[.7rem] bg-transparent border-[#262626] border-[1px] px-[.6rem] rounded-sm"> class="flex lato text-[.7rem] bg-transparent border-[#262626] border-[1px] px-[.6rem] rounded-sm">
{{ planData.premium ? getPremiumName(planData.premium_type) : 'FREE' }} {{ planData.premium ? getTagName(planData.premium_type) : 'FREE' }}
</div> </div>
</div> </div>
</div> </div>
@@ -187,9 +187,6 @@ const { showDrawer } = useDrawer();
</div> </div>
<div class="poppins"> {{ daysLeft }} days left </div> <div class="poppins"> {{ daysLeft }} days left </div>
</div> </div>
<div class="flex justify-center">
Subscription: {{ planData.subscription_status }}
</div>
</div> </div>
</div> </div>
<div class="my-4 w-full bg-gray-400/30 h-[1px]"> <div class="my-4 w-full bg-gray-400/30 h-[1px]">

View File

@@ -45,15 +45,12 @@ export default defineNuxtConfig({
AI_PROJECT: process.env.AI_PROJECT, AI_PROJECT: process.env.AI_PROJECT,
AI_KEY: process.env.AI_KEY, AI_KEY: process.env.AI_KEY,
EMAIL_SECRET: process.env.EMAIL_SECRET, EMAIL_SECRET: process.env.EMAIL_SECRET,
PAYMENT_SECRET: process.env.PAYMENT_SECRET,
AUTH_JWT_SECRET: process.env.AUTH_JWT_SECRET, AUTH_JWT_SECRET: process.env.AUTH_JWT_SECRET,
GOOGLE_AUTH_CLIENT_ID: process.env.GOOGLE_AUTH_CLIENT_ID, GOOGLE_AUTH_CLIENT_ID: process.env.GOOGLE_AUTH_CLIENT_ID,
GOOGLE_AUTH_CLIENT_SECRET: process.env.GOOGLE_AUTH_CLIENT_SECRET, GOOGLE_AUTH_CLIENT_SECRET: process.env.GOOGLE_AUTH_CLIENT_SECRET,
GITHUB_AUTH_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID, GITHUB_AUTH_CLIENT_ID: process.env.GITHUB_AUTH_CLIENT_ID,
GITHUB_AUTH_CLIENT_SECRET: process.env.GITHUB_AUTH_CLIENT_SECRET, GITHUB_AUTH_CLIENT_SECRET: process.env.GITHUB_AUTH_CLIENT_SECRET,
STRIPE_SECRET: process.env.STRIPE_SECRET,
STRIPE_WH_SECRET: process.env.STRIPE_WH_SECRET,
STRIPE_SECRET_TEST: process.env.STRIPE_SECRET_TEST,
STRIPE_WH_SECRET_TEST: process.env.STRIPE_WH_SECRET_TEST,
NOAUTH_USER_EMAIL: process.env.NOAUTH_USER_EMAIL, NOAUTH_USER_EMAIL: process.env.NOAUTH_USER_EMAIL,
NOAUTH_USER_PASS: process.env.NOAUTH_USER_PASS, NOAUTH_USER_PASS: process.env.NOAUTH_USER_PASS,
MODE: process.env.MODE || 'NONE', MODE: process.env.MODE || 'NONE',

View File

@@ -5255,8 +5255,8 @@ packages:
peerDependencies: peerDependencies:
vue: ^3.2.0 vue: ^3.2.0
vue3-google-signin@2.0.1: vue3-google-signin@2.1.1:
resolution: {integrity: sha512-vZTlVrG56JERtqQ+6YI8e92wqfhAMDyNONCsLgKKXxzCNCWEfSkZcAvz7COm9V4bvzmGsebZ8KC3ljol2qsIcg==} resolution: {integrity: sha512-RwlwyeCv8+PZjK35C/UyJN4/9MH+Fsz4bJDO7IjtmRuOaTMrvmMmI6SP5qkJgD7w9MIKOCj5rYVFMAL7+NCM0A==}
peerDependencies: peerDependencies:
vue: ^3 vue: ^3
@@ -9868,7 +9868,7 @@ snapshots:
dependencies: dependencies:
'@nuxt/kit': 3.11.2(rollup@4.18.0) '@nuxt/kit': 3.11.2(rollup@4.18.0)
unimport: 3.7.2(rollup@4.18.0) unimport: 3.7.2(rollup@4.18.0)
vue3-google-signin: 2.0.1(vue@3.4.27(typescript@5.4.2)) vue3-google-signin: 2.1.1(vue@3.4.27(typescript@5.4.2))
transitivePeerDependencies: transitivePeerDependencies:
- rollup - rollup
- supports-color - supports-color
@@ -11525,7 +11525,7 @@ snapshots:
vue-observe-visibility: 2.0.0-alpha.1(vue@3.4.27(typescript@5.4.2)) vue-observe-visibility: 2.0.0-alpha.1(vue@3.4.27(typescript@5.4.2))
vue-resize: 2.0.0-alpha.1(vue@3.4.27(typescript@5.4.2)) vue-resize: 2.0.0-alpha.1(vue@3.4.27(typescript@5.4.2))
vue3-google-signin@2.0.1(vue@3.4.27(typescript@5.4.2)): vue3-google-signin@2.1.1(vue@3.4.27(typescript@5.4.2)):
dependencies: dependencies:
vue: 3.4.27(typescript@5.4.2) vue: 3.4.27(typescript@5.4.2)

View File

@@ -1,126 +0,0 @@
import { UserLimitModel } from "@schema/UserLimitSchema";
import { AIPlugin } from "../Plugin";
import { MAX_LOG_LIMIT_PERCENT } from "@data/broker/Limits";
import { ProjectModel } from "@schema/project/ProjectSchema";
import StripeService from "~/server/services/StripeService";
import { InvoiceData } from "~/server/api/pay/invoices";
export class AiBilling extends AIPlugin<[
'getBillingInfo',
'getLimits',
'getInvoices'
]> {
constructor() {
super({
'getInvoices': {
handler: async (data: { user_id: string }) => {
const project = await ProjectModel.findOne({ user_id: data.user_id });
if (!project) return { error: 'Project not found' };
const invoices = await StripeService.getInvoices(data.user_id);
if (!invoices) return [];
return invoices?.data.map(e => {
const result: InvoiceData = {
link: e.invoice_pdf || '',
id: e.number || '',
date: e.created * 1000,
status: e.status || 'NO_STATUS',
cost: e.amount_due
}
return result;
});
},
tool: {
type: 'function',
function: {
name: 'getInvoices',
description: 'Gets the invoices of the user project',
parameters: {}
}
}
},
'getBillingInfo': {
handler: async (data: { user_id: string }) => {
return { error: 'NOT IMPLEMENTED YET' }
// if (project.subscription_id === 'onetime') {
// const projectLimits = await ProjectLimitModel.findOne({ project_id: data.project_id });
// if (!projectLimits) return { error: '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: data.project_id });
// if (!projectLimits) return { error: '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' : (subscription?.status ?? '?')
// }
// return result;
},
tool: {
type: 'function',
function: {
name: 'getBillingInfo',
description: 'Gets the informations about the billing of the user project, limits, count, subscription_status, is premium, premium type, billing start at, billing expire at',
parameters: {}
}
}
},
'getLimits': {
handler: async (data: { project_id: string }) => {
return { error: 'NOT IMPLEMENTED YET' }
// const projectLimits = await ProjectLimitModel.findOne({ project_id: data.project_id });
// if (!projectLimits) return { error: 'Project limits not found' };
// const TOTAL_COUNT = projectLimits.events + projectLimits.visits;
// const COUNT_LIMIT = projectLimits.limit;
// return {
// total: TOTAL_COUNT,
// limit: COUNT_LIMIT,
// limited: TOTAL_COUNT > COUNT_LIMIT * MAX_LOG_LIMIT_PERCENT,
// percent: Math.round(100 / COUNT_LIMIT * TOTAL_COUNT)
// }
},
tool: {
type: 'function',
function: {
name: 'getLimits',
description: 'Gets the informations about the limits of the user project',
parameters: {}
}
}
},
})
}
}
export const AiBillingInstance = new AiBilling();

View File

@@ -2,7 +2,6 @@ import { ProjectModel } from "@schema/project/ProjectSchema";
import { ProjectCountModel } from "@schema/project/ProjectsCounts"; import { ProjectCountModel } from "@schema/project/ProjectsCounts";
import { UserLimitModel } from "@schema/UserLimitSchema"; import { UserLimitModel } from "@schema/UserLimitSchema";
import { UserModel } from "@schema/UserSchema"; import { UserModel } from "@schema/UserSchema";
import StripeService from '~/server/services/StripeService';
import { PremiumModel } from "~/shared/schema/PremiumSchema"; import { PremiumModel } from "~/shared/schema/PremiumSchema";
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
@@ -20,14 +19,18 @@ export default defineEventHandler(async event => {
const premium = await PremiumModel.findOne({ user_id: userData.id }); const premium = await PremiumModel.findOne({ user_id: userData.id });
const subscription = // const subscription =
premium?.subscription_id ? // premium?.subscription_id ?
await StripeService.getSubscription(premium.subscription_id) : 'NONE'; // await StripeService.getSubscription(premium.subscription_id) : 'NONE';
const customer = // const customer =
premium?.customer_id ? // premium?.customer_id ?
await StripeService.getCustomer(premium.customer_id) : 'NONE'; // await StripeService.getCustomer(premium.customer_id) : 'NONE';
return { project, limits, counts, user, subscription, customer } return {
project, limits, counts, user,
subscription: '',
customer: ''
}
}); });

View File

@@ -4,6 +4,7 @@ import { UserModel } from '@schema/UserSchema';
import { PasswordModel } from '@schema/PasswordSchema'; import { PasswordModel } from '@schema/PasswordSchema';
import { EmailService } from '@services/EmailService'; import { EmailService } from '@services/EmailService';
import { EmailServiceHelper } from '~/server/services/EmailServiceHelper'; import { EmailServiceHelper } from '~/server/services/EmailServiceHelper';
import { PaymentServiceHelper } from '~/server/services/PaymentServiceHelper';
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
@@ -13,15 +14,22 @@ export default defineEventHandler(async event => {
if (!data) return setResponseStatus(event, 400, 'Error decoding register_code'); if (!data) return setResponseStatus(event, 400, 'Error decoding register_code');
try { try {
await PasswordModel.create({ email: data.email, password: data.password }) await PasswordModel.updateOne({ email: data.email }, { password: data.password }, { upsert: true });
await UserModel.create({ email: data.email, given_name: '', name: 'EmailLogin', locale: '', picture: '', created_at: Date.now() });
const user = await UserModel.create({ email: data.email, given_name: '', name: 'EmailLogin', locale: '', picture: '', created_at: Date.now() });
const [ok, error] = await PaymentServiceHelper.create_customer(user.id);
if (!ok) throw error;
setImmediate(() => { setImmediate(() => {
const emailData = EmailService.getEmailServerInfo('welcome', { target: data.email }); const emailData = EmailService.getEmailServerInfo('welcome', { target: data.email });
EmailServiceHelper.sendEmail(emailData); EmailServiceHelper.sendEmail(emailData);
}); });
const jwt = createUserJwt({ email: data.email, name: 'EmailLogin' }); const jwt = createUserJwt({ email: data.email, name: 'EmailLogin' });
return sendRedirect(event, `https://dashboard.litlyx.com/jwt_login?jwt_login=${jwt}`); return sendRedirect(event, `https://dashboard.litlyx.com/jwt_login?jwt_login=${jwt}`);
} catch (ex) { } catch (ex) {
console.error(ex);
return setResponseStatus(event, 400, 'Error creating user'); return setResponseStatus(event, 400, 'Error creating user');
} }

View File

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

View File

@@ -1,5 +1,5 @@
import { getPlanFromId } from "@data/PREMIUM"; import { getPlanFromId } from "@data/PREMIUM";
import StripeService from '~/server/services/StripeService'; // import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
@@ -9,29 +9,30 @@ export default defineEventHandler(async event => {
const { project, pid } = data; const { project, pid } = data;
const body = await readBody(event); // const body = await readBody(event);
const { planId } = body; // const { planId } = body;
const PLAN = getPlanFromId(planId); // const PLAN = getPlanFromId(planId);
if (!PLAN) { // if (!PLAN) {
console.error('PLAN', planId, 'NOT EXIST'); // console.error('PLAN', planId, 'NOT EXIST');
return setResponseStatus(event, 400, 'Plan not exist'); // return setResponseStatus(event, 400, 'Plan not exist');
} // }
const checkout = await StripeService.createPayment( // const checkout = await StripeService.createPayment(
StripeService.testMode ? PLAN.PRICE_TEST : PLAN.PRICE, // StripeService.testMode ? PLAN.PRICE_TEST : PLAN.PRICE,
'https://dashboard.litlyx.com/payment_ok', // 'https://dashboard.litlyx.com/payment_ok',
pid, // pid,
project.customer_id // project.customer_id
); // );
if (!checkout) { // if (!checkout) {
console.error('Cannot create payment', { plan: PLAN }); // console.error('Cannot create payment', { plan: PLAN });
return setResponseStatus(event, 400, 'Cannot create payment'); // return setResponseStatus(event, 400, 'Cannot create payment');
} // }
return checkout.url; // return checkout.url;
return '';
}); });

View File

@@ -1,5 +1,5 @@
import StripeService from '~/server/services/StripeService'; import { PaymentServiceHelper } from '~/server/services/PaymentServiceHelper';
import { PremiumModel } from '~/shared/schema/PremiumSchema'; import { PremiumModel } from '~/shared/schema/PremiumSchema';
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
@@ -10,9 +10,9 @@ export default defineEventHandler(async event => {
const premium = await PremiumModel.findOne({ user_id: data.user.id }) const premium = await PremiumModel.findOne({ user_id: data.user.id })
if (!premium) return; if (!premium) return;
const customer = await StripeService.getCustomer(premium.customer_id); const [ok, customerInfoOrError] = await PaymentServiceHelper.customer_info(data.user.id);
if (customer?.deleted) return; if (!ok) throw customerInfoOrError;
return customer?.address; return customerInfoOrError;
}); });

View File

@@ -1,9 +1,7 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA";
import { Redis } from "~/server/services/CacheService"; import { Redis } from "~/server/services/CacheService";
import StripeService from '~/server/services/StripeService'; import { PaymentServiceHelper } from "~/server/services/PaymentServiceHelper";
import { PremiumModel } from "~/shared/schema/PremiumSchema"; import { PremiumModel } from "~/shared/schema/PremiumSchema";
export type InvoiceData = { export type InvoiceData = {
date: number, date: number,
cost: number, cost: number,
@@ -22,10 +20,13 @@ export default defineEventHandler(async event => {
const premium = await PremiumModel.findOne({ user_id: data.user.id }); const premium = await PremiumModel.findOne({ user_id: data.user.id });
if (!premium) return []; if (!premium) return [];
const invoices = await StripeService.getInvoices(premium.customer_id); const [ok, invoicesOrError] = await PaymentServiceHelper.invoices_list(data.user.id);
if (!invoices) return []; if (!ok) {
console.error(invoicesOrError);
return [];
}
return invoices?.data.map(e => { return invoicesOrError.invoices.map(e => {
const result: InvoiceData = { const result: InvoiceData = {
link: e.invoice_pdf || '', link: e.invoice_pdf || '',
id: e.number || '', id: e.number || '',
@@ -36,7 +37,6 @@ export default defineEventHandler(async event => {
return result; return result;
}); });
}); });
}); });

View File

@@ -1,6 +1,6 @@
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 StripeService from '~/server/services/StripeService';
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) {
@@ -38,13 +38,13 @@ 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 currentPlan = getPlanFromId(project.premium_type);
if (!currentPlan) return setResponseStatus(event, 400, 'Current plan not found'); // if (!currentPlan) return setResponseStatus(event, 400, 'Current plan not found');
const planToActivate = getPlanToActivate(currentPlan.ID); // const planToActivate = getPlanToActivate(currentPlan.ID);
if (!planToActivate) return setResponseStatus(event, 400, 'Cannot use code on current plan'); // if (!planToActivate) return setResponseStatus(event, 400, 'Cannot use code on current plan');
await StripeService.createSubscription(project.customer_id, planToActivate.ID); // await StripeService.createSubscription(project.customer_id, planToActivate.ID);
await useAppsumoCode(pid, code); // await useAppsumoCode(pid, code);
}); });

View File

@@ -1,19 +1,17 @@
import { getUserProjectFromId } from "~/server/LIVE_DEMO_DATA"; import { PaymentServiceHelper } from '~/server/services/PaymentServiceHelper';
import StripeService from '~/server/services/StripeService'; import { PremiumModel } from '~/shared/schema/PremiumSchema';
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
const data = await getRequestData(event, []); const data = await getRequestData(event, []);
if (!data) return; if (!data) return;
const { project } = data; const premium = await PremiumModel.findOne({ user_id: data.user.id })
if (!premium) return;
if (!project.customer_id) return setResponseStatus(event, 400, 'Project has no customer_id');
const body = await readBody(event); const body = await readBody(event);
const res = await StripeService.setCustomerInfo(project.customer_id, body);
return { ok: true, data: res } return await PaymentServiceHelper.update_customer_info(data.user.id, body);
}); });

View File

@@ -1,6 +1,5 @@
import { ProjectModel, TProject } from "@schema/project/ProjectSchema"; import { ProjectModel, TProject } from "@schema/project/ProjectSchema";
import { ProjectCountModel } from "@schema/project/ProjectsCounts"; import { ProjectCountModel } from "@schema/project/ProjectsCounts";
import StripeService from '~/server/services/StripeService';
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
@@ -19,57 +18,11 @@ export default defineEventHandler(async event => {
const existingUserProjects = await ProjectModel.countDocuments({ owner: userData.id }); const existingUserProjects = await ProjectModel.countDocuments({ owner: userData.id });
if (existingUserProjects >= maxProjects) return setResponseStatus(event, 400, 'Already have max number of projects'); if (existingUserProjects >= maxProjects) return setResponseStatus(event, 400, 'Already have max number of projects');
if (StripeService.isDisabled()) { const project = await ProjectModel.create({ owner: userData.id, name: newProjectName });
const project = await ProjectModel.create({ await ProjectCountModel.create({ project_id: project._id, events: 0, visits: 0, sessions: 0 });
owner: userData.id,
name: newProjectName,
premium: false,
premium_type: 0,
customer_id: 'DISABLED_MODE',
subscription_id: "DISABLED_MODE",
premium_expire_at: new Date(3000, 1, 1)
});
return project.toJSON() as TProject;
await ProjectCountModel.create({
project_id: project._id,
events: 0,
visits: 0,
sessions: 0
});
return project.toJSON() as TProject;
} else {
const customer = await StripeService.createCustomer(userData.user.email);
if (!customer) return setResponseStatus(event, 400, 'Error creating customer');
const subscription = await StripeService.createFreeSubscription(customer.id);
if (!subscription) return setResponseStatus(event, 400, 'Error creating subscription');
const project = await ProjectModel.create({
owner: userData.id,
name: newProjectName,
premium: false,
premium_type: 0,
customer_id: customer.id,
subscription_id: subscription.id,
premium_expire_at: subscription.current_period_end * 1000
});
await ProjectCountModel.create({
project_id: project._id,
events: 0,
visits: 0,
sessions: 0
});
return project.toJSON() as TProject;
}

View File

@@ -1,5 +1,4 @@
import { UserLimitModel } from "@schema/UserLimitSchema"; import { UserLimitModel } from "@schema/UserLimitSchema";
import StripeService from '~/server/services/StripeService';
import { PremiumModel } from "~/shared/schema/PremiumSchema"; import { PremiumModel } from "~/shared/schema/PremiumSchema";
export default defineEventHandler(async event => { export default defineEventHandler(async event => {
@@ -22,14 +21,12 @@ export default defineEventHandler(async event => {
billing_expire_at: userLimits.billing_expire_at, billing_expire_at: userLimits.billing_expire_at,
limit: userLimits.limit, limit: userLimits.limit,
count: userLimits.events + userLimits.visits, count: userLimits.events + userLimits.visits,
subscription_status: StripeService.isDisabled() ? 'Disabled mode' : ('One time payment') subscription_status: 'One time'
} }
return result; return result;
} }
const subscription = await StripeService.getSubscription(premium.subscription_id);
const userLimits = await UserLimitModel.findOne({ user_id: data.user.id }); const userLimits = await UserLimitModel.findOne({ user_id: data.user.id });
if (!userLimits) return setResponseStatus(event, 400, 'User limits not found'); if (!userLimits) return setResponseStatus(event, 400, 'User limits not found');
@@ -41,7 +38,7 @@ export default defineEventHandler(async event => {
billing_expire_at: userLimits.billing_expire_at, billing_expire_at: userLimits.billing_expire_at,
limit: userLimits.limit, limit: userLimits.limit,
count: userLimits.events + userLimits.visits, count: userLimits.events + userLimits.visits,
subscription_status: StripeService.isDisabled() ? 'Disabled mode' : (subscription?.status ?? '?') subscription_status: ''
} }
return result; return result;

View File

@@ -6,7 +6,6 @@ import { UserSettingsModel } from "@schema/UserSettings";
import { AiChatModel } from "@schema/ai/AiChatSchema"; import { AiChatModel } from "@schema/ai/AiChatSchema";
import { LimitNotifyModel } from "@schema/broker/LimitNotifySchema"; import { LimitNotifyModel } from "@schema/broker/LimitNotifySchema";
import { SessionModel } from "@schema/metrics/SessionSchema"; import { SessionModel } from "@schema/metrics/SessionSchema";
import StripeService from "~/server/services/StripeService";
import { UserModel } from "@schema/UserSchema"; import { UserModel } from "@schema/UserSchema";
import { AddressBlacklistModel } from "~/shared/schema/shields/AddressBlacklistSchema"; import { AddressBlacklistModel } from "~/shared/schema/shields/AddressBlacklistSchema";
import { DomainWhitelistModel } from "~/shared/schema/shields/DomainWhitelistSchema"; import { DomainWhitelistModel } from "~/shared/schema/shields/DomainWhitelistSchema";
@@ -36,7 +35,7 @@ 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 StripeService.deleteCustomer(premium.customer_id); // await StripeService.deleteCustomer(premium.customer_id);
for (const project of projects) { for (const project of projects) {
const project_id = project._id; const project_id = project._id;
@@ -52,11 +51,10 @@ export default defineEventHandler(async event => {
const countryBlacklistDeletation = await CountryBlacklistModel.deleteMany({ project_id }); const countryBlacklistDeletation = await CountryBlacklistModel.deleteMany({ project_id });
const domainWhitelistDeletation = await DomainWhitelistModel.deleteMany({ project_id }); const domainWhitelistDeletation = await DomainWhitelistModel.deleteMany({ project_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

@@ -1,6 +1,5 @@
import mongoose from "mongoose"; import mongoose from "mongoose";
import { Redis } from "~/server/services/CacheService"; import { Redis } from "~/server/services/CacheService";
import StripeService from '~/server/services/StripeService';
import { logger } from "./Logger"; import { logger } from "./Logger";
@@ -13,16 +12,6 @@ export default async () => {
logger.info('[SERVER] Initializing'); logger.info('[SERVER] Initializing');
if (config.STRIPE_SECRET) {
const TEST_MODE = config.MODE === 'TEST';
StripeService.init(config.STRIPE_SECRET, config.STRIPE_WH_SECRET, TEST_MODE);
logger.info('[STRIPE] Initialized');
} else {
StripeService.disable();
logger.warn('[STRIPE] No stripe key - Disabled mode');
}
if (!connection || connection.connection.readyState == mongoose.ConnectionStates.disconnected) { if (!connection || connection.connection.readyState == mongoose.ConnectionStates.disconnected) {
logger.info('[DATABASE] Connecting'); logger.info('[DATABASE] Connecting');
connection = await mongoose.connect(config.MONGO_CONNECTION_STRING); connection = await mongoose.connect(config.MONGO_CONNECTION_STRING);

View File

@@ -6,7 +6,6 @@ import { AiChatModel } from '@schema/ai/AiChatSchema';
import { AiEventsInstance } from '../ai/functions/AI_Events'; import { AiEventsInstance } from '../ai/functions/AI_Events';
import { AiVisitsInstance } from '../ai/functions/AI_Visits'; import { AiVisitsInstance } from '../ai/functions/AI_Visits';
import { AiSessionsInstance } from '../ai/functions/AI_Sessions'; import { AiSessionsInstance } from '../ai/functions/AI_Sessions';
import { AiBillingInstance } from '../ai/functions/AI_Billing';
import { AiSnapshotInstance } from '../ai/functions/AI_Snapshots'; import { AiSnapshotInstance } from '../ai/functions/AI_Snapshots';
import { AiComposableChartInstance } from '../ai/functions/AI_ComposableChart'; import { AiComposableChartInstance } from '../ai/functions/AI_ComposableChart';
@@ -20,7 +19,6 @@ const tools: OpenAI.Chat.Completions.ChatCompletionTool[] = [
...AiVisitsInstance.getTools(), ...AiVisitsInstance.getTools(),
...AiEventsInstance.getTools(), ...AiEventsInstance.getTools(),
...AiSessionsInstance.getTools(), ...AiSessionsInstance.getTools(),
...AiBillingInstance.getTools(),
...AiSnapshotInstance.getTools(), ...AiSnapshotInstance.getTools(),
...AiComposableChartInstance.getTools(), ...AiComposableChartInstance.getTools(),
] ]
@@ -30,7 +28,6 @@ const functions: any = {
...AiVisitsInstance.getHandlers(), ...AiVisitsInstance.getHandlers(),
...AiEventsInstance.getHandlers(), ...AiEventsInstance.getHandlers(),
...AiSessionsInstance.getHandlers(), ...AiSessionsInstance.getHandlers(),
...AiBillingInstance.getHandlers(),
...AiSnapshotInstance.getHandlers(), ...AiSnapshotInstance.getHandlers(),
...AiComposableChartInstance.getHandlers() ...AiComposableChartInstance.getHandlers()
} }

View File

@@ -0,0 +1,50 @@
const { PAYMENT_SECRET } = useRuntimeConfig();
type ErrorResponse = [false, Error];
type OkResponse<T> = [true, T];
type PaymentServiceResponse<T> = Promise<OkResponse<T> | ErrorResponse>
export class PaymentServiceHelper {
static BASE_URL = 'https://test-payments.litlyx.com/payment';
private static async send(endpoint: string, body: Record<string, any>): PaymentServiceResponse<any> {
try {
const res = await $fetch(`${this.BASE_URL}${endpoint}`, {
body: JSON.stringify(body),
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-litlyx-token': PAYMENT_SECRET
}
})
return [true, res];
} catch (ex: any) {
console.error(ex);
return [false, ex];
}
}
static async create_customer(user_id: string): PaymentServiceResponse<{ ok: true }> {
return await this.send('/create_customer', { user_id });
}
static async create_payment(user_id: string, plan_id: number): PaymentServiceResponse<{ url: string }> {
return await this.send('/create_payment', { user_id, plan_id });
}
static async invoices_list(user_id: string): PaymentServiceResponse<{ invoices: any[] }> {
return await this.send('/invoices_list', { user_id });
}
static async customer_info(user_id: string): PaymentServiceResponse<any> {
return await this.send('/customer_info', { user_id });
}
static async update_customer_info(user_id: string, address: { line1: string, line2: string, city: string, country: string, postal_code: string, state: string }): PaymentServiceResponse<{ ok: true }> {
return await this.send('/update_customer_info', { user_id, address });
}
}

View File

@@ -1,225 +0,0 @@
import { getPlanFromId, getPlanFromTag, PREMIUM_TAG } from '@data/PREMIUM';
import Stripe from 'stripe';
class StripeService {
private stripe?: Stripe;
private privateKey?: string;
private webhookSecret?: string;
public testMode?: boolean;
private disabledMode: boolean = false;
init(privateKey: string, webhookSecret: string, testMode: boolean = false) {
this.privateKey = privateKey;
this.webhookSecret = webhookSecret;
this.stripe = new Stripe(this.privateKey);
this.testMode = testMode;
}
disable() { this.disabledMode = true; }
enable() { this.disabledMode = false; }
isDisabled() { return this.disabledMode; }
parseWebhook(body: any, sig: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
if (!this.webhookSecret) {
console.error('Stripe not initialized')
return;
}
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 createPayment(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'],
line_items: [
{ price, quantity: 1 }
],
subscription_data: {
metadata: { pid },
},
customer,
success_url,
mode: 'subscription'
});
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');
const subscription = await this.stripe.subscriptions.cancel(subscriptionId);
return subscription;
}
async getSubscription(subscriptionId: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const subscription = await this.stripe.subscriptions.retrieve(subscriptionId);
return subscription;
}
async getAllSubscriptions(customer_id: string) {
if (this.disabledMode) return;
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) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const invoices = await this.stripe?.invoices.list({ customer: customer_id });
return invoices;
}
async getCustomer(customer_id: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const customer = await this.stripe.customers.retrieve(customer_id, { expand: [] })
return customer;
}
async createCustomer(email: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const customer = await this.stripe.customers.create({ email });
return customer;
}
async setCustomerInfo(customer_id: string, address: { line1: string, line2: string, city: string, country: string, postal_code: string, state: string }) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const customer = await this.stripe.customers.update(customer_id, {
address: {
line1: address.line1,
line2: address.line2,
city: address.city,
country: address.country,
postal_code: address.postal_code,
state: address.state
}
})
return customer.id;
}
async deleteCustomer(customer_id: string) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const { deleted } = await this.stripe.customers.del(customer_id);
return deleted;
}
async createStripeCode(plan: PREMIUM_TAG) {
if (this.disabledMode) return;
if (!this.stripe) throw Error('Stripe not initialized');
const INCUBATION_COUPON = 'sDD7Weh3';
if (plan === 'INCUBATION') {
await this.stripe.promotionCodes.create({
coupon: INCUBATION_COUPON,
active: true,
code: 'TESTCACCA1',
max_redemptions: 1,
})
return true;
}
return false;
}
async createSubscription(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 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;
if (!this.stripe) throw Error('Stripe not initialized');
const FREE_PLAN = getPlanFromTag('FREE');
const subscription = await this.stripe.subscriptions.create({
customer: customer_id,
items: [
{ price: this.testMode ? FREE_PLAN.PRICE_TEST : FREE_PLAN.PRICE, quantity: 1 }
]
});
return subscription;
}
}
const instance = new StripeService();
export default instance;

View File

@@ -80,7 +80,10 @@ export async function onPaymentSuccess(event: Event.InvoicePaidEvent) {
} }
} }
await addSubscriptionToUser(premiumData.user_id.toString(), plan, subscription_id, event.data.object.period_start, event.data.object.period_end); await addSubscriptionToUser(premiumData.user_id.toString(), plan, subscription_id,
event.data.object.lines.data[0].period.start,
event.data.object.lines.data[0].period.end
);
setTimeout(() => { setTimeout(() => {
if (plan.ID == 0) return; if (plan.ID == 0) return;

View File

@@ -17,6 +17,12 @@ console.log('Stripe started in', STRIPE_TESTMODE ? 'TESTMODE' : 'LIVEMODE');
const app = express(); const app = express();
app.use((req, res, next) => {
console.log(req.path);
next();
})
app.use('/webhook', webhookRouter); app.use('/webhook', webhookRouter);
app.use('/payment', paymentRouter); app.use('/payment', paymentRouter);

View File

@@ -5,14 +5,41 @@ import StripeService from '../services/StripeService';
import { sendJson } from '../Utils'; import { sendJson } from '../Utils';
import { PremiumModel } from '../shared/schema/PremiumSchema'; import { PremiumModel } from '../shared/schema/PremiumSchema';
import { Types } from 'mongoose'; import { Types } from 'mongoose';
import { UserModel } from '../shared/schema/UserSchema';
export const paymentRouter = Router(); export const paymentRouter = Router();
export const ZBodyCreateCustomer = z.object({
user_id: z.string()
});
paymentRouter.post('/create_customer', json(), async (req, res) => {
try {
const createCustomerData = ZBodyCreateCustomer.parse(req.body);
const user = await UserModel.findOne({ _id: createCustomerData.user_id });
if (!user) return sendJson(res, 400, { error: 'user not found' });
const customer = await StripeService.createCustomer(user.email);
const freesub = await StripeService.createFreeSubscription(customer.id);
await PremiumModel.create({
user_id: user.id,
customer_id: customer.id,
premium_type: 0,
subscription_id: freesub.id
})
return sendJson(res, 200, { ok: true });
} catch (ex) {
res.status(500).json({ error: ex.message });
}
});
export const ZBodyCreatePayment = z.object({ export const ZBodyCreatePayment = z.object({
user_id: z.string(), user_id: z.string(),
plan_id: z.number() plan_id: z.number()
}) });
paymentRouter.post('/create', json(), async (req, res) => { paymentRouter.post('/create', json(), async (req, res) => {
try { try {
@@ -42,3 +69,81 @@ paymentRouter.post('/create', json(), async (req, res) => {
res.status(500).json({ error: ex.message }); res.status(500).json({ error: ex.message });
} }
}); });
export const ZBodyInvoicesList = z.object({
user_id: z.string()
});
paymentRouter.post('/invoices_list', json(), async (req, res) => {
try {
const invoicesListData = ZBodyInvoicesList.parse(req.body);
const premiumData = await PremiumModel.findOne({ user_id: invoicesListData.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' });
const invoices = await StripeService.getInvoices(premiumData.customer_id);
return sendJson(res, 200, { invoices: invoices.data });
} catch (ex) {
res.status(500).json({ error: ex.message });
}
});
export const ZBodyCustomerInfo = z.object({
user_id: z.string()
});
paymentRouter.post('/customer_info', json(), async (req, res) => {
try {
const customerInfoData = ZBodyCustomerInfo.parse(req.body);
const premiumData = await PremiumModel.findOne({ user_id: customerInfoData.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' });
const customer = await StripeService.getCustomer(premiumData.customer_id);
if (!customer) return sendJson(res, 200, {});
if (customer.deleted === true) return sendJson(res, 200, {});
return sendJson(res, 200, customer.address);
} catch (ex) {
console.error(ex);
res.status(500).json({ error: ex.message });
}
});
export const ZBodyUpdateCustomerInfo = z.object({
user_id: z.string(),
address: z.object({
line1: z.string(),
line2: z.string(),
city: z.string(),
country: z.string(),
postal_code: z.string(),
state: z.string()
})
});
paymentRouter.post('/update_customer_info', json(), async (req, res) => {
try {
const updateCustomerInfoData = ZBodyUpdateCustomerInfo.parse(req.body);
const premiumData = await PremiumModel.findOne({ user_id: updateCustomerInfoData.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.setCustomerInfo(
premiumData.customer_id,
updateCustomerInfoData.address as any
);
return sendJson(res, 200, { ok: true });
} catch (ex) {
res.status(500).json({ error: ex.message });
}
});