payment plan settings

main
alex 2024-02-24 07:33:13 +01:00
parent d2489b3093
commit 5321476433
5 changed files with 184 additions and 16 deletions

View File

@ -261,6 +261,139 @@ export async function StripeWebhook(req: Request, res: Response) {
return res.status(400).send(`Webhook Error: ${error}`); return res.status(400).send(`Webhook Error: ${error}`);
} }
let userId;
let customerId;
let user;
switch (event.type) {
case "checkout.session.completed":
userId = (event.data.object as Stripe.Checkout.Session)?.metadata
?.userId;
if (userId === undefined) {
logger.error(
"StripeWebhook: checkout.session.completed: userId undefined"
);
break;
}
// delete user pending payment
const pendingPayment = await UserPendingPayment.findOne({
where: {
payment_session_id: (event.data.object as Stripe.Checkout.Session)
.id,
},
});
if (pendingPayment !== null) {
await pendingPayment.destroy();
let user = await User.findOne({
where: {
user_id: userId,
},
});
if (user !== null && user.state === ACCOUNT_STATE.INIT_PAYMENT) {
// update user state
await User.update(
{
state: ACCOUNT_STATE.ACTIVE,
stripe_customer_id: (
event.data.object as Stripe.Checkout.Session
).customer as string,
},
{
where: {
user_id: userId,
},
}
);
userLogger.info(
userId as string,
"StripeWebhook: user state updated to ACTIVE"
);
}
}
break;
case "checkout.session.expired":
logger.info(
`StripeWebhook: checkout.session.expired: ${event.data.object.id}`
);
break;
case "billing_portal.session.created":
customerId = event.data.object.customer as string;
user = await User.findOne({
where: {
stripe_customer_id: customerId,
},
});
if (user === null) {
logger.error(
`StripeWebhook: customer.subscription.updated: user not found for customerId: ${customerId}`
);
break;
}
userLogger.info(
user.user_id as string,
`StripeWebhook: billing_portal.session.created: customerId: ${customerId}`
);
break;
case "customer.subscription.updated":
const subscription = event.data.object as Stripe.Subscription;
const status = subscription.status;
const trialEnd = subscription.trial_end as number | null;
const canceledAt = subscription.canceled_at;
customerId = subscription.customer as string;
logger.info(
`StripeWebhook: customer.subscription.updated: ${subscription.id} status: ${status} trialEnd: ${trialEnd} canceledAt: ${canceledAt} customerId: ${customerId}`
);
user = await User.findOne({
where: {
stripe_customer_id: customerId,
},
});
if (user === null) {
logger.error(
`StripeWebhook: customer.subscription.updated: user not found for customerId: ${customerId}`
);
break;
}
let updateData: any = {
payment_plan_status: status,
payment_plan_trial_end: trialEnd !== null ? trialEnd : null,
payment_plan_canceled_at: canceledAt !== null ? canceledAt : null,
};
logger.info(
`StripeWebhook: customer.subscription.updated: updating user: ${
user.user_id
} ${JSON.stringify(updateData)}`
);
await User.update(updateData, {
where: {
stripe_customer_id: customerId,
},
});
break;
default:
logger.warn(`StripeWebhook: Ignoring unknown event: ${event.type}`);
break;
}
/*
if (event.type === "checkout.session.completed") { if (event.type === "checkout.session.completed") {
const userId = (event.data.object as Stripe.Checkout.Session)?.metadata const userId = (event.data.object as Stripe.Checkout.Session)?.metadata
?.userId; ?.userId;
@ -307,11 +440,11 @@ export async function StripeWebhook(req: Request, res: Response) {
logger.info(`webhook checkout.session.expired: ${event.data.object.id}`); logger.info(`webhook checkout.session.expired: ${event.data.object.id}`);
} else { } else {
logger.warn(`Ignoring unknown event: ${event.type}`); logger.warn(`Ignoring unknown event: ${event.type}`);
} } */
res.sendStatus(200); res.sendStatus(200);
} catch (error) { } catch (error) {
logger.error("StripeWebhook", error as string); logger.error("StripeWebhook err:", error as string);
res.status(500).send({ err: "invalid request" }); res.status(500).send({ err: "invalid request" });
} }
} }

View File

