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}`);
}
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") {
const userId = (event.data.object as Stripe.Checkout.Session)?.metadata
?.userId;
@ -307,11 +440,11 @@ export async function StripeWebhook(req: Request, res: Response) {
logger.info(`webhook checkout.session.expired: ${event.data.object.id}`);
} else {
logger.warn(`Ignoring unknown event: ${event.type}`);
}
} */
res.sendStatus(200);
} catch (error) {
logger.error("StripeWebhook", error as string);
logger.error("StripeWebhook err:", error as string);
res.status(500).send({ err: "invalid request" });
}
}

View File

@ -20,6 +20,7 @@ import {
CALENDAR_MIN_EARLIEST_BOOKING_TIME,
EMAIL_VERIFICATION_STATE,
PAYMENT_PLAN_SETTINGS,
PAYMENT_PLAN_STATUS,
Roles,
USER_ANALYTICS_ENABLED_DEFAULT,
} from "../utils/constants";
@ -175,6 +176,7 @@ export async function SignUp(req: Request, res: Response) {
analytics_enabled: USER_ANALYTICS_ENABLED_DEFAULT,
state: ACCOUNT_STATE.INIT_PAYMENT,
payment_plan: paymentPlan,
payment_plan_status: PAYMENT_PLAN_STATUS.TRAILING,
});
const checkoutSessionUrl = await CreateCheckoutSession(
@ -449,6 +451,9 @@ export async function GetUser(req: Request, res: Response) {
"language",
"analytics_enabled",
"payment_plan",
"payment_plan_status",
"payment_plan_trial_end",
"payment_plan_canceled_at",
"created_at",
],
});
@ -476,10 +481,14 @@ export async function GetUser(req: Request, res: Response) {
language: user.language,
analytics_enabled: user.analytics_enabled,
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,
// only temporary until we have a proper permissions system
permissions: [] as string[],
paymentPlanSettings: PAYMENT_PLAN_SETTINGS[user.payment_plan],
};
// 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
Session.update(

View File

@ -18,6 +18,9 @@ interface UserAttributes {
google_account_picture?: string;
analytics_enabled: boolean;
payment_plan: number;
payment_plan_status?: string;
payment_plan_trial_end?: number;
payment_plan_canceled_at?: number;
stripe_customer_id?: string;
}
@ -37,6 +40,9 @@ class User extends Model<UserAttributes> implements UserAttributes {
declare google_account_picture: string;
declare analytics_enabled: boolean;
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 created_at: Date;
}
@ -95,7 +101,7 @@ User.init(
},
google_account_picture: {
type: DataTypes.STRING(1050),
allowNull: true, // varchar(1050) needs to be set manually
allowNull: true,
},
analytics_enabled: {
type: DataTypes.BOOLEAN,
@ -105,6 +111,18 @@ User.init(
type: DataTypes.TINYINT,
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: {
type: DataTypes.STRING,
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 enum PAYMENT_PLAN {
DEMO = 0,
BASIC = 1,
PREMIUM = 2,
BASIC = 0,
PREMIUM = 1,
}
export const PAYMENT_PLAN_SETTINGS = [
{
// demo
maxEmployees: 5,
calendarMaxFutureBookingDays: 14,
},
{
// basic
id: "basic",
id: "basic", // used in the backend for identifiying the stripe pricing product
name: "Basic", // used in the frontend
maxEmployees: 15,
calendarMaxFutureBookingDays: 60,
},
{
// premium
id: "premium",
id: "premium", // used in the backend for identifiying the stripe pricing product
name: "Premium", // used in the frontend
maxEmployees: 20,
calendarMaxFutureBookingDays: 90,
},
];
export enum PAYMENT_PLAN_STATUS {
TRAILING = "trailing",
}

View File

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