change plan
parent
d7390e7024
commit
a41a0923da
|
@ -28,7 +28,7 @@
|
|||
"pino-pretty": "^10.3.1",
|
||||
"react-microsoft-clarity": "^1.2.0",
|
||||
"sequelize": "^6.35.2",
|
||||
"stripe": "^14.16.0",
|
||||
"stripe": "^14.23.0",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
"uuid": "^9.0.1",
|
||||
|
@ -2786,9 +2786,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/stripe": {
|
||||
"version": "14.16.0",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-14.16.0.tgz",
|
||||
"integrity": "sha512-1gOr2LzafWV84cPIO5Md/QPh4XVPLKULVuRpBVOV3Plq3seiHmg/eeOktX+hDl8jpNZuORHYaUJGrNqrABLwdg==",
|
||||
"version": "14.23.0",
|
||||
"resolved": "https://registry.npmjs.org/stripe/-/stripe-14.23.0.tgz",
|
||||
"integrity": "sha512-OPD7LqBmni6uDdqA05GGgMZyyRWxJOehONBNC9tYgY4Uh089EtXd6QLIgRGrqTDlQH3cA2BXo848nxwa/zsQzw==",
|
||||
"dependencies": {
|
||||
"@types/node": ">=8.1.0",
|
||||
"qs": "^6.11.0"
|
||||
|
|
|
@ -30,7 +30,7 @@
|
|||
"pino-pretty": "^10.3.1",
|
||||
"react-microsoft-clarity": "^1.2.0",
|
||||
"sequelize": "^6.35.2",
|
||||
"stripe": "^14.16.0",
|
||||
"stripe": "^14.23.0",
|
||||
"swagger-jsdoc": "^6.2.8",
|
||||
"swagger-ui-express": "^5.0.0",
|
||||
"uuid": "^9.0.1",
|
||||
|
|
|
@ -6,11 +6,9 @@ import {
|
|||
PAYMENT_PLAN,
|
||||
PAYMENT_PLAN_SETTINGS,
|
||||
} from "../utils/constants";
|
||||
import UserPendingPayment from "../models/userPendingPayment";
|
||||
import User from "../models/user";
|
||||
import { getUserSession, stripe } from "../utils/utils";
|
||||
import Store from "../models/store";
|
||||
import Session from "../models/session";
|
||||
import { telegramNotification } from "../utils/adminDashboard";
|
||||
|
||||
const ZeitAdlerBasicMonthly = "za-basic-monthly";
|
||||
const ZeitAdlerBasicYearly = "za-basic-yearly";
|
||||
|
@ -198,17 +196,57 @@ export async function CreateCheckoutSession(req: Request, res: Response) {
|
|||
subscriptions.data.length > 0 &&
|
||||
subscriptions.data[0].id !== priceId
|
||||
) {
|
||||
const latestSubscription = subscriptions.data[0];
|
||||
const currentSubscription = subscriptions.data[0];
|
||||
const currentItem = currentSubscription.items.data[0];
|
||||
|
||||
await stripe.subscriptions.update(latestSubscription.id, {
|
||||
items: [
|
||||
{
|
||||
id: latestSubscription.items.data[0].id,
|
||||
price: priceId,
|
||||
},
|
||||
],
|
||||
// Calculate the prorated credit for the old subscription
|
||||
const endCurrentSub = currentSubscription.current_period_end;
|
||||
const now = Math.floor(Date.now() / 1000);
|
||||
let proratedCredit = 0;
|
||||
if (currentItem.price.unit_amount !== null) {
|
||||
proratedCredit = Math.round(
|
||||
((endCurrentSub - now) /
|
||||
(endCurrentSub - currentSubscription.current_period_start)) *
|
||||
currentItem.price.unit_amount
|
||||
);
|
||||
}
|
||||
|
||||
// Create an invoice item for the prorated credit
|
||||
/*await stripe.invoiceItems.create({
|
||||
customer: user.stripe_customer_id,
|
||||
amount: -proratedCredit,
|
||||
currency: currentItem.price.currency,
|
||||
description: "Prorated credit for plan switch",
|
||||
}); */
|
||||
|
||||
const updatedSubscription = await stripe.subscriptions.update(
|
||||
currentSubscription.id,
|
||||
{
|
||||
cancel_at_period_end: false,
|
||||
proration_behavior: "always_invoice",
|
||||
items: [
|
||||
{
|
||||
id: currentSubscription.items.data[0].id,
|
||||
price: priceId,
|
||||
},
|
||||
],
|
||||
}
|
||||
);
|
||||
|
||||
// Attempt to pay any open invoices immediately, ensuring the customer pays for the new plan right away
|
||||
const invoice = await stripe.invoices.create({
|
||||
customer: user.stripe_customer_id,
|
||||
subscription: updatedSubscription.id,
|
||||
auto_advance: true, // Automatically pay the invoice if the customer has a default payment method
|
||||
});
|
||||
|
||||
// Finalize the invoice immediately to collect payment
|
||||
if (invoice.status !== "paid") {
|
||||
await stripe.invoices.finalizeInvoice(invoice.id, {
|
||||
auto_advance: true,
|
||||
});
|
||||
}
|
||||
|
||||
// update user
|
||||
|
||||
await User.update(
|
||||
|
@ -224,7 +262,12 @@ export async function CreateCheckoutSession(req: Request, res: Response) {
|
|||
|
||||
userLogger.info(
|
||||
userSession.user_id,
|
||||
`CreateCheckoutSession: update subscription ${latestSubscription.id} to ${priceId}`
|
||||
`CreateCheckoutSession: update subscription ${currentSubscription.id} to ${priceId}`
|
||||
);
|
||||
|
||||
telegramNotification(
|
||||
1,
|
||||
`User ${userSession.user_id} updated subscription ${currentSubscription.id} to ${priceId}`
|
||||
);
|
||||
|
||||
return res.status(200).json({ status: "ok" });
|
||||
|
@ -233,7 +276,7 @@ export async function CreateCheckoutSession(req: Request, res: Response) {
|
|||
|
||||
// create checkout session
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
let sessionData = {
|
||||
payment_method_types: ["card"],
|
||||
line_items: [
|
||||
{
|
||||
|
@ -243,20 +286,33 @@ export async function CreateCheckoutSession(req: Request, res: Response) {
|
|||
],
|
||||
billing_address_collection: "required",
|
||||
mode: "subscription",
|
||||
subscription_data: {
|
||||
trial_end: new Date(user.payment_plan_trial_end).getTime() / 1000,
|
||||
},
|
||||
|
||||
success_url: `${DASHBOARD_URL}/payment-plan`,
|
||||
cancel_url: `${DASHBOARD_URL}/payment-plan`,
|
||||
metadata: {
|
||||
userId: userSession.user_id,
|
||||
payment_plan_interval: lookupKey === ZeitAdlerBasicYearly ? 1 : 0,
|
||||
} /*
|
||||
},
|
||||
/*
|
||||
automatic_tax: {
|
||||
// see https://dashboard.stripe.com/settings/tax
|
||||
enabled: true,
|
||||
},*/,
|
||||
});
|
||||
},*/
|
||||
} as any;
|
||||
|
||||
if (
|
||||
user.payment_plan_trial_end !== null &&
|
||||
new Date(user.payment_plan_trial_end) > new Date()
|
||||
) {
|
||||
sessionData = {
|
||||
...sessionData,
|
||||
subscription_data: {
|
||||
trial_end: new Date(user.payment_plan_trial_end).getTime() / 1000,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const session = await stripe.checkout.sessions.create(sessionData);
|
||||
/*
|
||||
await UserPendingPayment.create({
|
||||
payment_session_id: session.id,
|
||||
|
@ -280,7 +336,7 @@ export async function CreateCheckoutSession(req: Request, res: Response) {
|
|||
|
||||
// user is redirected to dashboard checkout page after successful payment
|
||||
// and then makes request to this endpoint to create a user session
|
||||
export async function CheckoutSuccess(req: Request, res: Response) {
|
||||
/*export async function CheckoutSuccess(req: Request, res: Response) {
|
||||
try {
|
||||
const { sessionId } = req.body;
|
||||
|
||||
|
@ -365,7 +421,7 @@ export async function CheckoutCanceled(req: Request, res: Response) {
|
|||
logger.error("CheckoutCanceled", error as string);
|
||||
res.status(500).send({ err: "invalid request" });
|
||||
}
|
||||
}
|
||||
}*/
|
||||
|
||||
export async function StripeWebhook(req: Request, res: Response) {
|
||||
try {
|
||||
|
@ -449,6 +505,11 @@ export async function StripeWebhook(req: Request, res: Response) {
|
|||
event.data.object
|
||||
)}`
|
||||
);
|
||||
|
||||
telegramNotification(
|
||||
1,
|
||||
`User ${userId} completed checkout session ${session.id}`
|
||||
);
|
||||
} else {
|
||||
logger.error(
|
||||
`StripeWebhook: checkout.session.completed: user not found for userId: ${userId}`
|
||||
|
@ -511,6 +572,10 @@ export async function StripeWebhook(req: Request, res: Response) {
|
|||
payment_plan_status: status,
|
||||
/*payment_plan_trial_end:
|
||||
trialEnd !== null ? new Date(trialEnd * 1000) : null, */
|
||||
payment_plan_cancel_at:
|
||||
subscription.cancel_at !== null
|
||||
? new Date(subscription.cancel_at * 1000)
|
||||
: null, // this is if the user cancels the subscription after the trial period
|
||||
payment_plan_canceled_at:
|
||||
canceledAt !== null ? new Date(canceledAt * 1000) : null,
|
||||
};
|
||||
|
@ -528,57 +593,13 @@ export async function StripeWebhook(req: Request, res: Response) {
|
|||
);
|
||||
break;
|
||||
default:
|
||||
logger.warn(`StripeWebhook: Ignoring unknown event: ${event.type}`);
|
||||
logger.warn(
|
||||
`StripeWebhook: Ignoring unknown event: ${
|
||||
event.type
|
||||
} data: ${JSON.stringify(event.data)}`
|
||||
);
|
||||
break;
|
||||
}
|
||||
/*
|
||||
if (event.type === "checkout.session.completed") {
|
||||
const userId = (event.data.object as Stripe.Checkout.Session)?.metadata
|
||||
?.userId;
|
||||
|
||||
// delete user pending payment
|
||||
|
||||
logger.info(
|
||||
"delete user pending payment",
|
||||
(event.data.object as Stripe.Checkout.Session).id
|
||||
);
|
||||
|
||||
const pendingPayment = await UserPendingPayment.findOne({
|
||||
where: {
|
||||
payment_session_id: (event.data.object as Stripe.Checkout.Session).id,
|
||||
},
|
||||
});
|
||||
|
||||
if (pendingPayment !== null) {
|
||||
await pendingPayment.destroy();
|
||||
|
||||
const 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,
|
||||
},
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
} else if (event.type === "checkout.session.expired") {
|
||||
logger.info(`webhook checkout.session.expired: ${event.data.object.id}`);
|
||||
} else {
|
||||
logger.warn(`Ignoring unknown event: ${event.type}`);
|
||||
} */
|
||||
|
||||
res.sendStatus(200);
|
||||
} catch (error) {
|
||||
|
@ -599,7 +620,11 @@ export async function GetBillingPortal(req: Request, res: Response) {
|
|||
where: {
|
||||
user_id: userSession.user_id,
|
||||
},
|
||||
attributes: ["stripe_customer_id"],
|
||||
attributes: [
|
||||
"payment_plan",
|
||||
"payment_plan_interval",
|
||||
"stripe_customer_id",
|
||||
],
|
||||
});
|
||||
|
||||
if (user === null || user.stripe_customer_id === null) {
|
||||
|
@ -611,6 +636,74 @@ export async function GetBillingPortal(req: Request, res: Response) {
|
|||
return res.status(400).send({ err: "invalid request" });
|
||||
}
|
||||
|
||||
// after the trial period, the subscribtion is deleted
|
||||
// so we need to check if the user has a subscription or not
|
||||
|
||||
const subscriptions = await stripe.subscriptions.list({
|
||||
customer: user.stripe_customer_id,
|
||||
limit: 1,
|
||||
});
|
||||
|
||||
console.log("subs", subscriptions.data.length, user.payment_plan);
|
||||
|
||||
if (subscriptions.data.length === 0) {
|
||||
let lookupKey = [ZeitAdlerBasicMonthly, ZeitAdlerBasicYearly];
|
||||
|
||||
console.log("user", user.payment_plan_interval);
|
||||
|
||||
await loadPrices();
|
||||
|
||||
let priceId = cachedPrices.find(
|
||||
(price) => price.lookup_key === lookupKey[user.payment_plan_interval]
|
||||
)?.id;
|
||||
|
||||
if (priceId === undefined) {
|
||||
return res.status(400).send({ err: "invalid request" });
|
||||
}
|
||||
|
||||
console.log(
|
||||
"priceId",
|
||||
priceId,
|
||||
user.payment_plan_interval,
|
||||
user.payment_plan_trial_end
|
||||
);
|
||||
|
||||
const session = await stripe.checkout.sessions.create({
|
||||
payment_method_types: ["card"],
|
||||
line_items: [
|
||||
{
|
||||
price: priceId,
|
||||
quantity: 1,
|
||||
},
|
||||
],
|
||||
billing_address_collection: "required",
|
||||
mode: "subscription",
|
||||
/*subscription_data: {
|
||||
trial_end: new Date(user.payment_plan_trial_end).getTime() / 1000,
|
||||
},*/
|
||||
success_url: `${DASHBOARD_URL}/payment-plan`,
|
||||
cancel_url: `${DASHBOARD_URL}/payment-plan`,
|
||||
metadata: {
|
||||
userId: userSession.user_id,
|
||||
payment_plan_interval: user.payment_plan_interval,
|
||||
},
|
||||
/*
|
||||
automatic_tax: {
|
||||
// see https://dashboard.stripe.com/settings/tax
|
||||
enabled: true,
|
||||
},*/
|
||||
});
|
||||
|
||||
userLogger.info(
|
||||
userSession.user_id,
|
||||
`GetBillingPortal: create checkout session ${session.id}`
|
||||
);
|
||||
|
||||
return res.status(200).send({
|
||||
url: session.url,
|
||||
});
|
||||
}
|
||||
|
||||
const session = await stripe.billingPortal.sessions.create({
|
||||
customer: user.stripe_customer_id,
|
||||
return_url: `${DASHBOARD_URL}/payment-plan`,
|
||||
|
|
|
@ -474,6 +474,7 @@ export async function GetUser(req: Request, res: Response) {
|
|||
"analytics_enabled",
|
||||
"payment_plan",
|
||||
"payment_plan_trial_end",
|
||||
"payment_plan_cancel_at",
|
||||
"payment_plan_canceled_at",
|
||||
"created_at",
|
||||
],
|
||||
|
@ -517,7 +518,11 @@ export async function GetUser(req: Request, res: Response) {
|
|||
user.payment_plan === PAYMENT_PLAN.TRAILING ||
|
||||
user.payment_plan_canceled_at !== null
|
||||
) {
|
||||
respData.user["payment_plan_trial_end"] = user.payment_plan_trial_end;
|
||||
if (user.payment_plan_cancel_at !== null) {
|
||||
respData.user["payment_plan_trial_end"] = user.payment_plan_cancel_at;
|
||||
} else {
|
||||
respData.user["payment_plan_trial_end"] = user.payment_plan_trial_end;
|
||||
}
|
||||
}
|
||||
|
||||
// if user is not a store master, then check if user is a worker
|
||||
|
@ -557,6 +562,8 @@ export async function GetUser(req: Request, res: Response) {
|
|||
user.payment_plan
|
||||
}. Possible highest payment plan is ${PAYMENT_PLAN_SETTINGS.length - 1}`
|
||||
);
|
||||
|
||||
return res.status(400).send({ err: "invalid request" });
|
||||
}
|
||||
|
||||
// update user session last_used
|
||||
|
|
|
@ -17,10 +17,11 @@ interface UserAttributes {
|
|||
google_account_name?: string;
|
||||
google_account_picture?: string;
|
||||
analytics_enabled: boolean;
|
||||
payment_plan: number;
|
||||
payment_plan_interval?: number;
|
||||
payment_plan_trial_end?: Date;
|
||||
payment_plan_canceled_at?: Date;
|
||||
payment_plan: number; // 0 trailing, 1 basic
|
||||
payment_plan_interval?: number; // how often the payment plan is charged (e.g. 0 monthly, 1 yearly)
|
||||
payment_plan_trial_end?: Date; // when the payment plan trial ends
|
||||
payment_plan_cancel_at?: Date; // when the payment plan will be canceled (e.g. after trial)
|
||||
payment_plan_canceled_at?: Date; // when the payment plan was canceled
|
||||
stripe_customer_id?: string;
|
||||
}
|
||||
|
||||
|
@ -42,6 +43,7 @@ class User extends Model<UserAttributes> implements UserAttributes {
|
|||
declare payment_plan: number;
|
||||
declare payment_plan_interval: number;
|
||||
declare payment_plan_trial_end: Date;
|
||||
declare payment_plan_cancel_at: Date;
|
||||
declare payment_plan_canceled_at: Date;
|
||||
declare stripe_customer_id: string;
|
||||
declare created_at: Date;
|
||||
|
@ -119,6 +121,10 @@ User.init(
|
|||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
payment_plan_cancel_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
},
|
||||
payment_plan_canceled_at: {
|
||||
type: DataTypes.DATE,
|
||||
allowNull: true,
|
||||
|
|
|
@ -10,8 +10,8 @@ router.get("/prices/:productId", paymentController.GetPrices);
|
|||
// webhook is registered in server.ts as it needs registered before the body parser
|
||||
|
||||
router.post("/checkout", paymentController.CreateCheckoutSession);
|
||||
router.post("/checkout/success", paymentController.CheckoutSuccess);
|
||||
router.post("/checkout/canceled", paymentController.CheckoutCanceled);
|
||||
// router.post("/checkout/success", paymentController.CheckoutSuccess);
|
||||
// router.post("/checkout/canceled", paymentController.CheckoutCanceled);
|
||||
router.get("/portal", paymentController.GetBillingPortal);
|
||||
|
||||
export default router;
|
||||
|
|
|
@ -183,7 +183,9 @@ export function isCalendarMinEarliestBookingTimeValid(
|
|||
}
|
||||
|
||||
export function isPaymentPlanValid(paymentPlan: number) {
|
||||
return paymentPlan >= PAYMENT_PLAN.BASIC && paymentPlan <= PAYMENT_PLAN.BASIC;
|
||||
return (
|
||||
paymentPlan >= PAYMENT_PLAN.TRAILING && paymentPlan <= PAYMENT_PLAN.BASIC
|
||||
);
|
||||
}
|
||||
|
||||
export function isCompanyNameValid(companyName: string) {
|
||||
|
|
Loading…
Reference in New Issue