@ -20,6 +20,7 @@ import {
CALENDAR_MIN_EARLIEST_BOOKING_TIME, CALENDAR_MIN_EARLIEST_BOOKING_TIME,
EMAIL_VERIFICATION_STATE, EMAIL_VERIFICATION_STATE,
PAYMENT_PLAN_SETTINGS, PAYMENT_PLAN_SETTINGS,
PAYMENT_PLAN_STATUS,
Roles, Roles,
USER_ANALYTICS_ENABLED_DEFAULT, USER_ANALYTICS_ENABLED_DEFAULT,
} from "../utils/constants"; } from "../utils/constants";
@ -175,6 +176,7 @@ export async function SignUp(req: Request, res: Response) {
analytics_enabled: USER_ANALYTICS_ENABLED_DEFAULT, analytics_enabled: USER_ANALYTICS_ENABLED_DEFAULT,
state: ACCOUNT_STATE.INIT_PAYMENT, state: ACCOUNT_STATE.INIT_PAYMENT,
payment_plan: paymentPlan, payment_plan: paymentPlan,
payment_plan_status: PAYMENT_PLAN_STATUS.TRAILING,
}); });
const checkoutSessionUrl = await CreateCheckoutSession( const checkoutSessionUrl = await CreateCheckoutSession(
@ -449,6 +451,9 @@ export async function GetUser(req: Request, res: Response) {
"language", "language",
"analytics_enabled", "analytics_enabled",
"payment_plan", "payment_plan",
"payment_plan_status",
"payment_plan_trial_end",
"payment_plan_canceled_at",
"created_at", "created_at",
], ],
}); });
@ -476,10 +481,14 @@ export async function GetUser(req: Request, res: Response) {
language: user.language, language: user.language,
analytics_enabled: user.analytics_enabled, analytics_enabled: user.analytics_enabled,
payment_plan: user.payment_plan, payment_plan: user.payment_plan,
payment_plan_status: user.payment_plan_status,
payment_plan_trial_end: user.payment_plan_trial_end,
payment_plan_canceled_at: user.payment_plan_canceled_at,
}, },
stores: stores, stores: stores,
// only temporary until we have a proper permissions system // only temporary until we have a proper permissions system
permissions: [] as string[], permissions: [] as string[],
paymentPlanSettings: PAYMENT_PLAN_SETTINGS[user.payment_plan],
}; };
// if user is not a store master, then check if user is a worker // if user is not a store master, then check if user is a worker
@ -512,6 +521,16 @@ export async function GetUser(req: Request, res: Response) {
); );
} }
// send error if user has invalid payment plan
if (!isPaymentPlanValid(user.payment_plan)) {
userLogger.error(
session.user_id,
`User has invalid payment plan: ${
user.payment_plan
}. Possible highest payment plan is ${PAYMENT_PLAN_SETTINGS.length - 1}`
);
}
// update user session last_used // update user session last_used
Session.update( Session.update(

View File

@ -18,6 +18,9 @@ interface UserAttributes {
google_account_picture?: string; google_account_picture?: string;
analytics_enabled: boolean; analytics_enabled: boolean;
payment_plan: number; payment_plan: number;
payment_plan_status?: string;
payment_plan_trial_end?: number;
payment_plan_canceled_at?: number;
stripe_customer_id?: string; stripe_customer_id?: string;
} }
@ -37,6 +40,9 @@ class User extends Model<UserAttributes> implements UserAttributes {
declare google_account_picture: string; declare google_account_picture: string;
declare analytics_enabled: boolean; declare analytics_enabled: boolean;
declare payment_plan: number; declare payment_plan: number;
declare payment_plan_status: string;
declare payment_plan_trial_end: number;
declare payment_plan_canceled_at: number;
declare stripe_customer_id: string; declare stripe_customer_id: string;
declare created_at: Date; declare created_at: Date;
} }
@ -95,7 +101,7 @@ User.init(
}, },
google_account_picture: { google_account_picture: {
type: DataTypes.STRING(1050), type: DataTypes.STRING(1050),
allowNull: true, // varchar(1050) needs to be set manually allowNull: true,
}, },
analytics_enabled: { analytics_enabled: {
type: DataTypes.BOOLEAN, type: DataTypes.BOOLEAN,
@ -105,6 +111,18 @@ User.init(
type: DataTypes.TINYINT, type: DataTypes.TINYINT,
allowNull: false, allowNull: false,
}, },
payment_plan_status: {
type: DataTypes.STRING,
allowNull: true,
},
payment_plan_trial_end: {
type: DataTypes.INTEGER,
allowNull: true,
},
payment_plan_canceled_at: {
type: DataTypes.INTEGER,
allowNull: true,
},
stripe_customer_id: { stripe_customer_id: {
type: DataTypes.STRING, type: DataTypes.STRING,
allowNull: true, allowNull: true,

View File

@ -89,27 +89,25 @@ export const ACCOUNT_EXPORT_URL = `${DASHBOARD_URL}/api/v1/user/profile/export/`
export const TERMIN_PLANNER_URL = process.env.TERMIN_PLANNER_URL; export const TERMIN_PLANNER_URL = process.env.TERMIN_PLANNER_URL;
export enum PAYMENT_PLAN { export enum PAYMENT_PLAN {
DEMO = 0, BASIC = 0,
BASIC = 1, PREMIUM = 1,
PREMIUM = 2,
} }
export const PAYMENT_PLAN_SETTINGS = [ export const PAYMENT_PLAN_SETTINGS = [
{ {
// demo id: "basic", // used in the backend for identifiying the stripe pricing product
maxEmployees: 5, name: "Basic", // used in the frontend
calendarMaxFutureBookingDays: 14,
},
{
// basic
id: "basic",
maxEmployees: 15, maxEmployees: 15,
calendarMaxFutureBookingDays: 60, calendarMaxFutureBookingDays: 60,
}, },
{ {
// premium id: "premium", // used in the backend for identifiying the stripe pricing product
id: "premium", name: "Premium", // used in the frontend
maxEmployees: 20, maxEmployees: 20,
calendarMaxFutureBookingDays: 90, calendarMaxFutureBookingDays: 90,
}, },
]; ];
export enum PAYMENT_PLAN_STATUS {
TRAILING = "trailing",
}

View File

@ -184,7 +184,7 @@ export function isCalendarMinEarliestBookingTimeValid(
export function isPaymentPlanValid(paymentPlan: number) { export function isPaymentPlanValid(paymentPlan: number) {
return ( return (
paymentPlan !== PAYMENT_PLAN.DEMO && paymentPlan <= PAYMENT_PLAN.PREMIUM paymentPlan >= PAYMENT_PLAN.BASIC && paymentPlan <= PAYMENT_PLAN.PREMIUM
); );
} }