payment plan settings
parent
d2489b3093
commit
5321476433
|
@ -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" });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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(
|
||||||
|
|
|
@ -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,
|
||||||
|
|
|
@ -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",
|
||||||
|
}
|
||||||
|
|
|
@ -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
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
Loading…
Reference in New Issue