diff --git a/src/controllers/paymentController.ts b/src/controllers/paymentController.ts index 7de2ed3..104de72 100644 --- a/src/controllers/paymentController.ts +++ b/src/controllers/paymentController.ts @@ -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" }); } } diff --git a/src/controllers/userController.ts b/src/controllers/userController.ts index 28bb928..e5f3722 100644 --- a/src/controllers/userController.ts +++ b/src/controllers/userController.ts @@ -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( diff --git a/src/models/user.ts b/src/models/user.ts index 2e543e7..b76598e 100644 --- a/src/models/user.ts +++ b/src/models/user.ts @@ -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 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, diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 90dd23f..8083deb 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -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", +} diff --git a/src/validator/validator.ts b/src/validator/validator.ts index b176f91..b11fd3e 100644 --- a/src/validator/validator.ts +++ b/src/validator/validator.ts @@ -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 ); }